feat(core): add package manager parsers and stringifiers (#11953)

This commit is contained in:
Miroslav Jonaš 2022-09-20 20:47:27 +02:00 committed by GitHub
parent 6c0a838a59
commit 074ac5ec22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 22535 additions and 501 deletions

View File

@ -118,6 +118,7 @@
"@types/tar-stream": "^2.2.2",
"@types/tmp": "^0.2.0",
"@types/yargs": "^17.0.10",
"@types/yarnpkg__lockfile": "^1.1.5",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"@typescript-eslint/type-utils": "^5.36.1",
@ -292,6 +293,9 @@
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.0",
"@yarnpkg/lockfile": "^1.1.0",
"@yarnpkg/parsers": "^3.0.0-rc.18",
"@zkochan/js-yaml": "0.0.6",
"axios": "0.21.1",
"classnames": "^2.3.1",
"cliui": "^7.0.2",

View File

@ -155,7 +155,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
@ -469,7 +480,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
@ -762,7 +784,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
@ -1120,7 +1153,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [

View File

@ -166,7 +166,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [

View File

@ -145,7 +145,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [

View File

@ -165,7 +165,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [
@ -459,7 +470,18 @@ Object {
},
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/naming-convention": Array [
"error",
Object {
"format": Array [
"camelCase",
"UPPER_CASE",
],
"leadingUnderscore": "forbid",
"selector": "variable",
"trailingUnderscore": "forbid",
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": Array [

View File

@ -33,6 +33,9 @@
"homepage": "https://nx.dev",
"dependencies": {
"@parcel/watcher": "2.0.4",
"@yarnpkg/lockfile": "^1.1.0",
"@yarnpkg/parsers": "^3.0.0-rc.18",
"@zkochan/js-yaml": "0.0.6",
"chalk": "4.1.0",
"chokidar": "^3.5.1",
"cli-cursor": "3.1.0",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`npm LockFile utility should parse lockfile correctly 1`] = `
Object {
"@ampproject/remapping@2.2.0": Object {
"dependencies": Object {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9",
},
"engines": Object {
"node": ">=6.0.0",
},
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
"packageMeta": Array [
Object {
"dev": true,
"optional": undefined,
"path": "node_modules/@ampproject/remapping",
},
],
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"version": "2.2.0",
},
}
`;
exports[`npm LockFile utility should parse lockfile correctly 2`] = `
Object {
"typescript@4.8.3": Object {
"bin": Object {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver",
},
"engines": Object {
"node": ">=4.2.0",
},
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
"packageMeta": Array [
Object {
"dev": true,
"optional": undefined,
"path": "node_modules/typescript",
},
],
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz",
"version": "4.8.3",
},
}
`;

View File

@ -0,0 +1,111 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`pnpm LockFile utility lock file with inline specifiers should parse lockfile (IS) 1`] = `
Object {
"@ampproject/remapping@2.2.0": Object {
"engines": Object {
"node": ">=6.0.0",
},
"packageMeta": Array [
Object {
"dependencyDetails": Object {
"dependencies": Object {
"@jridgewell/gen-mapping": "0.1.1",
"@jridgewell/trace-mapping": "0.3.15",
},
"dev": true,
},
"isDependency": false,
"isDevDependency": false,
"key": "/@ampproject/remapping/2.2.0",
"specifier": undefined,
},
],
"resolution": Object {
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
},
"version": "2.2.0",
},
}
`;
exports[`pnpm LockFile utility lock file with inline specifiers should parse lockfile (IS) 2`] = `
Object {
"typescript@4.8.3": Object {
"engines": Object {
"node": ">=4.2.0",
},
"packageMeta": Array [
Object {
"dependencyDetails": Object {
"dev": true,
"hasBin": true,
},
"isDependency": false,
"isDevDependency": true,
"key": "/typescript/4.8.3",
"specifier": "~4.8.2",
},
],
"resolution": Object {
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
},
"version": "4.8.3",
},
}
`;
exports[`pnpm LockFile utility standard lock file should parse lockfile correctly 1`] = `
Object {
"@ampproject/remapping@2.2.0": Object {
"engines": Object {
"node": ">=6.0.0",
},
"packageMeta": Array [
Object {
"dependencyDetails": Object {
"dependencies": Object {
"@jridgewell/gen-mapping": "0.1.1",
"@jridgewell/trace-mapping": "0.3.15",
},
"dev": true,
},
"isDependency": undefined,
"isDevDependency": false,
"key": "/@ampproject/remapping/2.2.0",
"specifier": undefined,
},
],
"resolution": Object {
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
},
"version": "2.2.0",
},
}
`;
exports[`pnpm LockFile utility standard lock file should parse lockfile correctly 2`] = `
Object {
"typescript@4.8.3": Object {
"engines": Object {
"node": ">=4.2.0",
},
"packageMeta": Array [
Object {
"dependencyDetails": Object {
"dev": true,
"hasBin": true,
},
"isDependency": undefined,
"isDevDependency": true,
"key": "/typescript/4.8.3",
"specifier": "~4.8.2",
},
],
"resolution": Object {
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
},
"version": "4.8.3",
},
}
`;

View File

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`yarn LockFile utility berry should parse lockfile correctly 1`] = `
Object {
"@ampproject/remapping@2.2.0": Object {
"checksum": "d74d170d06468913921d72430259424b7e4c826b5a7d39ff839a29d547efb97dc577caa8ba3fb5cf023624e9af9d09651afc3d4112a45e2050328abc9b3a2292",
"dependencies": Object {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9",
},
"languageName": "node",
"linkType": "hard",
"packageMeta": Array [
"@ampproject/remapping@npm:^2.1.0",
],
"resolution": "@ampproject/remapping@npm:2.2.0",
"version": "2.2.0",
},
}
`;
exports[`yarn LockFile utility berry should parse lockfile correctly 2`] = `
Object {
"typescript@4.8.3": Object {
"bin": Object {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver",
},
"checksum": "8286a5edcaf3d68e65c451aa1e7150ad1cf53ee0813c07ec35b7abdfdb10f355ecaa13c6a226a694ae7a67785fd7eeebf89f845da0b4f7e4a35561ddc459aba0",
"languageName": "node",
"linkType": "hard",
"packageMeta": Array [
"typescript@npm:~4.8.2",
],
"resolution": "typescript@npm:4.8.3",
"version": "4.8.3",
},
}
`;
exports[`yarn LockFile utility classic should parse lockfile correctly 1`] = `
Object {
"@ampproject/remapping@2.2.0": Object {
"dependencies": Object {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9",
},
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
"packageMeta": Array [
"@ampproject/remapping@^2.1.0",
],
"resolved": "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d",
"version": "2.2.0",
},
}
`;
exports[`yarn LockFile utility classic should parse lockfile correctly 2`] = `
Object {
"typescript@4.8.3": Object {
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
"packageMeta": Array [
"typescript@~4.8.2",
],
"resolved": "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88",
"version": "4.8.3",
},
}
`;

View File

@ -0,0 +1,16 @@
export interface PackageDependency {
version?: string;
packageMeta: unknown[];
dependencies?: Record<string, string>;
dependenciesMeta?: Record<string, { optional: string }>; // todo: THIS IS FOR YARN 2
peerDependencies?: Record<string, string>;
peerDependenciesMeta?: Record<string, { optional: string }>; // todo: THIS IS FOR YARN 2
[key: string]: any;
}
export type PackageVersions = Record<string, PackageDependency>;
export type LockFileData = {
dependencies: Record<string, PackageVersions>;
lockFileMetadata?: Record<string, any>;
};

View File

@ -0,0 +1,46 @@
import { readFileSync, writeFileSync } from 'fs-extra';
import { detectPackageManager, PackageManager } from '../package-manager';
import { parseYarnLockFile, stringifyYarnLockFile } from './yarn';
import { parseNpmLockFile, stringifyNpmLockFile } from './npm';
import { parsePnpmLockFile, stringifyPnpmLockFile } from './pnpm';
import { LockFileData } from './lock-file-type';
export function parseLockFile(
packageManager: PackageManager = detectPackageManager()
): LockFileData {
if (packageManager === 'yarn') {
const file = readFileSync('yarn.lock', 'utf8');
return parseYarnLockFile(file);
}
if (packageManager === 'pnpm') {
const file = readFileSync('pnpm-lock.yaml', 'utf8');
return parsePnpmLockFile(file);
}
if (packageManager === 'npm') {
const file = readFileSync('package-lock.json', 'utf8');
return parseNpmLockFile(file);
}
throw Error(`Unknown package manager: ${packageManager}`);
}
export function writeLockFile(
lockFile: LockFileData,
packageManager: PackageManager = detectPackageManager()
): void {
if (packageManager === 'yarn') {
const content = stringifyYarnLockFile(lockFile);
writeFileSync('yarn.lock', content);
return;
}
if (packageManager === 'pnpm') {
const content = stringifyPnpmLockFile(lockFile);
writeFileSync('pnpm-lock.yaml', content);
return;
}
if (packageManager === 'npm') {
const content = stringifyNpmLockFile(lockFile);
writeFileSync('package-lock.json', content);
return;
}
throw Error(`Unknown package manager: ${packageManager}`);
}

View File

@ -0,0 +1,75 @@
import { parseNpmLockFile, stringifyNpmLockFile } from './npm';
import { lockFile } from './__fixtures__/npm.lock';
describe('npm LockFile utility', () => {
const parsedLockFile = parseNpmLockFile(lockFile);
it('should parse lockfile correctly', () => {
expect(parsedLockFile.lockFileMetadata).toEqual({
metadata: {
lockfileVersion: 2,
name: 'test',
requires: true,
version: '0.0.0',
},
rootPackage: {
devDependencies: {
'@nrwl/cli': '14.7.5',
'@nrwl/workspace': '14.7.5',
nx: '14.7.5',
prettier: '^2.6.2',
typescript: '~4.8.2',
},
license: 'MIT',
name: 'test',
version: '0.0.0',
},
});
expect(Object.keys(parsedLockFile.dependencies).length).toEqual(324);
expect(
parsedLockFile.dependencies['@ampproject/remapping']
).toMatchSnapshot();
expect(parsedLockFile.dependencies['typescript']).toMatchSnapshot();
});
it('should map various versions of packages', () => {
expect(
Object.keys(parsedLockFile.dependencies['@jridgewell/gen-mapping']).length
).toEqual(2);
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.1.1'
]
).toBeDefined();
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.3.2'
]
).toBeDefined();
});
it('should map various instances of the same version', () => {
const jestResolveDependency =
parsedLockFile.dependencies['jest-resolve']['jest-resolve@28.1.3'];
expect(jestResolveDependency.packageMeta.length).toEqual(2);
expect((jestResolveDependency.packageMeta[0] as any).path).toEqual(
'node_modules/jest-runner/node_modules/jest-resolve'
);
expect((jestResolveDependency.packageMeta[1] as any).path).toEqual(
'node_modules/jest-runtime/node_modules/jest-resolve'
);
});
it('should map optional field', () => {
const tsDependency =
parsedLockFile.dependencies['typescript']['typescript@4.8.3'];
expect((tsDependency.packageMeta[0] as any).optional).toBeFalsy();
const fsEventsDependency =
parsedLockFile.dependencies['fsevents']['fsevents@2.3.2'];
expect((fsEventsDependency.packageMeta[0] as any).optional).toBeTruthy();
});
it('should match the original file on stringification', () => {
expect(stringifyNpmLockFile(parsedLockFile)).toEqual(lockFile);
});
});

