feat: updated to new crossversion lockfile parser to facilitate upgrading to yarn 3

Signed-off-by: Mark David Avery <mark@webark.cc>
This commit is contained in:
Mark David Avery
2022-05-07 17:30:09 -07:00
parent d06fbf7bb0
commit 4f73352608
6 changed files with 152 additions and 14 deletions
+1
View File
@@ -58,6 +58,7 @@
"@typescript-eslint/eslint-plugin": "^5.9.0",
"@typescript-eslint/parser": "^5.9.0",
"@yarnpkg/lockfile": "^1.1.0",
"@yarnpkg/parsers": "^3.0.0-rc.4",
"bfj": "^7.0.2",
"buffer": "^6.0.3",
"chalk": "^4.0.0",
+6 -1
View File
@@ -25,9 +25,14 @@ describe('createPackageVersionProvider', () => {
mockFs.restore();
});
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
it('should provide package versions', async () => {
mockFs({
'yarn.lock': `
'yarn.lock': `${HEADER}
"a@^0.1.0":
version "0.1.5"
@@ -153,3 +153,92 @@ describe('Lockfile', () => {
expect(lockfile.toString()).toBe(mockBDedup);
});
});
const newHeader = `# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 6
cacheKey: 8
`;
const mockANew = `${newHeader}
a@^1:
version: 1.0.1
dependencies:
b: ^2
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
b@2.0.x:
version: 2.0.1
b@^2:
version: 2.0.0
`;
const mockANewDedup = `${newHeader}
a@^1:
version: 1.0.1
dependencies:
b: ^2
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
b@2.0.x:
version: 2.0.1
b@^2:
version: 2.0.1
`;
describe('New Lockfile', () => {
afterEach(() => {
mockFs.restore();
});
it('should load and serialize mockANew', async () => {
mockFs({
'/yarn.lock': mockANew,
});
const lockfile = await Lockfile.load('/yarn.lock');
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
]);
expect(lockfile.toString()).toBe(mockANew);
});
it('should deduplicate and save mockANew', async () => {
mockFs({
'/yarn.lock': mockANew,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze();
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
newVersions: [
{
name: 'b',
range: '^2',
oldVersion: '2.0.0',
newVersion: '2.0.1',
},
],
});
expect(lockfile.toString()).toBe(mockANew);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockANewDedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockANew);
await expect(lockfile.save()).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewDedup,
);
});
});
+38 -8
View File
@@ -16,10 +16,8 @@
import fs from 'fs-extra';
import semver from 'semver';
import {
parse as parseLockfile,
stringify as stringifyLockfile,
} from '@yarnpkg/lockfile';
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
@@ -66,9 +64,40 @@ type AnalyzeResult = {
newRanges: AnalyzeResultNewRange[];
};
function parseLockfile(lockfileContents: string) {
try {
return {
object: parseSyml(lockfileContents),
type: 'success',
};
} catch (err) {
return {
object: null,
type: err,
};
}
}
// the new yarn header is handled out of band of the parsing
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
const NEW_HEADER = `${[
`# This file is generated by running "yarn install" inside your project.\n`,
`# Manual changes might be lost - proceed with caution!\n`,
].join(``)}\n`;
function stringifyLockfile(data: LockfileData, legacy: boolean) {
return legacy
? legacyStringifyLockfile(data)
: NEW_HEADER + stringifySyml(data);
}
// taken from yarn parser package
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
export class Lockfile {
static async load(path: string) {
const lockfileContents = await fs.readFile(path, 'utf8');
const legacy = LEGACY_REGEX.test(lockfileContents);
const lockfile = parseLockfile(lockfileContents);
if (lockfile.type !== 'success') {
throw new Error(`Failed yarn.lock parse with ${lockfile.type}`);
@@ -79,7 +108,7 @@ export class Lockfile {
for (const [key, value] of Object.entries(data)) {
const [, name, range] = ENTRY_PATTERN.exec(key) ?? [];
if (!name) {
if (!name && key !== '__metadata') {
throw new Error(`Failed to parse yarn.lock entry '${key}'`);
}
@@ -91,13 +120,14 @@ export class Lockfile {
queries.push({ range, version: value.version });
}
return new Lockfile(path, packages, data);
return new Lockfile(path, packages, data, legacy);
}
private constructor(
private readonly path: string,
private readonly packages: Map<string, LockfileQueryEntry[]>,
private readonly data: LockfileData,
private readonly legacy: boolean = false,
) {}
/** Get the entries for a single package in the lockfile */
@@ -120,7 +150,7 @@ export class Lockfile {
};
for (const [name, allEntries] of this.packages) {
if (filter && !filter(name)) {
if (typeof name !== 'string' || (filter && !filter(name))) {
continue;
}
@@ -267,6 +297,6 @@ export class Lockfile {
}
toString() {
return stringifyLockfile(this.data);
return stringifyLockfile(this.data, this.legacy);
}
}