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
@@ -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,
);
});
});
+341
View File
@@ -0,0 +1,341 @@
/*
* 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 semver from 'semver';
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
import { ExtendedPackage } from '../monorepo';
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;
};
/** Entries that have an invalid version range, for example an npm tag */
type AnalyzeResultInvalidRange = {
name: string;
range: string;
};
/** Entries that can be deduplicated by bumping to an existing higher version */
type AnalyzeResultNewVersion = {
name: string;
range: string;
oldVersion: string;
newVersion: string;
};
/** Entries that would need a dependency update in package.json to be deduplicated */
type AnalyzeResultNewRange = {
name: string;
oldRange: string;
newRange: string;
oldVersion: string;
newVersion: string;
};
type AnalyzeResult = {
invalidRanges: AnalyzeResultInvalidRange[];
newVersions: AnalyzeResultNewVersion[];
newRanges: AnalyzeResultNewRange[];
};
// 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`;
// 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;
// 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) {
const legacy = LEGACY_REGEX.test(content);
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, legacy);
}
private constructor(
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 */
get(name: string): LockfileQueryEntry[] | undefined {
return this.packages.get(name);
}
/** Returns the name of all packages available in the lockfile */
keys(): IterableIterator<string> {
return this.packages.keys();
}
/** Analyzes the lockfile to identify possible actions and warnings for the entries */
analyze(options: {
filter?: (name: string) => boolean;
localPackages: Map<string, ExtendedPackage>;
}): AnalyzeResult {
const { filter, localPackages } = options;
const result: AnalyzeResult = {
invalidRanges: [],
newVersions: [],
newRanges: [],
};
for (const [name, allEntries] of this.packages) {
if (filter && !filter(name)) {
continue;
}
// Get rid of and signal any invalid ranges upfront
const invalid = allEntries.filter(
e => !semver.validRange(e.range) && !e.range.startsWith('workspace:'),
);
result.invalidRanges.push(
...invalid.map(({ range }) => ({ name, range })),
);
// Grab all valid entries, if there aren't at least 2 different valid ones we're done
const entries = allEntries.filter(e => semver.validRange(e.range));
if (entries.length < 2) {
continue;
}
// Find all versions currently in use
const versions = Array.from(new Set(entries.map(e => e.version)))
.map(v => {
// Translate workspace:^ references to the actual version
if (v === '0.0.0-use.local') {
const local = localPackages.get(name);
if (!local) {
throw new Error(`No local package found for ${name}`);
}
if (!local.packageJson.version) {
throw new Error(`No version found for local package ${name}`);
}
return {
entryVersion: v,
actualVersion: local.packageJson.version,
};
}
return { entryVersion: v, actualVersion: v };
})
.sort((v1, v2) => semver.rcompare(v1.actualVersion, v2.actualVersion));
// If we're not using at least 2 different versions we're done
if (versions.length < 2) {
continue;
}
// TODO(Rugvip): Support bumping into workspace ranges too
const acceptedVersions = new Set<string>();
for (const { version, range } of entries) {
// Finds the highest matching version from the the known versions
// TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one
const acceptedVersion = versions.find(v =>
semver.satisfies(v.actualVersion, range),
);
if (!acceptedVersion) {
throw new Error(
`No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`,
);
}
if (acceptedVersion.entryVersion !== version) {
result.newVersions.push({
name,
range,
newVersion: acceptedVersion.entryVersion,
oldVersion: version,
});
}
acceptedVersions.add(acceptedVersion.actualVersion);
}
// If all ranges were able to accept the same version, we're done
if (acceptedVersions.size === 1) {
continue;
}
// Find the max version that we may want bump older packages to
const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0];
// Find all existing ranges that satisfy the new max version, and pick the one that
// results in the highest minimum allowed version, usually being the more specific one
const maxEntry = entries
.filter(e => semver.satisfies(maxVersion, e.range))
.map(e => ({ e, min: semver.minVersion(e.range) }))
.filter(p => p.min)
.sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e;
if (!maxEntry) {
throw new Error(
`No entry found that satisfies max version '${maxVersion}'`,
);
}
// Find all entries that don't satisfy the max version
for (const { version, range } of entries) {
if (semver.satisfies(maxVersion, range)) {
continue;
}
result.newRanges.push({
name,
oldRange: range,
newRange: maxEntry.range,
oldVersion: version,
newVersion: maxVersion,
});
}
}
return result;
}
remove(name: string, range: string): boolean {
const query = `${name}@${range}`;
const existed = Boolean(this.data[query]);
delete this.data[query];
const newEntries = this.packages.get(name)?.filter(e => e.range !== range);
if (newEntries) {
this.packages.set(name, newEntries);
}
return existed;
}
/** Modifies the lockfile by bumping packages to the suggested versions */
replaceVersions(results: AnalyzeResultNewVersion[]) {
for (const { name, range, oldVersion, newVersion } of results) {
const query = `${name}@${range}`;
// Update the backing data
const entryData = this.data[query];
if (!entryData) {
throw new Error(`No entry data for ${query}`);
}
if (entryData.version !== oldVersion) {
throw new Error(
`Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
);
}
// Modifying the data in the entry is not enough, we need to reference an existing version object
const matchingEntry = Object.entries(this.data).find(
([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion,
);
if (!matchingEntry) {
throw new Error(
`No matching entry found for ${name} at version ${newVersion}`,
);
}
this.data[query] = matchingEntry[1];
// Update our internal data structure
const entry = this.packages.get(name)?.find(e => e.range === range);
if (!entry) {
throw new Error(`No entry data for ${query}`);
}
if (entry.version !== oldVersion) {
throw new Error(
`Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
);
}
entry.version = newVersion;
}
}
async save(path: string) {
await fs.writeFile(path, this.toString(), 'utf8');
}
toString() {
return this.legacy
? legacyStringifyLockfile(this.data)
: NEW_HEADER + stringifySyml(this.data);
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { Lockfile } from './Lockfile';
export { fetchPackageInfo, mapDependencies } from './packages';
export type { YarnInfoInspectData } from './packages';
@@ -0,0 +1,155 @@
/*
* 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 mockFs from 'mock-fs';
import path from 'path';
import * as runObj from '../run';
import * as yarn from '../yarn';
import { fetchPackageInfo, mapDependencies } from './packages';
import { NotFoundError } from '../errors';
jest.mock('../run', () => {
return {
run: jest.fn(),
execFile: jest.fn(),
};
});
jest.mock('../yarn', () => {
return {
detectYarnVersion: jest.fn(),
};
});
describe('fetchPackageInfo', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should forward info for yarn classic', async () => {
jest.spyOn(runObj, 'execFile').mockResolvedValue({
stdout: `{"type":"inspect","data":{"the":"data"}}`,
stderr: '',
});
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic');
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
the: 'data',
});
expect(runObj.execFile).toHaveBeenCalledWith(
'yarn',
['info', '--json', 'my-package'],
{ shell: true },
);
});
it('should forward info for yarn berry', async () => {
jest
.spyOn(runObj, 'execFile')
.mockResolvedValue({ stdout: `{"the":"data"}`, stderr: '' });
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
the: 'data',
});
expect(runObj.execFile).toHaveBeenCalledWith(
'yarn',
['npm', 'info', '--json', 'my-package'],
{ shell: true },
);
});
it('should throw if no info with yarn classic', async () => {
jest
.spyOn(runObj, 'execFile')
.mockResolvedValue({ stdout: '', stderr: '' });
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic');
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
new NotFoundError(`No package information found for package my-package`),
);
});
it('should throw if no info with yarn berry', async () => {
jest
.spyOn(runObj, 'execFile')
.mockRejectedValue({ stdout: 'bla bla bla Response Code: 404 bla bla' });
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
new NotFoundError(`No package information found for package my-package`),
);
});
});
describe('mapDependencies', () => {
afterEach(() => {
mockFs.restore();
jest.resetAllMocks();
});
it('should read dependencies', async () => {
mockFs({
'/root/package.json': JSON.stringify({
workspaces: {
packages: ['pkgs/*'],
},
}),
'/root/pkgs/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/core': '1 || 2',
},
}),
'/root/pkgs/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/core': '3',
'@backstage/cli': '^0',
},
}),
});
const dependencyMap = await mapDependencies('/root', '@backstage/*');
expect(Array.from(dependencyMap)).toEqual([
[
'@backstage/core',
[
{
name: 'a',
range: '1 || 2',
location: path.resolve('/root/pkgs/a'),
},
{
name: 'b',
range: '3',
location: path.resolve('/root/pkgs/b'),
},
],
],
[
'@backstage/cli',
[
{
name: 'b',
range: '^0',
location: path.resolve('/root/pkgs/b'),
},
],
],
]);
});
});
+125
View File
@@ -0,0 +1,125 @@
/*
* 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 minimatch from 'minimatch';
import { getPackages } from '@manypkg/get-packages';
import { NotFoundError } from '../errors';
import { detectYarnVersion } from '../yarn';
import { execFile } from '../run';
const DEP_TYPES = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
] as const;
// Package data as returned by `yarn info`
export type YarnInfoInspectData = {
name: string;
'dist-tags': Record<string, string>;
versions: string[];
time: { [version: string]: string };
};
// Possible `yarn info` output
type YarnInfo = {
type: 'inspect';
data: YarnInfoInspectData | { type: string; data: unknown };
};
type PkgVersionInfo = {
range: string;
name: string;
location: string;
};
export async function fetchPackageInfo(
name: string,
): Promise<YarnInfoInspectData> {
const yarnVersion = await detectYarnVersion();
const cmd = yarnVersion === 'classic' ? ['info'] : ['npm', 'info'];
try {
const { stdout: output } = await execFile(
'yarn',
[...cmd, '--json', name],
{ shell: true },
);
if (!output) {
throw new NotFoundError(
`No package information found for package ${name}`,
);
}
if (yarnVersion === 'berry') {
return JSON.parse(output) as YarnInfoInspectData;
}
const info = JSON.parse(output) as YarnInfo;
if (info.type !== 'inspect') {
throw new Error(`Received unknown yarn info for ${name}, ${output}`);
}
return info.data as YarnInfoInspectData;
} catch (error) {
if (yarnVersion === 'classic') {
throw error;
}
if (error?.stdout.includes('Response Code: 404')) {
throw new NotFoundError(
`No package information found for package ${name}`,
);
}
throw error;
}
}
/** Map all dependencies in the repo as dependency => dependents */
export async function mapDependencies(
targetDir: string,
pattern: string,
): Promise<Map<string, PkgVersionInfo[]>> {
const { packages, root } = await getPackages(targetDir);
// Include root package.json too
packages.push(root);
const dependencyMap = new Map<string, PkgVersionInfo[]>();
for (const pkg of packages) {
const deps = DEP_TYPES.flatMap(
t => Object.entries(pkg.packageJson[t] ?? {}) as [string, string][],
);
for (const [name, range] of deps) {
if (minimatch(name, pattern)) {
dependencyMap.set(
name,
(dependencyMap.get(name) ?? []).concat({
range,
name: pkg.packageJson.name,
location: pkg.dir,
}),
);
}
}
}
return dependencyMap;
}