View File

@ -0,0 +1,200 @@
import {
LockFileData,
PackageDependency,
PackageVersions,
} from './lock-file-type';
import { sortObject } from './utils';
type PackageMeta = {
path: string;
optional?: boolean;
dev?: boolean;
};
type Dependencies = Record<string, PackageDependency>;
type NpmDependency = {
version: string;
resolved: string;
integrity: string;
requires?: Record<string, string>;
dependencies?: Record<string, NpmDependency>;
};
export type NpmLockFile = {
name?: string;
lockfileVersion: number;
requires?: boolean;
packages: Dependencies;
dependencies?: Record<string, NpmDependency>;
};
/**
* Parses package-lock.json file to `LockFileData` object
*
* @param lockFile
* @returns
*/
export function parseNpmLockFile(lockFile: string): LockFileData {
const { packages, dependencies, ...metadata } = JSON.parse(
lockFile
) as NpmLockFile;
return {
dependencies: mapPackages(packages),
lockFileMetadata: {
metadata,
rootPackage: packages[''],
},
};
}
// Maps /node_modules/@abc/def with version 1.2.3 => @abc/def > @abc/dev@1.2.3
function mapPackages(packages: Dependencies): LockFileData['dependencies'] {
const mappedPackages: LockFileData['dependencies'] = {};
Object.entries(packages).forEach(([key, { dev, optional, ...value }]) => {
// skip root package
if (!key) {
return;
}
const packageName = key.slice(key.lastIndexOf('node_modules/') + 13);
mappedPackages[packageName] = mappedPackages[packageName] || {};
const newKey = packageName + '@' + value.version;
mappedPackages[packageName][newKey] = mappedPackages[packageName][
newKey
] || {
...value,
packageMeta: [],
};
mappedPackages[packageName][newKey].packageMeta.push({
path: key,
dev,
optional,
});
});
return mappedPackages;
}
/**
* Generates package-lock.json file from `LockFileData` object
*
* @param lockFile
* @returns
*/
export function stringifyNpmLockFile(lockFileData: LockFileData): string {
const dependencies = {};
const packages = {
'': lockFileData.lockFileMetadata.rootPackage,
};
Object.entries(lockFileData.dependencies).forEach(
([packageName, packageVersions]) => {
Object.values(packageVersions).forEach(({ packageMeta, ...value }) => {
(packageMeta as PackageMeta[]).forEach(({ path, dev, optional }) => {
const {
version,
resolved,
integrity,
license,
devOptional,
hasInstallScript,
...rest
} = value;
// we are sorting the properties to get as close as possible to the original package-lock.json
packages[path] = {
version,
resolved,
integrity,
dev,
devOptional,
hasInstallScript,
license,
optional,
...rest,
};
});
unmapDependencies(
dependencies,
packageName,
value,
packageMeta as PackageMeta[]
);
});
}
);
const lockFileJson: NpmLockFile = {
...lockFileData.lockFileMetadata.metadata,
packages: sortObject(packages),
dependencies: sortDependencies(dependencies),
};
return JSON.stringify(lockFileJson, null, 2) + '\n';
}
function unmapDependencies(
dependencies: Record<string, NpmDependency>,
packageName: string,
value: Omit<PackageDependency, 'packageMeta'>,
packageMeta: PackageMeta[]
): void {
packageMeta.forEach(({ path, dev, optional }) => {
const projectPath = path.split('node_modules/').slice(1);
let current = dependencies;
while (projectPath.length > 1) {
const parentName = projectPath.shift().replace(/\/$/, '');
current[parentName] = current[parentName] || ({} as NpmDependency);
const parent = current[parentName];
parent.dependencies = parent.dependencies || {};
current = parent.dependencies;
}
let unsortedRequires;
if (value.dependencies) {
unsortedRequires = value.dependencies;
}
if (value.optionalDependencies) {
unsortedRequires = {
...unsortedRequires,
...value.optionalDependencies,
};
}
let requires;
if (unsortedRequires) {
Object.entries(unsortedRequires)
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([key, value]) => {
requires = requires || {};
requires[key] = value;
});
}
current[packageName] = {
version: value.version,
resolved: value.resolved,
integrity: value.integrity,
...(dev !== undefined && { dev }),
...(value.devOptional !== undefined && {
devOptional: value.devOptional,
}),
...(optional !== undefined && { optional }),
...(requires && { requires }),
...current[packageName],
};
});
}
// todo(meeroslav): use sortObject here as well
function sortDependencies(
unsortedDependencies: Record<string, NpmDependency>
): Record<string, NpmDependency> {
const dependencies = {};
Object.entries(unsortedDependencies)
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([key, value]) => {
dependencies[key] = {
...value,
...(value.dependencies && {
dependencies: sortDependencies(value.dependencies),
}),
};
});
return dependencies;
}

