Validate Yarn lockfile selectors (#14663)
This commit is contained in:
@@ -34,6 +34,10 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Validate Yarn lockfile
|
||||
run: yarn test-yarn-lock && yarn verify-yarn-lock
|
||||
working-directory: Extension
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install ${{ inputs.yarn-args }}
|
||||
working-directory: Extension
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const dependencySections = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
||||
|
||||
function parseLockfileKey(key) {
|
||||
const selectors = [];
|
||||
let selectorStart = 0;
|
||||
let quoted = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let index = 0; index < key.length; index++) {
|
||||
const character = key[index];
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character === '\\' && quoted) {
|
||||
escaped = true;
|
||||
} else if (character === '"') {
|
||||
quoted = !quoted;
|
||||
} else if (character === ',' && !quoted) {
|
||||
selectors.push(key.slice(selectorStart, index));
|
||||
selectorStart = index + 1;
|
||||
}
|
||||
}
|
||||
selectors.push(key.slice(selectorStart));
|
||||
|
||||
return selectors.map(selector => {
|
||||
const trimmedSelector = selector.trim();
|
||||
return trimmedSelector.startsWith('"') ? JSON.parse(trimmedSelector) : trimmedSelector;
|
||||
});
|
||||
}
|
||||
|
||||
function parseLockfileSelectors(lockfile) {
|
||||
const selectors = new Set();
|
||||
for (const line of lockfile.split(/\r?\n/)) {
|
||||
if (/^[^\s#].*:\s*$/.test(line)) {
|
||||
for (const selector of parseLockfileKey(line.replace(/:\s*$/, ''))) {
|
||||
selectors.add(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function getResolutionPackageName(pattern) {
|
||||
const segments = pattern.split('/');
|
||||
const packageName = segments.at(-1);
|
||||
const scope = segments.at(-2);
|
||||
return scope?.startsWith('@') ? `${scope}/${packageName}` : packageName;
|
||||
}
|
||||
|
||||
function getExpectedSelectors(manifest) {
|
||||
const selectors = [];
|
||||
for (const section of dependencySections) {
|
||||
for (const [packageName, range] of Object.entries(manifest[section] ?? {})) {
|
||||
selectors.push(`${packageName}@${range}`);
|
||||
}
|
||||
}
|
||||
for (const [pattern, range] of Object.entries(manifest.resolutions ?? {})) {
|
||||
selectors.push(`${getResolutionPackageName(pattern)}@${range}`);
|
||||
}
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function findMissingSelectors(manifest, lockfile) {
|
||||
const lockfileSelectors = parseLockfileSelectors(lockfile);
|
||||
return getExpectedSelectors(manifest)
|
||||
.filter(selector => !lockfileSelectors.has(selector))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function validateYarnLock(packageJsonPath, yarnLockPath) {
|
||||
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const lockfile = fs.readFileSync(yarnLockPath, 'utf8');
|
||||
const missingSelectors = findMissingSelectors(manifest, lockfile);
|
||||
if (missingSelectors.length > 0) {
|
||||
throw new Error(`yarn.lock is missing selectors required by package.json:\n${missingSelectors.map(selector => ` ${selector}`).join('\n')}\nRun yarn install to update yarn.lock.`);
|
||||
}
|
||||
}
|
||||
|
||||
const invokedUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : undefined;
|
||||
if (invokedUrl === import.meta.url) {
|
||||
const extensionRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const packageJsonPath = process.argv[2] ?? path.join(extensionRoot, 'package.json');
|
||||
const yarnLockPath = process.argv[3] ?? path.join(extensionRoot, 'yarn.lock');
|
||||
|
||||
try {
|
||||
validateYarnLock(packageJsonPath, yarnLockPath);
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export { findMissingSelectors, getExpectedSelectors, parseLockfileSelectors, validateYarnLock };
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { findMissingSelectors } from './verifyYarnLock.mjs';
|
||||
|
||||
test('reports a stale resolution selector', () => {
|
||||
const manifest = { resolutions: { 'fast-uri': '^3.1.5' } };
|
||||
const lockfile = `fast-uri@^3.0.1, fast-uri@^3.1.4:
|
||||
version "3.1.5"
|
||||
`;
|
||||
|
||||
assert.deepEqual(findMissingSelectors(manifest, lockfile), ['fast-uri@^3.1.5']);
|
||||
});
|
||||
|
||||
test('accepts direct, scoped, and nested resolution selectors', () => {
|
||||
const manifest = {
|
||||
dependencies: { '@scope/direct': '^1.0.0' },
|
||||
devDependencies: { 'gulp-typescript': '^5.0.1' },
|
||||
resolutions: {
|
||||
'@scope/resolved': '^2.0.0',
|
||||
'gulp-typescript/**/glob-parent': '^5.1.2',
|
||||
'parent/**/@nested/package': '~3.0.0'
|
||||
}
|
||||
};
|
||||
const lockfile = `"@nested/package@~3.0.0":
|
||||
version "3.0.1"
|
||||
|
||||
"@scope/direct@^1.0.0":
|
||||
version "1.0.0"
|
||||
|
||||
"@scope/resolved@^2.0.0":
|
||||
version "2.0.0"
|
||||
|
||||
glob-parent@^3.1.0, glob-parent@^5.1.2:
|
||||
version "5.1.2"
|
||||
|
||||
gulp-typescript@^5.0.1:
|
||||
version "5.0.1"
|
||||
`;
|
||||
|
||||
assert.deepEqual(findMissingSelectors(manifest, lockfile), []);
|
||||
});
|
||||
@@ -7163,6 +7163,8 @@
|
||||
"scripts": "ts-node -T .scripts/scripts.ts",
|
||||
"show": "ts-node -T .scripts/clean.ts show",
|
||||
"clean": "ts-node -T .scripts/clean.ts",
|
||||
"test-yarn-lock": "node --test .scripts/verifyYarnLock.test.mjs",
|
||||
"verify-yarn-lock": "node .scripts/verifyYarnLock.mjs",
|
||||
"test": "yarn install && (yarn verify prep --quiet || yarn prep) && (yarn verify compiled --quiet || yarn build) && ts-node -T .scripts/test.ts",
|
||||
"code": "yarn install && (yarn verify compiled --quiet || yarn build) && yarn verify binaries && ts-node -T .scripts/code.ts",
|
||||
"verify": "ts-node -T .scripts/verify.ts",
|
||||
|
||||
Reference in New Issue
Block a user