cli: add Lockfile.diff

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-12-12 18:56:56 +01:00
parent 9083673db8
commit d2ad8ca3af
2 changed files with 288 additions and 9 deletions
@@ -78,10 +78,12 @@ describe('Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('a')).toEqual([
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockA);
});
@@ -234,11 +236,13 @@ describe('New Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('a')).toEqual([
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2.0.1', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
{ range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockANew);
});
@@ -315,4 +319,222 @@ describe('New Lockfile', () => {
mockANewLocalDedup,
);
});
describe('diff', () => {
const lockfileLegacyA = Lockfile.parse(`${HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
b "^2"
b@3:
version "3.0.1"
integrity sha512-abc1
b@2.0.x:
version "2.0.1"
integrity sha512-abc2
b@^2:
version "2.0.0"
integrity sha512-abc3
c@^1:
version "1.0.1"
integrity x
`);
const lockfileLegacyB = Lockfile.parse(`${HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz-other
dependencies:
b "^2"
b@2.0.x, b@^2:
version "2.0.0"
integrity sha512-abc3
b@4:
version "4.0.0"
integrity sha512-abc
d@^1:
version "1.0.1"
integrity x
`);
const lockfileModernA = Lockfile.parse(`${HEADER}
"a@^1":
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
checksum sha512-xyz
dependencies:
b "^2"
"b@3":
version "3.0.1"
checksum sha512-abc1
"b@2.0.x":
version "2.0.1"
checksum sha512-abc2
"b@^2":
version "2.0.0"
checksum sha512-abc3
"c@^1":
version "1.0.1"
checksum x
`);
const lockfileModernB = Lockfile.parse(`${HEADER}
"a@^1":
version "1.0.1"
resolution "a@npm:1.0.1"
checksum sha512-xyz-other
dependencies:
b "^2"
"b@2.0.x, b@^2":
version "2.0.0"
checksum sha512-abc3
"b@4":
version "4.0.0"
checksum sha512-abc
"d@^1":
version "1.0.1"
checksum x
`);
it('should diff two legacy lockfiles', async () => {
expect(lockfileLegacyA.diff(lockfileLegacyB)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
expect(lockfileLegacyB.diff(lockfileLegacyA)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
});
it('should diff two modern lockfiles', async () => {
expect(lockfileModernA.diff(lockfileModernB)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
expect(lockfileModernB.diff(lockfileModernA)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
});
it('should diff legacy and modern lockfiles', async () => {
expect(lockfileLegacyA.diff(lockfileModernB)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
expect(lockfileLegacyB.diff(lockfileModernA)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
});
it('should diff modern and legacy lockfiles', async () => {
expect(lockfileModernA.diff(lockfileLegacyB)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
expect(lockfileModernB.diff(lockfileLegacyA)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
});
});
});
+59 -2
View File
@@ -26,7 +26,8 @@ type LockfileData = {
[entry: string]: {
version: string;
resolved?: string;
integrity?: string;
integrity?: string /* old */;
checksum?: string /* new */;
dependencies?: { [name: string]: string };
};
};
@@ -34,6 +35,7 @@ type LockfileData = {
type LockfileQueryEntry = {
range: string;
version: string;
dataKey: string;
};
/** Entries that have an invalid version range, for example an npm tag */
@@ -127,7 +129,7 @@ export class Lockfile {
if (range.startsWith('npm:')) {
range = range.slice('npm:'.length);
}
queries.push({ range, version: value.version });
queries.push({ range, version: value.version, dataKey: key });
}
}
@@ -326,6 +328,61 @@ export class Lockfile {
}
}
diff(newLockfile: Lockfile) {
const diff = {
added: new Array<{ name: string; range: string }>(),
removed: new Array<{ name: string; range: string }>(),
changed: new Array<{ name: string; range: string }>(),
};
// Keeps track of packages that only exist in the old lockfile
const remainingOldNames = new Set(this.packages.keys());
for (const [name, newQueries] of newLockfile.packages) {
remainingOldNames.delete(name);
const oldQueries = this.packages.get(name);
// If the packages doesn't exist in the old lockfile, add all entries
if (!oldQueries) {
diff.added.push(...newQueries.map(q => ({ name, range: q.range })));
continue;
}
const remainingOldRanges = new Set(oldQueries.map(q => q.range));
for (const newQuery of newQueries) {
remainingOldRanges.delete(newQuery.range);
const oldQuery = oldQueries.find(q => q.range === newQuery.range);
if (!oldQuery) {
diff.added.push({ name, range: newQuery.range });
continue;
}
const newPkg = newLockfile.data[newQuery.dataKey];
const oldPkg = this.data[oldQuery.dataKey];
if (newPkg && oldPkg) {
const oldCheck = oldPkg.integrity || oldPkg.checksum;
const newCheck = newPkg.integrity || newPkg.checksum;
if (!oldCheck || !newCheck || oldCheck !== newCheck) {
diff.changed.push({ name, range: newQuery.range });
}
}
}
for (const oldRange of remainingOldRanges) {
diff.removed.push({ name, range: oldRange });
}
}
for (const name of remainingOldNames) {
const queries = this.packages.get(name) ?? [];
diff.removed.push(...queries.map(q => ({ name, range: q.range })));
}
return diff;
}
async save(path: string) {
await fs.writeFile(path, this.toString(), 'utf8');
}