View File

@ -0,0 +1,191 @@
import { parsePnpmLockFile, stringifyPnpmLockFile } from './pnpm';
import {
lockFile,
lockFileWithInlineSpecifiers,
} from './__fixtures__/pnpm.lock';
describe('pnpm LockFile utility', () => {
describe('standard lock file', () => {
const parsedLockFile = parsePnpmLockFile(lockFile);
it('should parse lockfile correctly', () => {
expect(parsedLockFile.lockFileMetadata).toEqual({ lockfileVersion: 5.4 });
expect(Object.keys(parsedLockFile.dependencies).length).toEqual(324);
expect(
parsedLockFile.dependencies['@ampproject/remapping']
).toMatchSnapshot();
expect(parsedLockFile.dependencies['typescript']).toMatchSnapshot();
});
it('should map various versions of packages', () => {
expect(
Object.keys(parsedLockFile.dependencies['@jridgewell/gen-mapping'])
.length
).toEqual(2);
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.1.1'
]
).toBeDefined();
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.3.2'
]
).toBeDefined();
});
it('should map various instances of the same version', () => {
const jestResolveDependency =
parsedLockFile.dependencies['jest-pnp-resolver'][
'jest-pnp-resolver@1.2.2'
];
expect(jestResolveDependency.packageMeta.length).toEqual(2);
expect((jestResolveDependency.packageMeta[0] as any).key).toEqual(
'/jest-pnp-resolver/1.2.2_jest-resolve@28.1.1'
);
expect((jestResolveDependency.packageMeta[1] as any).key).toEqual(
'/jest-pnp-resolver/1.2.2_jest-resolve@28.1.3'
);
expect(
(jestResolveDependency.packageMeta[0] as any).dependencyDetails
.dependencies
).toEqual({ 'jest-resolve': '28.1.1' });
expect(
(jestResolveDependency.packageMeta[1] as any).dependencyDetails
.dependencies
).toEqual({ 'jest-resolve': '28.1.3' });
});
it('should properly extract specifier', () => {
expect(
(
parsedLockFile.dependencies['@ampproject/remapping'][
'@ampproject/remapping@2.2.0'
].packageMeta[0] as any
).specifier
).toBeUndefined();
expect(
(
parsedLockFile.dependencies['typescript']['typescript@4.8.3']
.packageMeta[0] as any
).specifier
).toEqual('~4.8.2');
});
it('should properly extract dev dependency', () => {
expect(
(
parsedLockFile.dependencies['@ampproject/remapping'][
'@ampproject/remapping@2.2.0'
].packageMeta[0] as any
).isDevDependency
).toEqual(false);
expect(
(
parsedLockFile.dependencies['typescript']['typescript@4.8.3']
.packageMeta[0] as any
).isDevDependency
).toEqual(true);
});
it('should match the original file on stringification', () => {
expect(stringifyPnpmLockFile(parsedLockFile)).toEqual(lockFile);
});
});
describe('lock file with inline specifiers', () => {
const parsedLockFile = parsePnpmLockFile(lockFileWithInlineSpecifiers);
it('should parse lockfile (IS)', () => {
expect(parsedLockFile.lockFileMetadata).toEqual({
lockfileVersion: '5.4-inlineSpecifiers',
});
expect(Object.keys(parsedLockFile.dependencies).length).toEqual(324);
expect(
parsedLockFile.dependencies['@ampproject/remapping']
).toMatchSnapshot();
expect(parsedLockFile.dependencies['typescript']).toMatchSnapshot();
});
it('should map various versions of packages (IS)', () => {
expect(
Object.keys(parsedLockFile.dependencies['@jridgewell/gen-mapping'])
.length
).toEqual(2);
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.1.1'
]
).toBeDefined();
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.3.2'
]
).toBeDefined();
});
it('should map various instances of the same version (IS)', () => {
const jestResolveDependency =
parsedLockFile.dependencies['jest-pnp-resolver'][
'jest-pnp-resolver@1.2.2'
];
expect(jestResolveDependency.packageMeta.length).toEqual(2);
expect((jestResolveDependency.packageMeta[0] as any).key).toEqual(
'/jest-pnp-resolver/1.2.2_jest-resolve@28.1.1'
);
expect((jestResolveDependency.packageMeta[1] as any).key).toEqual(
'/jest-pnp-resolver/1.2.2_jest-resolve@28.1.3'
);
expect(
(jestResolveDependency.packageMeta[0] as any).dependencyDetails
.dependencies
).toEqual({ 'jest-resolve': '28.1.1' });
expect(
(jestResolveDependency.packageMeta[1] as any).dependencyDetails
.dependencies
).toEqual({ 'jest-resolve': '28.1.3' });
});
it('should properly extract specifier (IS)', () => {
expect(
(
parsedLockFile.dependencies['@ampproject/remapping'][
'@ampproject/remapping@2.2.0'
].packageMeta[0] as any
).specifier
).toBeUndefined();
expect(
(
parsedLockFile.dependencies['typescript']['typescript@4.8.3']
.packageMeta[0] as any
).specifier
).toEqual('~4.8.2');
});
it('should properly extract dev dependency (IS)', () => {
expect(
(
parsedLockFile.dependencies['@ampproject/remapping'][
'@ampproject/remapping@2.2.0'
].packageMeta[0] as any
).isDevDependency
).toEqual(false);
expect(
(
parsedLockFile.dependencies['typescript']['typescript@4.8.3']
.packageMeta[0] as any
).isDevDependency
).toEqual(true);
});
it('should match the original file on stringification (IS)', () => {
expect(stringifyPnpmLockFile(parsedLockFile)).toEqual(
lockFileWithInlineSpecifiers
);
});
});
});

