cli: add Lockfile.diff
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user