diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d169a7767..d4264434ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,10 @@ jobs: node-version-file: '.nvmrc' cache: 'npm' + - name: 'Run lockfile check' + run: |- + npm run check:lockfile + - name: 'Install dependencies' run: |- npm ci diff --git a/package-lock.json b/package-lock.json index 0bd6785408..2f7cb92f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14567,6 +14567,8 @@ }, "packages/cli/node_modules/@testing-library/dom": { "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", "peer": true, @@ -14602,6 +14604,8 @@ }, "packages/cli/node_modules/@testing-library/react": { "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, "license": "MIT", "dependencies": { @@ -14653,6 +14657,8 @@ }, "packages/cli/node_modules/aria-query": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -14662,6 +14668,8 @@ }, "packages/cli/node_modules/emoji-regex": { "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, "packages/cli/node_modules/react-is": { @@ -14674,6 +14682,8 @@ }, "packages/cli/node_modules/string-width": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -14786,6 +14796,8 @@ }, "packages/core/node_modules/ignore": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", "engines": { "node": ">= 4" diff --git a/package.json b/package.json index 07f66bf682..a1e8ad80dc 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "prepare:package": "node scripts/prepare-package.js", "release:version": "node scripts/version.js", "telemetry": "node scripts/telemetry.js", + "check:lockfile": "node scripts/check-lockfile.js", "clean": "node scripts/clean.js" }, "bin": { diff --git a/scripts/check-lockfile.js b/scripts/check-lockfile.js new file mode 100644 index 0000000000..7cda57a7d6 --- /dev/null +++ b/scripts/check-lockfile.js @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, '..'); +const lockfilePath = join(root, 'package-lock.json'); + +function readJsonFile(filePath) { + try { + const fileContent = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(fileContent); + } catch (error) { + console.error(`Error reading or parsing ${filePath}:`, error); + return null; + } +} + +console.log('Checking lockfile...'); + +const lockfile = readJsonFile(lockfilePath); +if (lockfile === null) { + process.exit(1); +} +const packages = lockfile.packages || {}; +const invalidPackages = []; + +for (const [location, details] of Object.entries(packages)) { + // 1. Skip the root package itself. + if (location === '') { + continue; + } + + // 2. Skip local workspace packages. + // They are identifiable in two ways: + // a) As a symlink within node_modules. + // b) As the source package definition, whose path is not in node_modules. + if (details.link === true || !location.includes('node_modules')) { + continue; + } + + // 3. Any remaining package should be a third-party dependency. + // 1) Registry package with both "resolved" and "integrity" fields is valid. + if (details.resolved && details.integrity) { + continue; + } + // 2) Git and file dependencies only need a "resolved" field. + const isGitOrFileDep = + details.resolved?.startsWith('git') || + details.resolved?.startsWith('file:'); + if (isGitOrFileDep) { + continue; + } + + // Mark the left dependency as invalid. + invalidPackages.push(location); +} + +if (invalidPackages.length > 0) { + console.error( + '\nError: The following dependencies in package-lock.json are missing the "resolved" or "integrity" field:', + ); + invalidPackages.forEach((pkg) => console.error(`- ${pkg}`)); + process.exitCode = 1; +} else { + console.log('Lockfile check passed.'); + process.exitCode = 0; +}