View File

@ -0,0 +1,193 @@
import { LockFileData, PackageDependency } from './lock-file-type';
import { load, dump } from '@zkochan/js-yaml';
import { sortObject } from './utils';
type PackageMeta = {
key: string;
specifier?: string;
isDevDependency?: boolean;
isDependency?: boolean;
dependencyDetails: Record<string, Record<string, string>>;
};
type Dependencies = Record<string, Omit<PackageDependency, 'packageMeta'>>;
type InlineSpecifier = {
version: string;
specifier: string;
};
type PnpmLockFile = {
lockfileVersion: string;
specifiers?: Record<string, string>;
dependencies?: Record<
string,
string | { version: string; specifier: string }
>;
devDependencies?: Record<
string,
string | { version: string; specifier: string }
>;
packages: Dependencies;
};
const LOCKFILE_YAML_FORMAT = {
blankLines: true,
lineWidth: 1000,
noCompatMode: true,
noRefs: true,
sortKeys: false,
};
/**
* Parses pnpm-lock.yaml file to `LockFileData` object
*
* @param lockFile
* @returns
*/
export function parsePnpmLockFile(lockFile: string): LockFileData {
const { dependencies, devDependencies, packages, specifiers, ...metadata } =
load(lockFile) as PnpmLockFile;
return {
dependencies: mapPackages(
dependencies,
devDependencies,
specifiers,
packages,
metadata.lockfileVersion.toString().endsWith('inlineSpecifiers')
),
lockFileMetadata: { ...metadata },
};
}
function mapPackages(
dependencies: Record<string, string | InlineSpecifier>,
devDependencies: Record<string, string | InlineSpecifier>,
specifiers: Record<string, string>,
packages: Dependencies,
inlineSpecifiers: boolean
): LockFileData['dependencies'] {
const mappedPackages: LockFileData['dependencies'] = {};
Object.entries(packages).forEach(([key, value]) => {
const packageName = key.slice(1, key.lastIndexOf('/'));
mappedPackages[packageName] = mappedPackages[packageName] || {};
const matchingVersion = key.slice(key.lastIndexOf('/') + 1);
const version = matchingVersion.split('_')[0];
let isDependency, isDevDependency, specifier;
if (inlineSpecifiers) {
if (
dependencies &&
(dependencies[packageName] as InlineSpecifier)?.version ===
matchingVersion
) {
isDependency = true;
specifier = (dependencies[packageName] as InlineSpecifier).specifier;
} else {
isDependency = false;
}
if (
devDependencies &&
(devDependencies[packageName] as InlineSpecifier)?.version ===
matchingVersion
) {
isDevDependency = true;
specifier = (devDependencies[packageName] as InlineSpecifier).specifier;
} else {
isDevDependency = false;
}
} else {
isDependency =
dependencies && dependencies[packageName] === matchingVersion;
isDevDependency =
devDependencies && devDependencies[packageName] === matchingVersion;
if (isDependency || isDevDependency) {
specifier = specifiers[packageName];
}
}
const { resolution, engines, ...rest } = value;
const meta = {
key,
isDependency,
isDevDependency,
specifier,
dependencyDetails: rest,
};
const newKey = `${packageName}@${version}`;
mappedPackages[packageName][newKey] = mappedPackages[packageName][
newKey
] || {
resolution,
engines,
version,
packageMeta: [],
};
mappedPackages[packageName][newKey].packageMeta.push(meta);
});
return mappedPackages;
}
/**
* Generates pnpm-lock.yml file from `LockFileData` object
*
* @param lockFile
* @returns
*/
export function stringifyPnpmLockFile(lockFileData: LockFileData): string {
const pnpmLockFile = unmapPackages(lockFileData);
return dump(pnpmLockFile, LOCKFILE_YAML_FORMAT);
}
function unmapPackages(lockFileData: LockFileData): PnpmLockFile {
const devDependencies: Record<string, string | InlineSpecifier> = {};
const dependencies: Record<string, string | InlineSpecifier> = {};
const packages: Dependencies = {};
const specifiers: Record<string, string> = {};
const inlineSpecifiers = lockFileData.lockFileMetadata.lockfileVersion
.toString()
.endsWith('inlineSpecifiers');
Object.entries(lockFileData.dependencies).forEach(([packageName, versions]) =>
Object.values(versions).forEach(({ packageMeta, resolution, engines }) => {
(packageMeta as PackageMeta[]).forEach(
({
key,
specifier,
isDependency,
isDevDependency,
dependencyDetails,
}) => {
const version = key.slice(key.lastIndexOf('/') + 1);
if (isDependency) {
dependencies[packageName] = inlineSpecifiers
? { specifier, version }
: version;
}
if (isDevDependency) {
devDependencies[packageName] = inlineSpecifiers
? { specifier, version }
: version;
}
if (!inlineSpecifiers && specifier) {
specifiers[packageName] = specifier;
}
packages[key] = {
resolution,
engines,
...dependencyDetails,
};
}
);
})
);
return {
...(lockFileData.lockFileMetadata as { lockfileVersion: string }),
specifiers: sortObject(specifiers),
dependencies: sortObject(dependencies),
devDependencies: sortObject(devDependencies),
packages: sortObject(packages),
};
}

