feat(core): add package manager parsers and stringifiers (#11953)
This commit is contained in:
parent
6c0a838a59
commit
074ac5ec22
@ -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",
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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",
|
||||
|
||||
8111
packages/nx/src/utils/lock-file/__fixtures__/npm.lock.ts
Normal file
8111
packages/nx/src/utils/lock-file/__fixtures__/npm.lock.ts
Normal file
File diff suppressed because it is too large
Load Diff
5520
packages/nx/src/utils/lock-file/__fixtures__/pnpm.lock.ts
Normal file
5520
packages/nx/src/utils/lock-file/__fixtures__/pnpm.lock.ts
Normal file
File diff suppressed because it is too large
Load Diff
6869
packages/nx/src/utils/lock-file/__fixtures__/yarn.lock.ts
Normal file
6869
packages/nx/src/utils/lock-file/__fixtures__/yarn.lock.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
||||
111
packages/nx/src/utils/lock-file/__snapshots__/pnpm.spec.ts.snap
Normal file
111
packages/nx/src/utils/lock-file/__snapshots__/pnpm.spec.ts.snap
Normal 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",
|
||||
},
|
||||
}
|
||||
`;
|
||||
@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
||||
16
packages/nx/src/utils/lock-file/lock-file-type.ts
Normal file
16
packages/nx/src/utils/lock-file/lock-file-type.ts
Normal 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>;
|
||||
};
|
||||
46
packages/nx/src/utils/lock-file/lock-file.ts
Normal file
46
packages/nx/src/utils/lock-file/lock-file.ts
Normal 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}`);
|
||||
}
|
||||
75
packages/nx/src/utils/lock-file/npm.spec.ts
Normal file
75
packages/nx/src/utils/lock-file/npm.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
200
packages/nx/src/utils/lock-file/npm.ts
Normal file
200
packages/nx/src/utils/lock-file/npm.ts
Normal 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;
|
||||
}
|
||||
191
packages/nx/src/utils/lock-file/pnpm.spec.ts
Normal file
191
packages/nx/src/utils/lock-file/pnpm.spec.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
193
packages/nx/src/utils/lock-file/pnpm.ts
Normal file
193
packages/nx/src/utils/lock-file/pnpm.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
20
packages/nx/src/utils/lock-file/utils.ts
Normal file
20
packages/nx/src/utils/lock-file/utils.ts
Normal 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;
|
||||
}
|
||||
102
packages/nx/src/utils/lock-file/yarn.spec.ts
Normal file
102
packages/nx/src/utils/lock-file/yarn.spec.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
80
packages/nx/src/utils/lock-file/yarn.ts
Normal file
80
packages/nx/src/utils/lock-file/yarn.ts
Normal 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;
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user