From 202385be143607ada18654bd341668e53486e5db Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 29 Sep 2022 23:35:50 +0200 Subject: [PATCH] feat(core): add lock file to external nodes mapper --- packages/nx/src/config/project-graph.ts | 3 + .../__snapshots__/lock-file.spec.ts.snap | 124 ++++++++++++++++++ .../lock-file/__snapshots__/pnpm.spec.ts.snap | 26 ++-- .../nx/src/utils/lock-file/lock-file.spec.ts | 78 +++++------ packages/nx/src/utils/lock-file/lock-file.ts | 69 +++++++++- packages/nx/src/utils/lock-file/pnpm.ts | 23 ++-- packages/nx/src/utils/lock-file/utils.ts | 10 ++ 7 files changed, 263 insertions(+), 70 deletions(-) create mode 100644 packages/nx/src/utils/lock-file/__snapshots__/lock-file.spec.ts.snap diff --git a/packages/nx/src/config/project-graph.ts b/packages/nx/src/config/project-graph.ts index 22070905fd04e0..67b8669c1a24b4 100644 --- a/packages/nx/src/config/project-graph.ts +++ b/packages/nx/src/config/project-graph.ts @@ -107,6 +107,7 @@ export interface ProjectGraphProjectNode { /** * A node describing an external dependency + * `name` has as form of `npm:packageName` for primary dependencies or `npm:packageName@version` for hoisted ones */ export interface ProjectGraphExternalNode { type: 'npm'; @@ -114,6 +115,8 @@ export interface ProjectGraphExternalNode { data: { version: string; packageName: string; + dependencies?: Record; // dependencies of this version { [packageName]: [versionRange, actualVersion] } + peerDependencies?: Record; // dependencies of this version { [packageName]: [versionRange, actualVersion] } }; } diff --git a/packages/nx/src/utils/lock-file/__snapshots__/lock-file.spec.ts.snap b/packages/nx/src/utils/lock-file/__snapshots__/lock-file.spec.ts.snap new file mode 100644 index 00000000000000..663555ca3a08fa --- /dev/null +++ b/packages/nx/src/utils/lock-file/__snapshots__/lock-file.spec.ts.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lock-file mapLockFileDataToExternalNodes should map npm lock file data to external nodes 1`] = ` +Object { + "data": Object { + "dependencies": Object { + "cliui": Array [ + "^7.0.2", + "7.0.4", + ], + "escalade": Array [ + "^3.1.1", + "3.1.1", + ], + "get-caller-file": Array [ + "^2.0.5", + "2.0.5", + ], + "require-directory": Array [ + "^2.1.1", + "2.1.1", + ], + "string-width": Array [ + "^4.2.3", + "4.2.3", + ], + "y18n": Array [ + "^5.0.5", + "5.0.8", + ], + "yargs-parser": Array [ + "^21.0.0", + "21.0.1", + ], + }, + "packageName": "yargs", + "version": "17.5.1", + }, + "name": "npm:yargs", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map pnpm lock file data to external nodes 1`] = ` +Object { + "data": Object { + "dependencies": Object { + "cliui": Array [ + "7.0.4", + "7.0.4", + ], + "escalade": Array [ + "3.1.1", + "3.1.1", + ], + "get-caller-file": Array [ + "2.0.5", + "2.0.5", + ], + "require-directory": Array [ + "2.1.1", + "2.1.1", + ], + "string-width": Array [ + "4.2.3", + "4.2.3", + ], + "y18n": Array [ + "5.0.8", + "5.0.8", + ], + "yargs-parser": Array [ + "21.1.1", + "21.1.1", + ], + }, + "packageName": "yargs", + "version": "17.5.1", + }, + "name": "npm:yargs", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map yarn lock file data to external nodes 1`] = ` +Object { + "data": Object { + "dependencies": Object { + "cliui": Array [ + "^7.0.2", + "7.0.4", + ], + "escalade": Array [ + "^3.1.1", + "3.1.1", + ], + "get-caller-file": Array [ + "^2.0.5", + "2.0.5", + ], + "require-directory": Array [ + "^2.1.1", + "2.1.1", + ], + "string-width": Array [ + "^4.2.3", + "4.2.3", + ], + "y18n": Array [ + "^5.0.5", + "5.0.8", + ], + "yargs-parser": Array [ + "^21.0.0", + "21.1.1", + ], + }, + "packageName": "yargs", + "version": "17.5.1", + }, + "name": "npm:yargs", + "type": "npm", +} +`; diff --git a/packages/nx/src/utils/lock-file/__snapshots__/pnpm.spec.ts.snap b/packages/nx/src/utils/lock-file/__snapshots__/pnpm.spec.ts.snap index 88c98099ea139a..aae976bac24d16 100644 --- a/packages/nx/src/utils/lock-file/__snapshots__/pnpm.spec.ts.snap +++ b/packages/nx/src/utils/lock-file/__snapshots__/pnpm.spec.ts.snap @@ -3,6 +3,11 @@ exports[`pnpm LockFile utility lock file with inline specifiers should parse lockfile (IS) 1`] = ` Object { "@ampproject/remapping@2.2.0": Object { + "dependencies": Object { + "@jridgewell/gen-mapping": "0.1.1", + "@jridgewell/trace-mapping": "0.3.15", + }, + "dev": true, "engines": Object { "node": ">=6.0.0", }, @@ -13,7 +18,6 @@ Object { "@jridgewell/gen-mapping": "0.1.1", "@jridgewell/trace-mapping": "0.3.15", }, - "dev": true, }, "isDependency": false, "isDevDependency": false, @@ -32,15 +36,14 @@ Object { exports[`pnpm LockFile utility lock file with inline specifiers should parse lockfile (IS) 2`] = ` Object { "typescript@4.8.3": Object { + "dev": true, "engines": Object { "node": ">=4.2.0", }, + "hasBin": true, "packageMeta": Array [ Object { - "dependencyDetails": Object { - "dev": true, - "hasBin": true, - }, + "dependencyDetails": Object {}, "isDependency": false, "isDevDependency": true, "key": "/typescript/4.8.3", @@ -58,6 +61,11 @@ Object { exports[`pnpm LockFile utility standard lock file should parse lockfile correctly 1`] = ` Object { "@ampproject/remapping@2.2.0": Object { + "dependencies": Object { + "@jridgewell/gen-mapping": "0.1.1", + "@jridgewell/trace-mapping": "0.3.15", + }, + "dev": true, "engines": Object { "node": ">=6.0.0", }, @@ -68,7 +76,6 @@ Object { "@jridgewell/gen-mapping": "0.1.1", "@jridgewell/trace-mapping": "0.3.15", }, - "dev": true, }, "isDependency": false, "isDevDependency": false, @@ -87,15 +94,14 @@ Object { exports[`pnpm LockFile utility standard lock file should parse lockfile correctly 2`] = ` Object { "typescript@4.8.3": Object { + "dev": true, "engines": Object { "node": ">=4.2.0", }, + "hasBin": true, "packageMeta": Array [ Object { - "dependencyDetails": Object { - "dev": true, - "hasBin": true, - }, + "dependencyDetails": Object {}, "isDependency": false, "isDevDependency": true, "key": "/typescript/4.8.3", diff --git a/packages/nx/src/utils/lock-file/lock-file.spec.ts b/packages/nx/src/utils/lock-file/lock-file.spec.ts index 6ba1e188df63c8..b77ed4aa885c75 100644 --- a/packages/nx/src/utils/lock-file/lock-file.spec.ts +++ b/packages/nx/src/utils/lock-file/lock-file.spec.ts @@ -1,54 +1,40 @@ import { mapLockFileDataToExternalNodes } from './lock-file'; import { LockFileData } from './lock-file-type'; +import { parseNpmLockFile } from './npm'; +import { parsePnpmLockFile } from './pnpm'; +import { parseYarnLockFile } from './yarn'; +import { lockFileYargsOnly } from './__fixtures__/npm.lock'; +import { lockFileYargsOnly as pnpmLockFileYargsOnly } from './__fixtures__/pnpm.lock'; +import { lockFileDevkitAndYargs } from './__fixtures__/yarn.lock'; describe('lock-file', () => { describe('mapLockFileDataToExternalNodes', () => { - it('should map lock file data to external nodes', () => { - const lockFileData: LockFileData = { - dependencies: { - 'happy-nrwl': { - 'happy-nrwl@1.0.0': { - version: '1.0.0', - resolved: - 'https://registry.npmjs.org/happy-nrwl/-/happy-nrwl-1.0.0.tgz', - integrity: 'sha512-1', - packageMeta: ['happy-nrwl@1.0.0'], - }, - }, - 'happy-nrwl2': { - 'happy-nrwl2@^1.0.0': { - version: '1.2.0', - resolved: - 'https://registry.npmjs.org/happy-nrwl2/-/happy-nrwl2-1.2.0.tgz', - integrity: 'sha512-2', - packageMeta: ['happy-nrwl@^1.0.0'], - }, - }, - }, - }; - const externalNodes = mapLockFileDataToExternalNodes(lockFileData); - expect(externalNodes).toEqual({ - 'npm:happy-nrwl': { - type: 'npm', - name: 'npm:happy-nrwl', - data: { - version: '1.0.0', - resolved: - 'https://registry.npmjs.org/happy-nrwl/-/happy-nrwl-1.0.0.tgz', - integrity: 'sha512-1', - }, - }, - 'npm:happy-nrwl2': { - type: 'npm', - name: 'npm:happy-nrwl2', - data: { - version: '1.2.0', - resolved: - 'https://registry.npmjs.org/happy-nrwl2/-/happy-nrwl2-1.2.0.tgz', - integrity: 'sha512-2', - }, - }, - }); + it('should map yarn lock file data to external nodes', () => { + const lockFileData = parseYarnLockFile(lockFileDevkitAndYargs); + + const mappedExernalNodes = mapLockFileDataToExternalNodes(lockFileData); + + expect(mappedExernalNodes['npm:yargs']).toMatchSnapshot(); + }); + + it('should map npm lock file data to external nodes', () => { + const lockFileData = parseNpmLockFile(lockFileYargsOnly); + + const mappedExernalNodes = mapLockFileDataToExternalNodes(lockFileData); + + expect(mappedExernalNodes['npm:yargs']).toMatchSnapshot(); + }); + + it('should map pnpm lock file data to external nodes', () => { + const lockFileData = parsePnpmLockFile(pnpmLockFileYargsOnly); + + const mappedExernalNodes = mapLockFileDataToExternalNodes(lockFileData); + + console.log(JSON.stringify(lockFileData.dependencies['yargs'], null, 2)); + + console.log(JSON.stringify(mappedExernalNodes['npm:yargs'], null, 2)); + + expect(mappedExernalNodes['npm:yargs']).toMatchSnapshot(); }); }); }); diff --git a/packages/nx/src/utils/lock-file/lock-file.ts b/packages/nx/src/utils/lock-file/lock-file.ts index e05c64aacd4d6e..7b677fe7d5a5e8 100644 --- a/packages/nx/src/utils/lock-file/lock-file.ts +++ b/packages/nx/src/utils/lock-file/lock-file.ts @@ -15,11 +15,11 @@ import { prunePnpmLockFile, stringifyPnpmLockFile, } from './pnpm'; -import { LockFileData } from './lock-file-type'; +import { LockFileData, PackageVersions } from './lock-file-type'; import { workspaceRoot } from '../workspace-root'; import { join } from 'path'; -import { hashString } from './utils'; -import { ProjectGraphExternalNode } from 'nx/src/config/project-graph'; +import { findMatchingVersion, hashString } from './utils'; +import { ProjectGraphExternalNode } from '../../config/project-graph'; /** * Hashes lock file content @@ -67,10 +67,71 @@ export function parseLockFile( throw Error(`Unknown package manager: ${packageManager}`); } +/** + * Maps lock file data to {@link ProjectGraphExternalNode} hash map + * @param lockFileData + * @returns + */ export function mapLockFileDataToExternalNodes( lockFileData: LockFileData ): Record { - return {}; + const result: Record = {}; + Object.keys(lockFileData.dependencies).forEach((dep) => { + Object.keys(lockFileData.dependencies[dep]).forEach((version, index) => { + const nodeName: `npm:${string}` = !index + ? `npm:${dep}` + : `npm:${dep}@${version}`; + const packageVersion = lockFileData.dependencies[dep][version]; + + // map packages' transitive dependencies and peer dependencies to external nodes' versions + const dependencies = mapTransitiveDependencies( + lockFileData.dependencies, + packageVersion.dependencies + ); + const peerDependencies = mapTransitiveDependencies( + lockFileData.dependencies, + packageVersion.peerDependencies + ); + + // save external node + result[nodeName] = { + type: 'npm', + name: nodeName, + data: { + version: packageVersion.version, + packageName: dep, + ...(dependencies && { dependencies }), + ...(peerDependencies && { peerDependencies }), + }, + }; + }); + }); + return result; +} + +// Finds the maching version of each dependency of the package and +// maps each {package}:{versionRange} pair to {package}:[{versionRange}, {version}] +function mapTransitiveDependencies( + packages: Record, + dependencies: Record +): Record { + if (!dependencies) { + return undefined; + } + const result: Record = {}; + + Object.keys(dependencies).forEach((packageName) => { + const version = findMatchingVersion( + packages[packageName], + dependencies[packageName] + ); + result[packageName] = [ + dependencies[packageName], + version || dependencies[packageName], + ]; + }); + + return result; } /** diff --git a/packages/nx/src/utils/lock-file/pnpm.ts b/packages/nx/src/utils/lock-file/pnpm.ts index aa8a6abea5d52a..8bdda162a466de 100644 --- a/packages/nx/src/utils/lock-file/pnpm.ts +++ b/packages/nx/src/utils/lock-file/pnpm.ts @@ -73,14 +73,12 @@ function mapPackages( const mappedPackages: LockFileData['dependencies'] = {}; Object.entries(packages).forEach(([key, value]) => { - const { resolution, engines, ...dependencyDetails } = value; - // construct packageMeta object const meta = mapMetaInformation( { dependencies, devDependencies, specifiers }, inlineSpecifiers, key, - dependencyDetails + value ); // create new key @@ -95,8 +93,7 @@ function mapPackages( mappedPackages[packageName][newKey].packageMeta.push(meta); } else { mappedPackages[packageName][newKey] = { - resolution, - engines, + ...value, version, packageMeta: [meta], }; @@ -114,7 +111,7 @@ function mapMetaInformation( }: Omit, hasInlineSpefiers, key: string, - dependencyDetails: Record> + dependencyDetails: Omit ): PackageMeta { const matchingVersion = key.split('/').pop(); const packageName = key.slice(1, key.lastIndexOf('/')); @@ -148,7 +145,14 @@ function mapMetaInformation( isDependency, isDevDependency, specifier, - dependencyDetails, + dependencyDetails: { + ...(dependencyDetails.dependencies && { + dependencies: dependencyDetails.dependencies, + }), + ...(dependencyDetails.peerDependencies && { + peerDependencies: dependencyDetails.peerDependencies, + }), + }, }; } @@ -209,7 +213,7 @@ function unmapLockFile(lockFileData: LockFileData): PnpmLockFile { const packageName = packageNames[i]; const versions = Object.values(lockFileData.dependencies[packageName]); - versions.forEach(({ packageMeta, resolution, engines }) => { + versions.forEach(({ packageMeta, version: _, ...rest }) => { (packageMeta as PackageMeta[]).forEach((meta) => { const { key, specifier } = meta; @@ -231,8 +235,7 @@ function unmapLockFile(lockFileData: LockFileData): PnpmLockFile { devDependencies[packageName] = version; } packages[key] = { - resolution, - engines, + ...rest, ...meta.dependencyDetails, }; }); diff --git a/packages/nx/src/utils/lock-file/utils.ts b/packages/nx/src/utils/lock-file/utils.ts index 78c61549acdf34..30eae7e1e73f7d 100644 --- a/packages/nx/src/utils/lock-file/utils.ts +++ b/packages/nx/src/utils/lock-file/utils.ts @@ -1,4 +1,6 @@ +import { satisfies } from 'semver'; import { defaultHashing } from '../../hasher/hashing-impl'; +import { PackageVersions } from './lock-file-type'; /** * Simple sort function to ensure keys are ordered alphabetically @@ -29,3 +31,11 @@ export function sortObject( export function hashString(fileContent: string): string { return defaultHashing.hashArray([fileContent]); } + +export function findMatchingVersion( + packageVersions: PackageVersions, + version: string +) { + const versions = Object.values(packageVersions).map((v) => v.version); + return versions.find((v) => satisfies(v, version)); +}