View File

@ -0,0 +1,20 @@
/**
* Simple sort function to ensure keys are ordered alphabetically
* @param obj
* @returns
*/
export function sortObject<T = string>(
obj: Record<string, T>,
valueTransformator: (value: T) => any = (value) => value
): Record<string, T> | undefined {
const keys = Object.keys(obj);
if (keys.length === 0) {
return;
}
const result: Record<string, T> = {};
keys.sort().forEach((key) => {
result[key] = valueTransformator(obj[key]);
});
return result;
}

View File

@ -0,0 +1,102 @@
import { parseYarnLockFile, stringifyYarnLockFile } from './yarn';
import { lockFile, berryLockFile } from './__fixtures__/yarn.lock';
describe('yarn LockFile utility', () => {
describe('classic', () => {
const parsedLockFile = parseYarnLockFile(lockFile);
it('should parse lockfile correctly', () => {
expect(parsedLockFile.lockFileMetadata).toBeUndefined();
expect(Object.keys(parsedLockFile.dependencies).length).toEqual(324);
expect(
parsedLockFile.dependencies['@ampproject/remapping']
).toMatchSnapshot();
expect(parsedLockFile.dependencies['typescript']).toMatchSnapshot();
});
it('should map various versions of packages', () => {
expect(
Object.keys(parsedLockFile.dependencies['@jridgewell/gen-mapping'])
.length
).toEqual(2);
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.1.1'
]
).toBeDefined();
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.3.2'
]
).toBeDefined();
});
it('should map various instances of the same version', () => {
const babelCoreDependency =
parsedLockFile.dependencies['@babel/core']['@babel/core@7.19.1'];
expect(babelCoreDependency.packageMeta.length).toEqual(2);
expect(babelCoreDependency.packageMeta).toEqual([
'@babel/core@^7.11.6',
'@babel/core@^7.12.3',
]);
});
it('should match the original file on stringification', () => {
expect(stringifyYarnLockFile(parsedLockFile)).toEqual(lockFile);
});
});
describe('berry', () => {
const parsedLockFile = parseYarnLockFile(berryLockFile);
it('should parse lockfile correctly', () => {
expect(parsedLockFile.lockFileMetadata).toEqual({
__metadata: { cacheKey: '8', version: '6' },
});
expect(Object.keys(parsedLockFile.dependencies).length).toEqual(387);
expect(
parsedLockFile.dependencies['@ampproject/remapping']
).toMatchSnapshot();
expect(parsedLockFile.dependencies['typescript']).toMatchSnapshot();
});
it('should map various versions of packages', () => {
expect(
Object.keys(parsedLockFile.dependencies['@jridgewell/gen-mapping'])
.length
).toEqual(2);
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.1.1'
]
).toBeDefined();
expect(
parsedLockFile.dependencies['@jridgewell/gen-mapping'][
'@jridgewell/gen-mapping@0.3.2'
]
).toBeDefined();
});
it('should map various instances of the same version', () => {
const babelCoreDependency =
parsedLockFile.dependencies['@babel/core']['@babel/core@7.19.1'];
expect(babelCoreDependency.packageMeta.length).toEqual(2);
expect(babelCoreDependency.packageMeta).toEqual([
'@babel/core@npm:^7.11.6',
'@babel/core@npm:^7.12.3',
]);
});
it('should match the original file on stringification', () => {
const result = stringifyYarnLockFile(parsedLockFile);
expect(result).toMatch(
/This file was generated by Nx. Do not edit this file directly/
);
// we don't care about comment message
const removeComment = (value) => value.split(/\n/).slice(2).join('\n');
expect(removeComment(result)).toEqual(removeComment(berryLockFile));
});
});
});

