cli-node: only copy the lockfile logic that is needed, revert rest

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-04-06 11:30:14 +02:00
parent c9321bea99
commit 65a7a7620d
9 changed files with 512 additions and 376 deletions
@@ -14,9 +14,7 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { ExtendedPackage } from '../monorepo';
import { Lockfile } from './Lockfile';
const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
@@ -32,294 +30,11 @@ __metadata:
cacheKey: 8
`;
const mockA = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
b "^2"
b@2.0.x:
version "2.0.1"
b@^2:
version "2.0.0"
`;
const mockADedup = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
b "^2"
b@2.0.x, b@^2:
version "2.0.1"
`;
const mockB = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^1":
version "1.0.1"
"@s/a@^2.0.x":
version "2.0.0"
`;
const mockBDedup = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x":
version "2.0.0"
"@s/a@^1":
version "1.0.1"
`;
describe('Lockfile', () => {
afterEach(() => {
mockFs.restore();
});
it('should load and serialize mockA', async () => {
mockFs({
'/yarn.lock': mockA,
});
const lockfile = await Lockfile.load('/yarn.lock');
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', dataKey: 'b@2.0.x' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockA);
});
it('should deduplicate and save mockA', async () => {
mockFs({
'/yarn.lock': mockA,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
newVersions: [
{
name: 'b',
range: '^2',
oldVersion: '2.0.0',
newVersion: '2.0.1',
},
],
});
expect(lockfile.toString()).toBe(mockA);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockADedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA);
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup);
});
it('should deduplicate mockB', async () => {
mockFs({
'/yarn.lock': mockB,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [
{
name: '@s/a',
oldRange: '^1',
newRange: '^2.0.x',
oldVersion: '1.0.1',
newVersion: '2.0.0',
},
],
newVersions: [
{
name: '@s/a',
range: '*',
oldVersion: '1.0.1',
newVersion: '2.0.0',
},
{
name: '@s/a',
range: '1 || 2',
oldVersion: '1.0.1',
newVersion: '2.0.0',
},
],
});
expect(lockfile.toString()).toBe(mockB);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockBDedup);
});
});
const mockANew = `${MODERN_HEADER}
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, b@^2.0.1":
version: 2.0.1
b@^2:
version: 2.0.0
`;
const mockANewDedup = `${MODERN_HEADER}
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, b@^2.0.1":
version: 2.0.1
b@^2:
version: 2.0.1
`;
const mockANewLocal = `${MODERN_HEADER}
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, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 2.0.0
`;
const mockANewLocalDedup = `${MODERN_HEADER}
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, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 0.0.0-use.local
`;
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', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ 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);
});
it('should deduplicate and save mockANew', async () => {
mockFs({
'/yarn.lock': mockANew,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({ localPackages: new Map() });
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('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewDedup,
);
});
it('should deduplicate and save mockANewLocal', async () => {
mockFs({
'/yarn.lock': mockANewLocal,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({
localPackages: new Map([
[
'b',
{
packageJson: { version: '2.0.1' },
} as ExtendedPackage,
],
]),
});
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
newVersions: [
{
name: 'b',
range: '^2',
oldVersion: '2.0.0',
newVersion: '0.0.0-use.local',
},
],
});
expect(lockfile.toString()).toBe(mockANewLocal);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockANewLocalDedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocal,
);
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocalDedup,
);
});
describe('diff', () => {
const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER}
a@^1:
+188
View File
@@ -0,0 +1,188 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseSyml } from '@yarnpkg/parsers';
import fs from 'fs-extra';
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
type LockfileData = {
[entry: string]: {
version: string;
resolved?: string;
integrity?: string /* old */;
checksum?: string /* new */;
dependencies?: { [name: string]: string };
peerDependencies?: { [name: string]: string };
};
};
type LockfileQueryEntry = {
range: string;
version: string;
dataKey: string;
};
type LockfileDiffEntry = {
name: string;
range: string;
};
type LockfileDiff = {
added: LockfileDiffEntry[];
changed: LockfileDiffEntry[];
removed: LockfileDiffEntry[];
};
// these are special top level yarn keys.
// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9
const SPECIAL_OBJECT_KEYS = [
`__metadata`,
`version`,
`resolution`,
`dependencies`,
`peerDependencies`,
`dependenciesMeta`,
`peerDependenciesMeta`,
`binaries`,
];
export class Lockfile {
static async load(path: string) {
const lockfileContents = await fs.readFile(path, 'utf8');
return Lockfile.parse(lockfileContents);
}
static parse(content: string) {
let data: LockfileData;
try {
data = parseSyml(content);
} catch (err) {
throw new Error(`Failed yarn.lock parse, ${err}`);
}
const packages = new Map<string, LockfileQueryEntry[]>();
for (const [key, value] of Object.entries(data)) {
if (SPECIAL_OBJECT_KEYS.includes(key)) continue;
const [, name, ranges] = ENTRY_PATTERN.exec(key) ?? [];
if (!name) {
throw new Error(`Failed to parse yarn.lock entry '${key}'`);
}
let queries = packages.get(name);
if (!queries) {
queries = [];
packages.set(name, queries);
}
for (let range of ranges.split(/\s*,\s*/)) {
if (range.startsWith(`${name}@`)) {
range = range.slice(`${name}@`.length);
}
if (range.startsWith('npm:')) {
range = range.slice('npm:'.length);
}
queries.push({ range, version: value.version, dataKey: key });
}
}
return new Lockfile(packages, data);
}
private constructor(
private readonly packages: Map<string, LockfileQueryEntry[]>,
private readonly data: LockfileData,
) {}
createSimplifiedDependencyGraph(): Map<string, Set<string>> {
const graph = new Map<string, Set<string>>();
for (const [name, entries] of this.packages) {
const dependencies = new Set(
entries.flatMap(e => {
const data = this.data[e.dataKey];
return [
...Object.keys(data?.dependencies ?? {}),
...Object.keys(data?.peerDependencies ?? {}),
];
}),
);
graph.set(name, dependencies);
}
return graph;
}
/**
* Diff with another lockfile, returning entries that have been
* added, changed, and removed compared to the other lockfile.
*/
diff(otherLockfile: Lockfile): LockfileDiff {
const diff = {
added: new Array<{ name: string; range: string }>(),
changed: new Array<{ name: string; range: string }>(),
removed: new Array<{ name: string; range: string }>(),
};
// Keeps track of packages that only exist in this lockfile
const remainingOldNames = new Set(this.packages.keys());
for (const [name, otherQueries] of otherLockfile.packages) {
remainingOldNames.delete(name);
const thisQueries = this.packages.get(name);
// If the packages doesn't exist in this lockfile, add all entries
if (!thisQueries) {
diff.removed.push(...otherQueries.map(q => ({ name, range: q.range })));
continue;
}
const remainingOldRanges = new Set(thisQueries.map(q => q.range));
for (const otherQuery of otherQueries) {
remainingOldRanges.delete(otherQuery.range);
const thisQuery = thisQueries.find(q => q.range === otherQuery.range);
if (!thisQuery) {
diff.removed.push({ name, range: otherQuery.range });
continue;
}
const otherPkg = otherLockfile.data[otherQuery.dataKey];
const thisPkg = this.data[thisQuery.dataKey];
if (otherPkg && thisPkg) {
const thisCheck = thisPkg.integrity || thisPkg.checksum;
const otherCheck = otherPkg.integrity || otherPkg.checksum;
if (thisCheck !== otherCheck) {
diff.changed.push({ name, range: otherQuery.range });
}
}
}
for (const thisRange of remainingOldRanges) {
diff.added.push({ name, range: thisRange });
}
}
for (const name of remainingOldNames) {
const queries = this.packages.get(name) ?? [];
diff.added.push(...queries.map(q => ({ name, range: q.range })));
}
return diff;
}
}
@@ -17,7 +17,7 @@
import { resolve as resolvePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph } from './PackageGraph';
import { Lockfile } from '../versioning/Lockfile';
import { Lockfile } from './Lockfile';
import { listChangedFiles, readFileAtRef } from '../git';
jest.mock('../git');
@@ -19,7 +19,7 @@ import { getPackages, Package } from '@manypkg/get-packages';
import { paths } from '../paths';
import { PackageRole } from '../role';
import { listChangedFiles, readFileAtRef } from '../git';
import { Lockfile } from '../versioning';
import { Lockfile } from './Lockfile';
import { JsonValue } from '@backstage/types';
type PackageJSON = Package['packageJson'];
@@ -0,0 +1,322 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { ExtendedPackage } from '../monorepo';
import { Lockfile } from './Lockfile';
const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
const MODERN_HEADER = `# 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 mockA = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
b "^2"
b@2.0.x:
version "2.0.1"
b@^2:
version "2.0.0"
`;
const mockADedup = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
b "^2"
b@2.0.x, b@^2:
version "2.0.1"
`;
const mockB = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^1":
version "1.0.1"
"@s/a@^2.0.x":
version "2.0.0"
`;
const mockBDedup = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x":
version "2.0.0"
"@s/a@^1":
version "1.0.1"
`;
describe('Lockfile', () => {
afterEach(() => {
mockFs.restore();
});
it('should load and serialize mockA', async () => {
mockFs({
'/yarn.lock': mockA,
});
const lockfile = await Lockfile.load('/yarn.lock');
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', dataKey: 'b@2.0.x' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockA);
});
it('should deduplicate and save mockA', async () => {
mockFs({
'/yarn.lock': mockA,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
newVersions: [
{
name: 'b',
range: '^2',
oldVersion: '2.0.0',
newVersion: '2.0.1',
},
],
});
expect(lockfile.toString()).toBe(mockA);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockADedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA);
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup);
});
it('should deduplicate mockB', async () => {
mockFs({
'/yarn.lock': mockB,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [
{
name: '@s/a',
oldRange: '^1',
newRange: '^2.0.x',
oldVersion: '1.0.1',
newVersion: '2.0.0',
},
],
newVersions: [
{
name: '@s/a',
range: '*',
oldVersion: '1.0.1',
newVersion: '2.0.0',
},
{
name: '@s/a',
range: '1 || 2',
oldVersion: '1.0.1',
newVersion: '2.0.0',
},
],
});
expect(lockfile.toString()).toBe(mockB);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockBDedup);
});
});
const mockANew = `${MODERN_HEADER}
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, b@^2.0.1":
version: 2.0.1
b@^2:
version: 2.0.0
`;
const mockANewDedup = `${MODERN_HEADER}
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, b@^2.0.1":
version: 2.0.1
b@^2:
version: 2.0.1
`;
const mockANewLocal = `${MODERN_HEADER}
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, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 2.0.0
`;
const mockANewLocalDedup = `${MODERN_HEADER}
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, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 0.0.0-use.local
`;
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', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ 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);
});
it('should deduplicate and save mockANew', async () => {
mockFs({
'/yarn.lock': mockANew,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({ localPackages: new Map() });
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('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewDedup,
);
});
it('should deduplicate and save mockANewLocal', async () => {
mockFs({
'/yarn.lock': mockANewLocal,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({
localPackages: new Map([
[
'b',
{
packageJson: { version: '2.0.1' },
} as ExtendedPackage,
],
]),
});
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
newVersions: [
{
name: 'b',
range: '^2',
oldVersion: '2.0.0',
newVersion: '0.0.0-use.local',
},
],
});
expect(lockfile.toString()).toBe(mockANewLocal);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockANewLocalDedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocal,
);
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocalDedup,
);
});
});
@@ -39,17 +39,6 @@ type LockfileQueryEntry = {
dataKey: string;
};
type LockfileDiffEntry = {
name: string;
range: string;
};
type LockfileDiff = {
added: LockfileDiffEntry[];
changed: LockfileDiffEntry[];
removed: LockfileDiffEntry[];
};
/** Entries that have an invalid version range, for example an npm tag */
type AnalyzeResultInvalidRange = {
name: string;
@@ -340,84 +329,6 @@ export class Lockfile {
}
}
createSimplifiedDependencyGraph(): Map<string, Set<string>> {
const graph = new Map<string, Set<string>>();
for (const [name, entries] of this.packages) {
const dependencies = new Set(
entries.flatMap(e => {
const data = this.data[e.dataKey];
return [
...Object.keys(data?.dependencies ?? {}),
...Object.keys(data?.peerDependencies ?? {}),
];
}),
);
graph.set(name, dependencies);
}
return graph;
}
/**
* Diff with another lockfile, returning entries that have been
* added, changed, and removed compared to the other lockfile.
*/
diff(otherLockfile: Lockfile): LockfileDiff {
const diff = {
added: new Array<{ name: string; range: string }>(),
changed: new Array<{ name: string; range: string }>(),
removed: new Array<{ name: string; range: string }>(),
};
// Keeps track of packages that only exist in this lockfile
const remainingOldNames = new Set(this.packages.keys());
for (const [name, otherQueries] of otherLockfile.packages) {
remainingOldNames.delete(name);
const thisQueries = this.packages.get(name);
// If the packages doesn't exist in this lockfile, add all entries
if (!thisQueries) {
diff.removed.push(...otherQueries.map(q => ({ name, range: q.range })));
continue;
}
const remainingOldRanges = new Set(thisQueries.map(q => q.range));
for (const otherQuery of otherQueries) {
remainingOldRanges.delete(otherQuery.range);
const thisQuery = thisQueries.find(q => q.range === otherQuery.range);
if (!thisQuery) {
diff.removed.push({ name, range: otherQuery.range });
continue;
}
const otherPkg = otherLockfile.data[otherQuery.dataKey];
const thisPkg = this.data[thisQuery.dataKey];
if (otherPkg && thisPkg) {
const thisCheck = thisPkg.integrity || thisPkg.checksum;
const otherCheck = otherPkg.integrity || otherPkg.checksum;
if (thisCheck !== otherCheck) {
diff.changed.push({ name, range: otherQuery.range });
}
}
}
for (const thisRange of remainingOldRanges) {
diff.added.push({ name, range: thisRange });
}
}
for (const name of remainingOldNames) {
const queries = this.packages.get(name) ?? [];
diff.added.push(...queries.map(q => ({ name, range: q.range })));
}
return diff;
}
async save(path: string) {
await fs.writeFile(path, this.toString(), 'utf8');
}