View File

@ -0,0 +1,80 @@
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
import { stringify } from '@yarnpkg/lockfile';
import { LockFileData, PackageDependency } from './lock-file-type';
type YarnLockFile = Record<string, Omit<PackageDependency, 'packageMeta'>>;
/**
* Parses yarn.lock syml file and maps to `LockFileData` object
*
* @param lockFile
* @returns
*/
export function parseYarnLockFile(lockFile: string): LockFileData {
const { __metadata, ...dependencies } = parseSyml(lockFile);
return {
dependencies: mapPackages(dependencies),
...(__metadata ? { lockFileMetadata: { __metadata } } : {}),
};
}
function mapPackages(packages: YarnLockFile): LockFileData['dependencies'] {
const mappedPackages: LockFileData['dependencies'] = {};
Object.entries(packages).forEach(([keyExpr, value]) => {
const keys = keyExpr.split(', ');
const packageName = keys[0].slice(0, keys[0].lastIndexOf('@'));
mappedPackages[packageName] = mappedPackages[packageName] || {};
const newKey = `${packageName}@${value.version}`;
mappedPackages[packageName][newKey] =
mappedPackages[packageName][newKey] ||
({
...value,
packageMeta: [],
} as PackageDependency);
mappedPackages[packageName][newKey].packageMeta.push(...keys);
});
return mappedPackages;
}
/**
* Generates yarn.lock file from `LockFileData` object
*
* @param lockFileData
* @returns
*/
export function stringifyYarnLockFile(lockFileData: LockFileData): string {
const isBerry = !!lockFileData.lockFileMetadata?.__metadata;
const lockFile = {
...lockFileData.lockFileMetadata,
...unmapPackages(lockFileData.dependencies, isBerry),
};
if (isBerry) {
return (
`# This file was generated by Nx. Do not edit this file directly\n# Manual changes might be lost - proceed with caution!\n\n` +
stringifySyml(lockFile)
);
} else {
return stringify(lockFile);
}
}
function unmapPackages(
mappedPackages: YarnLockFile,
isBerry: boolean
): YarnLockFile {
const packages: YarnLockFile = {};
Object.values(mappedPackages).forEach((versions) => {
Object.values(versions).forEach((value) => {
const { packageMeta, ...rest } = value;
if (isBerry) {
packages[packageMeta.join(', ')] = rest;
} else {
packageMeta.forEach((key) => {
packages[key] = rest;
});
}
});
});
return packages;
}

View File

@ -6,6 +6,11 @@
"declaration": true,
"types": ["node"]
},
"exclude": ["**/*.spec.ts", "**/*_spec.ts", "jest.config.ts"],
"exclude": [
"**/*.spec.ts",
"**/*_spec.ts",
"jest.config.ts",
"**/__fixtures__/*.*"
],
"include": ["**/*.ts"]
}

1266
yarn.lock

File diff suppressed because it is too large Load Diff