From 5e293eba4cf24d825dfaee60ee4ddfe6f9075956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Jona=C5=A1?= Date: Thu, 6 Oct 2022 02:58:28 +0200 Subject: [PATCH] feat(core): map lock file data to external dependencies (#12185) --- e2e/js/src/js.test.ts | 21 +- e2e/node/src/node.test.ts | 24 +- packages/js/src/utils/versions.ts | 2 +- .../nx/src/command-line/print-affected.ts | 10 +- packages/nx/src/config/project-graph.ts | 8 + packages/nx/src/hasher/hasher.spec.ts | 39 +- packages/nx/src/hasher/hasher.ts | 66 +- .../affected/affected-project-graph.spec.ts | 40 + .../affected/locators/workspace-projects.ts | 1 - .../project-graph/build-project-graph.spec.ts | 53 ++ .../src/project-graph/build-project-graph.ts | 47 +- .../src/project-graph/nx-deps-cache.spec.ts | 8 +- .../nx/src/project-graph/nx-deps-cache.ts | 5 +- .../nx/src/project-graph/project-graph.ts | 2 +- .../__snapshots__/lock-file.spec.ts.snap | 685 ++++++++++++++++++ .../lock-file/__snapshots__/npm.spec.ts.snap | 2 + .../lock-file/__snapshots__/pnpm.spec.ts.snap | 30 +- .../lock-file/__snapshots__/yarn.spec.ts.snap | 4 + .../nx/src/utils/lock-file/lock-file-type.ts | 1 + .../nx/src/utils/lock-file/lock-file.spec.ts | 80 ++ packages/nx/src/utils/lock-file/lock-file.ts | 102 ++- packages/nx/src/utils/lock-file/npm.spec.ts | 11 + packages/nx/src/utils/lock-file/npm.ts | 4 +- packages/nx/src/utils/lock-file/pnpm.spec.ts | 10 + packages/nx/src/utils/lock-file/pnpm.ts | 42 +- packages/nx/src/utils/lock-file/utils.ts | 169 ++++- packages/nx/src/utils/lock-file/yarn.spec.ts | 12 + packages/nx/src/utils/lock-file/yarn.ts | 20 +- packages/webpack/src/utils/versions.ts | 2 +- 29 files changed, 1396 insertions(+), 104 deletions(-) create mode 100644 packages/nx/src/utils/lock-file/__snapshots__/lock-file.spec.ts.snap create mode 100644 packages/nx/src/utils/lock-file/lock-file.spec.ts diff --git a/e2e/js/src/js.test.ts b/e2e/js/src/js.test.ts index a0bc1eaf4cc8a..ee66c8861ad75 100644 --- a/e2e/js/src/js.test.ts +++ b/e2e/js/src/js.test.ts @@ -1,3 +1,4 @@ +import { satisfies } from 'semver'; import { checkFilesDoNotExist, checkFilesExist, @@ -150,10 +151,12 @@ describe('js e2e', () => { const rootPackageJson = readJson(`package.json`); - expect(readJson(`dist/libs/${lib}/package.json`)).toHaveProperty( - 'peerDependencies.tslib', - rootPackageJson.dependencies.tslib - ); + expect( + satisfies( + readJson(`dist/libs/${lib}/package.json`).peerDependencies.tslib, + rootPackageJson.dependencies.tslib + ) + ).toBeTruthy(); updateJson(`libs/${lib}/tsconfig.json`, (json) => { json.compilerOptions = { ...json.compilerOptions, importHelpers: false }; @@ -232,12 +235,12 @@ describe('js e2e', () => { runCLI(`build ${lib}`); - const rootPackageJson = readJson(`package.json`); + const swcHelpersFromRoot = + readJson(`package.json`).dependencies['@swc/helpers']; + const swcHelpersFromDist = readJson(`dist/libs/${lib}/package.json`) + .peerDependencies['@swc/helpers']; - expect(readJson(`dist/libs/${lib}/package.json`)).toHaveProperty( - 'peerDependencies.@swc/helpers', - rootPackageJson.dependencies['@swc/helpers'] - ); + expect(satisfies(swcHelpersFromDist, swcHelpersFromRoot)).toBeTruthy(); updateJson(`libs/${lib}/.lib.swcrc`, (json) => { json.jsc.externalHelpers = false; diff --git a/e2e/node/src/node.test.ts b/e2e/node/src/node.test.ts index 710d8f5f6d990..f65d0d3085804 100644 --- a/e2e/node/src/node.test.ts +++ b/e2e/node/src/node.test.ts @@ -10,7 +10,6 @@ import { packageInstall, promisifiedTreeKill, readFile, - removeFile, runCLI, runCLIAsync, runCommandUntil, @@ -21,6 +20,7 @@ import { } from '@nrwl/e2e/utils'; import { exec, execSync } from 'child_process'; import * as http from 'http'; +import { satisfies } from 'semver'; function getData(port): Promise { return new Promise((resolve) => { @@ -276,19 +276,25 @@ describe('Build Node apps', () => { ); expect(packageJson).toEqual( expect.objectContaining({ - dependencies: { - '@nestjs/common': '^9.0.0', - '@nestjs/core': '^9.0.0', - '@nestjs/platform-express': '^9.0.0', - 'reflect-metadata': '^0.1.13', - rxjs: '^7.0.0', - tslib: '^2.3.0', - }, main: 'main.js', name: expect.any(String), version: '0.0.1', }) ); + expect( + satisfies(packageJson.dependencies['@nestjs/common'], '^9.0.0') + ).toBeTruthy(); + expect( + satisfies(packageJson.dependencies['@nestjs/core'], '^9.0.0') + ).toBeTruthy(); + expect( + satisfies(packageJson.dependencies['@nestjs/platform-express'], '^9.0.0') + ).toBeTruthy(); + expect( + satisfies(packageJson.dependencies['reflect-metadata'], '^0.1.13') + ).toBeTruthy(); + expect(satisfies(packageJson.dependencies['rxjs'], '^7.0.0')).toBeTruthy(); + expect(satisfies(packageJson.dependencies['tslib'], '^2.3.0')).toBeTruthy(); const nodeapp = uniq('nodeapp'); runCLI(`generate @nrwl/node:app ${nodeapp}`); diff --git a/packages/js/src/utils/versions.ts b/packages/js/src/utils/versions.ts index 40cde46b9c348..3c9133879634d 100644 --- a/packages/js/src/utils/versions.ts +++ b/packages/js/src/utils/versions.ts @@ -4,5 +4,5 @@ export const nxVersion = require('../../package.json').version; export const esbuildVersion = '^0.15.7'; export const swcCliVersion = '~0.1.55'; -export const swcHelpersVersion = '~0.3.3'; +export const swcHelpersVersion = '~0.4.11'; export const typesNodeVersion = '18.7.1'; diff --git a/packages/nx/src/command-line/print-affected.ts b/packages/nx/src/command-line/print-affected.ts index 31e7d64adf57e..7c411b50f8019 100644 --- a/packages/nx/src/command-line/print-affected.ts +++ b/packages/nx/src/command-line/print-affected.ts @@ -86,7 +86,15 @@ async function createTasks( function serializeProjectGraph(projectGraph: ProjectGraph) { const nodes = Object.values(projectGraph.nodes).map((n) => n.name); - return { nodes, dependencies: projectGraph.dependencies }; + const dependencies = {}; + // we don't need external dependencies' dependencies for print-affected + // having them included makes the output unreadable + Object.keys(projectGraph.dependencies).forEach((key) => { + if (!key.startsWith('npm:')) { + dependencies[key] = projectGraph.dependencies[key]; + } + }); + return { nodes, dependencies }; } export function selectPrintAffected(wholeJson: any, wholeSelect: string) { diff --git a/packages/nx/src/config/project-graph.ts b/packages/nx/src/config/project-graph.ts index 22070905fd04e..c25bf950b4410 100644 --- a/packages/nx/src/config/project-graph.ts +++ b/packages/nx/src/config/project-graph.ts @@ -107,6 +107,13 @@ export interface ProjectGraphProjectNode { /** * A node describing an external dependency + * `name` has as form of: + * - `npm:packageName` for root dependencies or + * - `npm:packageName@version` for nested transitive dependencies + * + * This is vital for our node discovery to always point to root dependencies, + * while allowing tracking of the full tree of different nested versions + * */ export interface ProjectGraphExternalNode { type: 'npm'; @@ -114,6 +121,7 @@ export interface ProjectGraphExternalNode { data: { version: string; packageName: string; + hash?: string; }; } diff --git a/packages/nx/src/hasher/hasher.spec.ts b/packages/nx/src/hasher/hasher.spec.ts index d5ebb6e2e15d6..38454a34b5a0b 100644 --- a/packages/nx/src/hasher/hasher.spec.ts +++ b/packages/nx/src/hasher/hasher.spec.ts @@ -79,6 +79,7 @@ describe('Hasher', () => { root: 'libs/parent', targets: { build: { + executor: 'unknown', inputs: [ 'default', '^default', @@ -95,6 +96,7 @@ describe('Hasher', () => { dependencies: { parent: [], }, + externalNodes: {}, allWorkspaceFiles, }, {} as any, @@ -110,7 +112,6 @@ describe('Hasher', () => { overrides: { prop: 'prop-value' }, }); - expect(hash.value).toContain('yarn.lock.hash'); //implicits expect(hash.value).toContain('file.hash'); //project files expect(hash.value).toContain('prop-value'); //overrides expect(hash.value).toContain('parent'); //project @@ -122,10 +123,8 @@ describe('Hasher', () => { expect(hash.details.command).toEqual('parent|build||{"prop":"prop-value"}'); expect(hash.details.nodes).toEqual({ 'parent:{projectRoot}/**/*': - '/file|file.hash|{"root":"libs/parent","targets":{"build":{"inputs":["default","^default",{"runtime":"echo runtime123"},{"env":"TESTENV"},{"env":"NONEXISTENTENV"}]}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', - '{workspaceRoot}/yarn.lock': 'yarn.lock.hash', - '{workspaceRoot}/package-lock.json': 'package-lock.json.hash', - '{workspaceRoot}/pnpm-lock.yaml': 'pnpm-lock.yaml.hash', + '/file|file.hash|{"root":"libs/parent","targets":{"build":{"executor":"unknown","inputs":["default","^default",{"runtime":"echo runtime123"},{"env":"TESTENV"},{"env":"NONEXISTENTENV"}]}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', + parent: 'unknown', '{workspaceRoot}/nx.json': 'nx.json.hash', '{workspaceRoot}/.gitignore': '', '{workspaceRoot}/.nxignore': '', @@ -145,7 +144,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parent', - targets: { build: {} }, + targets: { build: { executor: 'unknown' } }, files: [ { file: '/filea.ts', hash: 'a.hash' }, { file: '/filea.spec.ts', hash: 'a.spec.hash' }, @@ -205,6 +204,7 @@ describe('Hasher', () => { targets: { build: { inputs: ['prod', '^prod'], + executor: 'unknown', }, }, files: [ @@ -221,7 +221,7 @@ describe('Hasher', () => { namedInputs: { prod: ['default'], }, - targets: { build: {} }, + targets: { build: { executor: 'unknown' } }, files: [ { file: 'libs/child/fileb.ts', hash: 'b.hash' }, { file: 'libs/child/fileb.spec.ts', hash: 'b.spec.hash' }, @@ -273,10 +273,12 @@ describe('Hasher', () => { targets: { build: { inputs: ['prod'], + executor: 'unknown', }, test: { inputs: ['default'], dependsOn: ['build'], + executor: 'unknown', }, }, files: [ @@ -339,6 +341,7 @@ describe('Hasher', () => { targets: { test: { inputs: ['default', '^prod'], + executor: 'unknown', }, }, files: [ @@ -362,6 +365,7 @@ describe('Hasher', () => { targets: { test: { inputs: ['default'], + executor: 'unknown', }, }, files: [ @@ -440,7 +444,7 @@ describe('Hasher', () => { data: { root: 'libs/parent', targets: { - build: {}, + build: { executor: '@nrwl/workspace:run-commands' }, }, files: [ { file: 'libs/parent/filea.ts', hash: 'a.hash' }, @@ -453,7 +457,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/child', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [ { file: 'libs/child/fileb.ts', hash: 'b.hash' }, { file: 'libs/child/fileb.spec.ts', hash: 'b.spec.hash' }, @@ -507,7 +511,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parent', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [{ file: '/file', hash: 'file.hash' }], }, }, @@ -531,7 +535,6 @@ describe('Hasher', () => { overrides: { prop: 'prop-value' }, }); - expect(hash.value).toContain('yarn.lock.hash'); //implicits expect(hash.value).toContain('file.hash'); //project files expect(hash.value).toContain('prop-value'); //overrides expect(hash.value).toContain('parent'); //project @@ -557,7 +560,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parent', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [{ file: '/filea.ts', hash: 'a.hash' }], }, }, @@ -566,7 +569,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/child', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [{ file: '/fileb.ts', hash: 'b.hash' }], }, }, @@ -588,7 +591,6 @@ describe('Hasher', () => { overrides: { prop: 'prop-value' }, }); - expect(tasksHash.value).toContain('yarn.lock.hash'); //implicits expect(tasksHash.value).toContain('a.hash'); //project files expect(tasksHash.value).toContain('b.hash'); //project files expect(tasksHash.value).toContain('prop-value'); //overrides @@ -612,7 +614,6 @@ describe('Hasher', () => { overrides: { prop: 'prop-value' }, }); - expect(hashb.value).toContain('yarn.lock.hash'); //implicits expect(hashb.value).toContain('a.hash'); //project files expect(hashb.value).toContain('b.hash'); //project files expect(hashb.value).toContain('prop-value'); //overrides @@ -640,7 +641,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parent', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [{ file: '/file', hash: 'some-hash' }], }, }, @@ -682,7 +683,7 @@ describe('Hasher', () => { type: 'lib', data: { root: 'libs/parents', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [], }, }, @@ -718,7 +719,7 @@ describe('Hasher', () => { type: 'app', data: { root: 'apps/app', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [{ file: '/filea.ts', hash: 'a.hash' }], }, }, @@ -767,7 +768,7 @@ describe('Hasher', () => { type: 'app', data: { root: 'apps/app', - targets: { build: {} }, + targets: { build: { executor: '@nrwl/workspace:run-commands' } }, files: [{ file: '/filea.ts', hash: 'a.hash' }], }, }, diff --git a/packages/nx/src/hasher/hasher.ts b/packages/nx/src/hasher/hasher.ts index f8bbeb4b8a2a2..c5c69b2000dee 100644 --- a/packages/nx/src/hasher/hasher.ts +++ b/packages/nx/src/hasher/hasher.ts @@ -84,10 +84,6 @@ export class Hasher { const legacyFilesetInputs = [ ...Object.keys(this.nxJson.implicitDependencies ?? {}), 'nx.json', - //TODO: vsavkin move the special cases into explicit ts support - 'package-lock.json', - 'yarn.lock', - 'pnpm-lock.yaml', // ignore files will change the set of inputs to the hasher '.gitignore', @@ -239,12 +235,21 @@ class TaskHasher { namedInputs ); - return this.hashSelfAndDepsInputs( + const selfAndInputs = await this.hashSelfAndDepsInputs( task.target.project, selfInputs, depsInputs, visited ); + + const target = this.hashTarget(task.target.project, task.target.target); + if (target) { + return { + value: this.hashing.hashArray([selfAndInputs.value, target.value]), + details: { ...selfAndInputs.details, ...target.details }, + }; + } + return selfAndInputs; }); } @@ -337,20 +342,14 @@ class TaskHasher { const n = this.projectGraph.externalNodes[projectName]; const version = n?.data?.version; let hash: string; - if (version) { - hash = this.hashing.hashArray([version]); + if (n?.data?.hash) { + // we already know the hash of this dependency + hash = n.data.hash; } else { // unknown dependency - // this may occur if a file has a dependency to a npm package - // which is not directly registestered in package.json - // but only indirectly through dependencies of registered - // npm packages - // when it is at a later stage registered in package.json - // the cache project graph will not know this module but - // the new project graph will know it - // The actual checksum added here is of no importance as - // the version is unknown and may only change when some - // other change occurs in package.json and/or package-lock.json + // this may occur dependency is not an npm package + // but rather symlinked in node_modules or it's pointing to a remote git repo + // in this case we have no information about the versioning of the given package hash = `__${projectName}__`; } return { @@ -361,6 +360,39 @@ class TaskHasher { }; } + private hashTarget(projectName: string, targetName: string): PartialHash { + const projectNode = this.projectGraph.nodes[projectName]; + const target = projectNode.data.targets[targetName]; + + if (!target) { + return; + } + + // we can only vouch for @nrwl packages's executors + // if it's "run commands" we skip traversing since we have no info what this command depends on + // for everything else we take the hash of the @nrwl package dependency tree + if ( + target.executor.startsWith(`@nrwl/`) && + target.executor !== `@nrwl/workspace:run-commands` + ) { + const executorPackage = target.executor.split(':')[0]; + const executorNode = `npm:${executorPackage}`; + if (this.projectGraph.externalNodes?.[executorNode]) { + return this.hashExternalDependency(executorNode); + } + } + + const hash = this.hashing.hashArray([ + JSON.stringify(this.projectGraph.externalNodes), + ]); + return { + value: hash, + details: { + [projectNode.name]: target.executor, + }, + }; + } + private async hashSelfInputs( projectName: string, inputs: ExpandedSelfInput[] diff --git a/packages/nx/src/project-graph/affected/affected-project-graph.spec.ts b/packages/nx/src/project-graph/affected/affected-project-graph.spec.ts index 79c032e2924fb..ff39895df0c0b 100644 --- a/packages/nx/src/project-graph/affected/affected-project-graph.spec.ts +++ b/packages/nx/src/project-graph/affected/affected-project-graph.spec.ts @@ -15,6 +15,7 @@ jest.mock('nx/src/utils/workspace-root', () => ({ describe('project graph', () => { let packageJson: any; + let packageLockJson: any; let workspaceJson: WorkspaceJsonConfiguration; let tsConfigJson: any; let nxJson: NxJsonConfiguration; @@ -24,6 +25,7 @@ describe('project graph', () => { process.env.NX_CACHE_PROJECT_GRAPH = 'false'; packageJson = { name: '@nrwl/workspace-src', + version: '0.0.0', scripts: { deploy: 'echo deploy', }, @@ -34,6 +36,43 @@ describe('project graph', () => { '@nrwl/workspace': '8.0.0', }, }; + packageLockJson = { + name: '@nrwl/workspace-src', + version: '0.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': packageJson, + 'node_modules/@nrwl/workspace': { + version: '15.0.0', + resolved: + 'https://registry.npmjs.org/@nrwl/workspace/-/@nrwl/workspace-15.0.0.tgz', + integrity: 'sha512-12345678==', + dev: true, + }, + 'node_modules/happy-nrwl': { + version: '4.0.0', + resolved: + 'https://registry.npmjs.org/happy-nrwl/-/happy-nrwl-1.0.0.tgz', + integrity: 'sha512-12345678==', + }, + }, + dependencies: { + '@nrwl/workspace': { + version: '15.0.0', + resolved: + 'https://registry.npmjs.org/@nrwl/workspace/-/@nrwl/workspace-15.0.0.tgz', + integrity: 'sha512-12345678==', + dev: true, + }, + 'happy-nrwl': { + version: '1.0.0', + resolved: + 'https://registry.npmjs.org/happy-nrwl/-/happy-nrwl-1.0.0.tgz', + integrity: 'sha512-12345678==', + }, + }, + }; workspaceJson = { version: 2, projects: { @@ -110,6 +149,7 @@ describe('project graph', () => { import * as happyNrwl from 'happy-nrwl'; `, './package.json': JSON.stringify(packageJson), + './package-lock.json': JSON.stringify(packageLockJson), './nx.json': JSON.stringify(nxJson), './workspace.json': JSON.stringify(workspaceJson), './tsconfig.base.json': JSON.stringify(tsConfigJson), diff --git a/packages/nx/src/project-graph/affected/locators/workspace-projects.ts b/packages/nx/src/project-graph/affected/locators/workspace-projects.ts index 8550d51225721..1dc5eb0cdef2c 100644 --- a/packages/nx/src/project-graph/affected/locators/workspace-projects.ts +++ b/packages/nx/src/project-graph/affected/locators/workspace-projects.ts @@ -35,7 +35,6 @@ export const getImplicitlyTouchedProjects: TouchedProjectLocator = ( const globalFiles = [ ...extractGlobalFilesFromInputs(nxJson, projectGraphNodes), 'nx.json', - 'package.json', ]; globalFiles.forEach((file) => { implicits[file] = '*' as any; diff --git a/packages/nx/src/project-graph/build-project-graph.spec.ts b/packages/nx/src/project-graph/build-project-graph.spec.ts index 8fbd65306bc85..634fa87369735 100644 --- a/packages/nx/src/project-graph/build-project-graph.spec.ts +++ b/packages/nx/src/project-graph/build-project-graph.spec.ts @@ -13,6 +13,7 @@ import { DependencyType } from '../config/project-graph'; describe('project graph', () => { let packageJson: any; + let packageLockJson: any; let workspaceJson: WorkspaceJsonConfiguration; let nxJson: NxJsonConfiguration; let tsConfigJson: any; @@ -22,6 +23,7 @@ describe('project graph', () => { defaultFileHasher.ensureInitialized(); packageJson = { name: '@nrwl/workspace-src', + version: '0.0.0', dependencies: { express: '4.0.0', 'happy-nrwl': '1.0.0', @@ -30,6 +32,56 @@ describe('project graph', () => { '@nrwl/workspace': '*', }, }; + packageLockJson = { + name: '@nrwl/workspace-src', + version: '0.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': packageJson, + 'node_modules/@nrwl/workspace': { + version: '15.0.0', + resolved: + 'https://registry.npmjs.org/@nrwl/workspace/-/@nrwl/workspace-15.0.0.tgz', + integrity: 'sha512-12345678==', + dev: true, + }, + 'node_modules/express': { + version: '4.0.0', + resolved: 'https://registry.npmjs.org/express/-/express-4.0.0.tgz', + integrity: 'sha512-12345678==', + engines: { + node: '>=4.2.0', + }, + }, + 'node_modules/happy-nrwl': { + version: '4.0.0', + resolved: + 'https://registry.npmjs.org/happy-nrwl/-/happy-nrwl-1.0.0.tgz', + integrity: 'sha512-12345678==', + }, + }, + dependencies: { + '@nrwl/workspace': { + version: '15.0.0', + resolved: + 'https://registry.npmjs.org/@nrwl/workspace/-/@nrwl/workspace-15.0.0.tgz', + integrity: 'sha512-12345678==', + dev: true, + }, + express: { + version: '4.0.0', + resolved: 'https://registry.npmjs.org/express/-/express-4.0.0.tgz', + integrity: 'sha512-12345678==', + }, + 'happy-nrwl': { + version: '1.0.0', + resolved: + 'https://registry.npmjs.org/happy-nrwl/-/happy-nrwl-1.0.0.tgz', + integrity: 'sha512-12345678==', + }, + }, + }; workspaceJson = { version: 2, projects: { @@ -125,6 +177,7 @@ describe('project graph', () => { export const LAZY = 'lazy lib'; `, './package.json': JSON.stringify(packageJson), + './package-lock.json': JSON.stringify(packageLockJson), './nx.json': JSON.stringify(nxJson), './workspace.json': JSON.stringify(workspaceJson), './tsconfig.base.json': JSON.stringify(tsConfigJson), diff --git a/packages/nx/src/project-graph/build-project-graph.ts b/packages/nx/src/project-graph/build-project-graph.ts index 9c3743ce21206..a877027215e05 100644 --- a/packages/nx/src/project-graph/build-project-graph.ts +++ b/packages/nx/src/project-graph/build-project-graph.ts @@ -25,6 +25,7 @@ import { getRootTsConfigPath } from '../utils/typescript'; import { ProjectFileMap, ProjectGraph, + ProjectGraphExternalNode, ProjectGraphProcessorContext, } from '../config/project-graph'; import { readJsonFile } from '../utils/fileutils'; @@ -39,6 +40,12 @@ import { readAllWorkspaceConfiguration, readNxJson, } from '../config/configuration'; +import { + lockFileExists, + lockFileHash, + mapLockFileDataToPartialGraph, + parseLockFile, +} from '../utils/lock-file/lock-file'; export async function buildProjectGraph() { const projectConfigurations = readAllWorkspaceConfiguration(); @@ -95,6 +102,17 @@ export async function buildProjectGraphUsingProjectFileMap( filesToProcess = projectFileMap; cachedFileData = {}; } + let partialGraph: ProjectGraph; + let lockHash = 'n/a'; + // during the create-nx-workspace lock file might not exists yet + if (lockFileExists()) { + lockHash = lockFileHash(); + if (cache && cache.lockFileHash === lockHash) { + partialGraph = isolatePartialGraphFromCache(cache); + } else { + partialGraph = mapLockFileDataToPartialGraph(parseLockFile()); + } + } const context = createContext( projectsConfigurations, nxJson, @@ -106,13 +124,15 @@ export async function buildProjectGraphUsingProjectFileMap( context, cachedFileData, projectGraphVersion, + partialGraph, packageJsonDeps ); const projectGraphCache = createCache( nxJson, packageJsonDeps, projectGraph, - rootTsConfig + rootTsConfig, + lockHash ); if (shouldWriteCache) { writeCache(projectGraphCache); @@ -129,19 +149,38 @@ function readCombinedDeps() { return { ...json.dependencies, ...json.devDependencies }; } +// extract only external nodes and their dependencies +function isolatePartialGraphFromCache(cache: ProjectGraphCache): ProjectGraph { + const dependencies = {}; + Object.keys(cache.dependencies).forEach((k) => { + if (cache.externalNodes[k]) { + dependencies[k] = cache.dependencies[k]; + } + }); + + return { + nodes: {}, + dependencies, + externalNodes: cache.externalNodes, + }; +} + async function buildProjectGraphUsingContext( nxJson: NxJsonConfiguration, ctx: ProjectGraphProcessorContext, cachedFileData: { [project: string]: { [file: string]: FileData } }, projectGraphVersion: string, - packageJsonDeps: { [packageName: string]: string } + partialGraph: ProjectGraph, + packageJsonDeps: Record ) { performance.mark('build project graph:start'); - const builder = new ProjectGraphBuilder(); + const builder = new ProjectGraphBuilder(partialGraph); buildWorkspaceProjectNodes(ctx, builder, nxJson); - buildNpmPackageNodes(builder); + if (!partialGraph) { + buildNpmPackageNodes(builder); + } for (const proj of Object.keys(cachedFileData)) { for (const f of builder.graph.nodes[proj].data.files) { const cached = cachedFileData[proj][f.file]; diff --git a/packages/nx/src/project-graph/nx-deps-cache.spec.ts b/packages/nx/src/project-graph/nx-deps-cache.spec.ts index 2266e6edc6b8b..e151196f5a34c 100644 --- a/packages/nx/src/project-graph/nx-deps-cache.spec.ts +++ b/packages/nx/src/project-graph/nx-deps-cache.spec.ts @@ -4,7 +4,6 @@ import { ProjectGraphCache, shouldRecomputeWholeGraph, } from './nx-deps-cache'; -import { createCache } from './nx-deps-cache'; import { ProjectGraph } from '../config/project-graph'; import { WorkspaceJsonConfiguration } from '../config/workspace-json-project-json'; import { NxJsonConfiguration } from '../config/nx-json'; @@ -299,7 +298,8 @@ describe('nx deps utils', () => { createNxJson({}), createPackageJsonDeps({}), createCache({}) as ProjectGraph, - {} + {}, + 'abcd1234' ); }); @@ -308,7 +308,8 @@ describe('nx deps utils', () => { createNxJson({}), createPackageJsonDeps({}), createCache({}) as ProjectGraph, - undefined + undefined, + 'abcd1234' ); expect(result).toBeDefined(); @@ -322,6 +323,7 @@ describe('nx deps utils', () => { '@nrwl/workspace': '12.0.0', plugin: '1.0.0', }, + lockFileHash: 'abcd1234', pathMappings: { mylib: ['libs/mylib/index.ts'], }, diff --git a/packages/nx/src/project-graph/nx-deps-cache.ts b/packages/nx/src/project-graph/nx-deps-cache.ts index 63c145c39cb8f..6191782158775 100644 --- a/packages/nx/src/project-graph/nx-deps-cache.ts +++ b/packages/nx/src/project-graph/nx-deps-cache.ts @@ -19,6 +19,7 @@ import { ProjectsConfigurations } from '../config/workspace-json-project-json'; export interface ProjectGraphCache { version: string; deps: Record; + lockFileHash: string; pathMappings: Record; nxJsonPlugins: { name: string; version: string }[]; pluginsConfig?: any; @@ -81,7 +82,8 @@ export function createCache( nxJson: NxJsonConfiguration<'*' | string[]>, packageJsonDeps: Record, projectGraph: ProjectGraph, - tsConfig: { compilerOptions?: { paths?: { [p: string]: any } } } + tsConfig: { compilerOptions?: { paths?: { [p: string]: any } } }, + lockFileHash: string ) { const nxJsonPlugins = (nxJson.plugins || []).map((p) => ({ name: p, @@ -90,6 +92,7 @@ export function createCache( const newValue: ProjectGraphCache = { version: projectGraph.version || '5.0', deps: packageJsonDeps, + lockFileHash, // compilerOptions may not exist, especially for repos converted through add-nx-to-monorepo pathMappings: tsConfig?.compilerOptions?.paths || {}, nxJsonPlugins, diff --git a/packages/nx/src/project-graph/project-graph.ts b/packages/nx/src/project-graph/project-graph.ts index baba451f025fc..5b328c3d3dd54 100644 --- a/packages/nx/src/project-graph/project-graph.ts +++ b/packages/nx/src/project-graph/project-graph.ts @@ -182,7 +182,7 @@ export function projectGraphAdapter( if (sourceVersion === targetVersion) { return projectGraph; } - if (sourceVersion === '5.0' && targetVersion === '4.0') { + if (+sourceVersion >= 5 && targetVersion === '4.0') { return projectGraphCompat5to4(projectGraph as ProjectGraph); } throw new Error( 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 0000000000000..cb088eb046b68 --- /dev/null +++ b/packages/nx/src/utils/lock-file/__snapshots__/lock-file.spec.ts.snap @@ -0,0 +1,685 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lock-file mapLockFileDataToExternalNodes should map npm lock file data to external nodes 1`] = ` +Object { + "data": Object { + "hash": "54342be9f4609650eba8e995881c5eb98c29b9d7bb95b9a699d9e9df68d6428e", + "packageName": "yargs", + "version": "17.5.1", + }, + "name": "npm:yargs", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map npm lock file data to external nodes 2`] = ` +Array [ + Object { + "source": "npm:yargs", + "target": "npm:cliui", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:escalade", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:get-caller-file", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:require-directory", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:string-width", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:y18n", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:yargs-parser", + "type": "static", + }, +] +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map pnpm lock file data to external nodes 1`] = ` +Object { + "data": Object { + "hash": "c22918aa52c9fdce78bf82451d8114e00b6d4375a8e5b1d1d96c6cf4755dbe1a", + "packageName": "yargs", + "version": "17.5.1", + }, + "name": "npm:yargs", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map pnpm lock file data to external nodes 2`] = ` +Array [ + Object { + "source": "npm:yargs", + "target": "npm:cliui", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:escalade", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:get-caller-file", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:require-directory", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:string-width", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:y18n", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:yargs-parser", + "type": "static", + }, +] +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex npm lock file 1`] = ` +Object { + "data": Object { + "hash": "1e61b9db38c17534ce427d40ce84f9c03b4c439253f659e9a48513a10a52b465", + "packageName": "nx", + "version": "14.7.5", + }, + "name": "npm:nx", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex npm lock file 2`] = ` +Array [ + Object { + "source": "npm:nx", + "target": "npm:@nrwl/cli", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:@nrwl/tao", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:@parcel/watcher", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:chalk", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:chokidar", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cli-cursor", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cli-spinners", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cliui", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:dotenv", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:enquirer", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:fast-glob", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:figures", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:flat", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:fs-extra", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:glob", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:ignore", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:js-yaml@4.1.0", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:jsonc-parser", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:minimatch", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:npm-run-path", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:open", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:semver", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:string-width", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tar-stream", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tmp", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tsconfig-paths", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tslib", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:v8-compile-cache", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:yargs", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:yargs-parser", + "type": "static", + }, +] +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex pnpm lock file 1`] = ` +Object { + "data": Object { + "hash": "1e61b9db38c17534ce427d40ce84f9c03b4c439253f659e9a48513a10a52b465", + "packageName": "nx", + "version": "14.7.5", + }, + "name": "npm:nx", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex pnpm lock file 2`] = ` +Array [ + Object { + "source": "npm:nx", + "target": "npm:@nrwl/cli", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:@nrwl/tao", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:@parcel/watcher", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:chalk", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:chokidar", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cli-cursor", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cli-spinners", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cliui", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:dotenv", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:enquirer", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:fast-glob", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:figures", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:flat", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:fs-extra", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:glob", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:ignore", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:js-yaml", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:jsonc-parser", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:minimatch", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:npm-run-path", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:open", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:semver", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:string-width", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tar-stream", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tmp", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tsconfig-paths", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tslib", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:v8-compile-cache", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:yargs", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:yargs-parser", + "type": "static", + }, +] +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex pnpm lock file 3`] = ` +Object { + "data": Object { + "hash": "6c5961e1a6ab89174727587860e480508f945407ce4c2cc7c9f1e46f181e6468", + "packageName": "@phenomnomnominal/tsquery", + "version": "4.1.1", + }, + "name": "npm:@phenomnomnominal/tsquery", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex pnpm lock file 4`] = ` +Array [ + Object { + "source": "npm:@phenomnomnominal/tsquery", + "target": "npm:esquery", + "type": "static", + }, + Object { + "source": "npm:@phenomnomnominal/tsquery", + "target": "npm:typescript", + "type": "static", + }, +] +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex yarn lock file 1`] = ` +Object { + "data": Object { + "hash": "1e61b9db38c17534ce427d40ce84f9c03b4c439253f659e9a48513a10a52b465", + "packageName": "nx", + "version": "14.7.5", + }, + "name": "npm:nx", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map successfully complex yarn lock file 2`] = ` +Array [ + Object { + "source": "npm:nx", + "target": "npm:@nrwl/cli", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:@nrwl/tao", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:@parcel/watcher", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:chalk", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:chokidar", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cli-cursor", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cli-spinners", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:cliui", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:dotenv", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:enquirer", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:fast-glob", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:figures", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:flat", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:fs-extra", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:glob", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:ignore", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:js-yaml", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:jsonc-parser", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:minimatch", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:npm-run-path", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:open", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:semver", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:string-width", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tar-stream", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tmp", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tsconfig-paths", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:tslib", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:v8-compile-cache", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:yargs", + "type": "static", + }, + Object { + "source": "npm:nx", + "target": "npm:yargs-parser", + "type": "static", + }, +] +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map yarn lock file data to external nodes 1`] = ` +Object { + "data": Object { + "hash": "c22918aa52c9fdce78bf82451d8114e00b6d4375a8e5b1d1d96c6cf4755dbe1a", + "packageName": "yargs", + "version": "17.5.1", + }, + "name": "npm:yargs", + "type": "npm", +} +`; + +exports[`lock-file mapLockFileDataToExternalNodes should map yarn lock file data to external nodes 2`] = ` +Array [ + Object { + "source": "npm:yargs", + "target": "npm:cliui", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:escalade", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:get-caller-file", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:require-directory", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:string-width", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:y18n", + "type": "static", + }, + Object { + "source": "npm:yargs", + "target": "npm:yargs-parser", + "type": "static", + }, +] +`; diff --git a/packages/nx/src/utils/lock-file/__snapshots__/npm.spec.ts.snap b/packages/nx/src/utils/lock-file/__snapshots__/npm.spec.ts.snap index 42a5887005d80..26e152616868e 100644 --- a/packages/nx/src/utils/lock-file/__snapshots__/npm.spec.ts.snap +++ b/packages/nx/src/utils/lock-file/__snapshots__/npm.spec.ts.snap @@ -19,6 +19,7 @@ Object { }, ], "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "rootVersion": true, "version": "2.2.0", }, } @@ -43,6 +44,7 @@ Object { }, ], "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "rootVersion": true, "version": "4.8.3", }, } 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 88c98099ea139..ca09f35d8edc0 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, @@ -24,6 +28,7 @@ Object { "resolution": Object { "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", }, + "rootVersion": true, "version": "2.2.0", }, } @@ -32,15 +37,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", @@ -50,6 +54,7 @@ Object { "resolution": Object { "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", }, + "rootVersion": true, "version": "4.8.3", }, } @@ -58,6 +63,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 +78,6 @@ Object { "@jridgewell/gen-mapping": "0.1.1", "@jridgewell/trace-mapping": "0.3.15", }, - "dev": true, }, "isDependency": false, "isDevDependency": false, @@ -79,6 +88,7 @@ Object { "resolution": Object { "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", }, + "rootVersion": true, "version": "2.2.0", }, } @@ -87,15 +97,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", @@ -105,6 +114,7 @@ Object { "resolution": Object { "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", }, + "rootVersion": true, "version": "4.8.3", }, } diff --git a/packages/nx/src/utils/lock-file/__snapshots__/yarn.spec.ts.snap b/packages/nx/src/utils/lock-file/__snapshots__/yarn.spec.ts.snap index c4716002d1abd..22175054f405d 100644 --- a/packages/nx/src/utils/lock-file/__snapshots__/yarn.spec.ts.snap +++ b/packages/nx/src/utils/lock-file/__snapshots__/yarn.spec.ts.snap @@ -14,6 +14,7 @@ Object { "@ampproject/remapping@npm:^2.1.0", ], "resolution": "@ampproject/remapping@npm:2.2.0", + "rootVersion": true, "version": "2.2.0", }, } @@ -33,6 +34,7 @@ Object { "typescript@npm:~4.8.2", ], "resolution": "typescript@npm:4.8.3", + "rootVersion": true, "version": "4.8.3", }, } @@ -50,6 +52,7 @@ Object { "@ampproject/remapping@^2.1.0", ], "resolved": "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d", + "rootVersion": true, "version": "2.2.0", }, } @@ -63,6 +66,7 @@ Object { "typescript@~4.8.2", ], "resolved": "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88", + "rootVersion": true, "version": "4.8.3", }, } diff --git a/packages/nx/src/utils/lock-file/lock-file-type.ts b/packages/nx/src/utils/lock-file/lock-file-type.ts index c51ace7999395..80a9c2a6cfcee 100644 --- a/packages/nx/src/utils/lock-file/lock-file-type.ts +++ b/packages/nx/src/utils/lock-file/lock-file-type.ts @@ -1,5 +1,6 @@ export interface PackageDependency { version?: string; + rootVersion?: boolean; packageMeta: any[]; dependencies?: Record; [key: string]: any; diff --git a/packages/nx/src/utils/lock-file/lock-file.spec.ts b/packages/nx/src/utils/lock-file/lock-file.spec.ts new file mode 100644 index 0000000000000..e2084a15c0451 --- /dev/null +++ b/packages/nx/src/utils/lock-file/lock-file.spec.ts @@ -0,0 +1,80 @@ +import { mapLockFileDataToPartialGraph } from './lock-file'; +import { parseNpmLockFile } from './npm'; +import { parsePnpmLockFile } from './pnpm'; +import { parseYarnLockFile } from './yarn'; +import { + lockFileYargsOnly as npmLockFileYargsOnly, + lockFile as npmLockFile, +} from './__fixtures__/npm.lock'; +import { + lockFileYargsOnly as pnpmLockFileYargsOnly, + lockFile as pnpmLockFile, +} from './__fixtures__/pnpm.lock'; +import { + lockFileDevkitAndYargs, + lockFile as yarnLockFile, +} from './__fixtures__/yarn.lock'; + +describe('lock-file', () => { + describe('mapLockFileDataToExternalNodes', () => { + it('should map yarn lock file data to external nodes', () => { + const lockFileData = parseYarnLockFile(lockFileDevkitAndYargs); + + const partialGraph = mapLockFileDataToPartialGraph(lockFileData); + + expect(partialGraph.externalNodes['npm:yargs']).toMatchSnapshot(); + expect(partialGraph.dependencies['npm:yargs']).toMatchSnapshot(); + }); + + it('should map successfully complex yarn lock file', () => { + const lockFileData = parseYarnLockFile(yarnLockFile); + + const partialGraph = mapLockFileDataToPartialGraph(lockFileData); + + expect(partialGraph.externalNodes['npm:nx']).toMatchSnapshot(); + expect(partialGraph.dependencies['npm:nx']).toMatchSnapshot(); + }); + + it('should map npm lock file data to external nodes', () => { + const lockFileData = parseNpmLockFile(npmLockFileYargsOnly); + + const partialGraph = mapLockFileDataToPartialGraph(lockFileData); + + expect(partialGraph.externalNodes['npm:yargs']).toMatchSnapshot(); + expect(partialGraph.dependencies['npm:yargs']).toMatchSnapshot(); + }); + + it('should map successfully complex npm lock file', () => { + const lockFileData = parseNpmLockFile(npmLockFile); + + const partialGraph = mapLockFileDataToPartialGraph(lockFileData); + + expect(partialGraph.externalNodes['npm:nx']).toMatchSnapshot(); + expect(partialGraph.dependencies['npm:nx']).toMatchSnapshot(); + }); + + it('should map pnpm lock file data to external nodes', () => { + const lockFileData = parsePnpmLockFile(pnpmLockFileYargsOnly); + + const partialGraph = mapLockFileDataToPartialGraph(lockFileData); + + expect(partialGraph.externalNodes['npm:yargs']).toMatchSnapshot(); + expect(partialGraph.dependencies['npm:yargs']).toMatchSnapshot(); + }); + + it('should map successfully complex pnpm lock file', () => { + const lockFileData = parsePnpmLockFile(pnpmLockFile); + + const partialGraph = mapLockFileDataToPartialGraph(lockFileData); + + expect(partialGraph.externalNodes['npm:nx']).toMatchSnapshot(); + expect(partialGraph.dependencies['npm:nx']).toMatchSnapshot(); + expect( + partialGraph.externalNodes['npm:@phenomnomnominal/tsquery'] + ).toMatchSnapshot(); + expect( + partialGraph.dependencies['npm:@phenomnomnominal/tsquery'] + ).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/nx/src/utils/lock-file/lock-file.ts b/packages/nx/src/utils/lock-file/lock-file.ts index 7149aae9a21d1..8d9702aa6b23e 100644 --- a/packages/nx/src/utils/lock-file/lock-file.ts +++ b/packages/nx/src/utils/lock-file/lock-file.ts @@ -18,7 +18,39 @@ import { import { LockFileData } from './lock-file-type'; import { workspaceRoot } from '../workspace-root'; import { join } from 'path'; -import { hashString } from './utils'; +import { + getNodeName, + hashExternalNodes, + hashString, + mapExternalNodeDependencies, +} from './utils'; +import { + ProjectGraph, + ProjectGraphExternalNode, +} from '../../config/project-graph'; +import { existsSync } from 'fs'; + +const YARN_LOCK_PATH = join(workspaceRoot, 'yarn.lock'); +const NPM_LOCK_PATH = join(workspaceRoot, 'package-lock.json'); +const PNPM_LOCK_PATH = join(workspaceRoot, 'pnpm-lock.yaml'); + +/** + * Check if lock file exists + */ +export function lockFileExists( + packageManager: PackageManager = detectPackageManager(workspaceRoot) +): boolean { + if (packageManager === 'yarn') { + return existsSync(YARN_LOCK_PATH); + } + if (packageManager === 'pnpm') { + return existsSync(PNPM_LOCK_PATH); + } + if (packageManager === 'npm') { + return existsSync(NPM_LOCK_PATH); + } + throw Error(`Unknown package manager ${packageManager} or lock file missing`); +} /** * Hashes lock file content @@ -28,13 +60,13 @@ export function lockFileHash( ): string { let file: string; if (packageManager === 'yarn') { - file = readFileSync(join(workspaceRoot, 'yarn.lock'), 'utf8'); + file = readFileSync(YARN_LOCK_PATH, 'utf8'); } if (packageManager === 'pnpm') { - file = readFileSync(join(workspaceRoot, 'pnpm-lock.yaml'), 'utf8'); + file = readFileSync(PNPM_LOCK_PATH, 'utf8'); } if (packageManager === 'npm') { - file = readFileSync(join(workspaceRoot, 'package-lock.json'), 'utf8'); + file = readFileSync(NPM_LOCK_PATH, 'utf8'); } if (file) { return hashString(file); @@ -52,20 +84,70 @@ export function parseLockFile( packageManager: PackageManager = detectPackageManager(workspaceRoot) ): LockFileData { if (packageManager === 'yarn') { - const file = readFileSync(join(workspaceRoot, 'yarn.lock'), 'utf8'); + const file = readFileSync(YARN_LOCK_PATH, 'utf8'); return parseYarnLockFile(file); } if (packageManager === 'pnpm') { - const file = readFileSync(join(workspaceRoot, 'pnpm-lock.yaml'), 'utf8'); + const file = readFileSync(PNPM_LOCK_PATH, 'utf8'); return parsePnpmLockFile(file); } if (packageManager === 'npm') { - const file = readFileSync(join(workspaceRoot, 'package-lock.json'), 'utf8'); + const file = readFileSync(NPM_LOCK_PATH, 'utf8'); return parseNpmLockFile(file); } throw Error(`Unknown package manager: ${packageManager}`); } +/** + * Maps lock file data to {@link ProjectGraphExternalNode} hash map + * @param lockFileData + * @returns + */ +export function mapLockFileDataToPartialGraph( + lockFileData: LockFileData +): ProjectGraph { + const result: ProjectGraph = { + dependencies: {}, + externalNodes: {}, + nodes: {}, + }; + const versionCache: Record = {}; + + Object.keys(lockFileData.dependencies).forEach((dep) => { + const versions = lockFileData.dependencies[dep]; + Object.keys(versions).forEach((nameVersion) => { + const packageVersion = versions[nameVersion]; + + // save external node + const nodeName = getNodeName( + dep, + packageVersion.version, + packageVersion.rootVersion + ); + result.externalNodes[nodeName] = { + type: 'npm', + name: nodeName, + data: { + version: packageVersion.version, + packageName: dep, + }, + }; + + const dependencies = mapExternalNodeDependencies( + nodeName, + packageVersion, + lockFileData.dependencies, + versionCache + ); + if (dependencies.length) { + result.dependencies[nodeName] = dependencies; + } + }); + }); + hashExternalNodes(result); + return result; +} + /** * Stringifies {@link LockFileData} content and writes it to lock file */ @@ -75,17 +157,17 @@ export function writeLockFile( ): void { if (packageManager === 'yarn') { const content = stringifyYarnLockFile(lockFile); - writeFileSync(join(workspaceRoot, 'yarn.lock'), content); + writeFileSync(YARN_LOCK_PATH, content); return; } if (packageManager === 'pnpm') { const content = stringifyPnpmLockFile(lockFile); - writeFileSync(join(workspaceRoot, 'pnpm-lock.yaml'), content); + writeFileSync(PNPM_LOCK_PATH, content); return; } if (packageManager === 'npm') { const content = stringifyNpmLockFile(lockFile); - writeFileSync(join(workspaceRoot, 'package-lock.json'), content); + writeFileSync(NPM_LOCK_PATH, content); return; } throw Error(`Unknown package manager: ${packageManager}`); diff --git a/packages/nx/src/utils/lock-file/npm.spec.ts b/packages/nx/src/utils/lock-file/npm.spec.ts index 2bed1d957d29b..069b22076937d 100644 --- a/packages/nx/src/utils/lock-file/npm.spec.ts +++ b/packages/nx/src/utils/lock-file/npm.spec.ts @@ -41,11 +41,22 @@ describe('npm LockFile utility', () => { '@jridgewell/gen-mapping@0.1.1' ] ).toBeDefined(); + // This is opposite from yarn and pnpm + expect( + parsedLockFile.dependencies['@jridgewell/gen-mapping'][ + '@jridgewell/gen-mapping@0.1.1' + ].rootVersion + ).toBeTruthy(); expect( parsedLockFile.dependencies['@jridgewell/gen-mapping'][ '@jridgewell/gen-mapping@0.3.2' ] ).toBeDefined(); + expect( + parsedLockFile.dependencies['@jridgewell/gen-mapping'][ + '@jridgewell/gen-mapping@0.3.2' + ].rootVersion + ).toBeFalsy(); }); it('should map various instances of the same version', () => { diff --git a/packages/nx/src/utils/lock-file/npm.ts b/packages/nx/src/utils/lock-file/npm.ts index 8730cf953fd9e..d2ddd92572494 100644 --- a/packages/nx/src/utils/lock-file/npm.ts +++ b/packages/nx/src/utils/lock-file/npm.ts @@ -69,9 +69,11 @@ function mapPackages(packages: Dependencies): LockFileData['dependencies'] { if (mappedPackages[packageName][newKey]) { mappedPackages[packageName][newKey].packageMeta.push(packageMeta); } else { + const rootVersion = key.split('/node_modules/').length === 1; mappedPackages[packageName][newKey] = { ...value, packageMeta: [packageMeta], + rootVersion, }; } }); @@ -115,7 +117,7 @@ export function stringifyNpmLockFile(lockFileData: LockFileData): string { // remapping the package back to package-lock format function unmapPackage( packages: Dependencies, - { packageMeta, ...value }: PackageDependency + { packageMeta, rootVersion, ...value }: PackageDependency ) { // we need to decompose value, to achieve particular field ordering const { diff --git a/packages/nx/src/utils/lock-file/pnpm.spec.ts b/packages/nx/src/utils/lock-file/pnpm.spec.ts index a72636f6b25ba..ca331606b4035 100644 --- a/packages/nx/src/utils/lock-file/pnpm.spec.ts +++ b/packages/nx/src/utils/lock-file/pnpm.spec.ts @@ -27,11 +27,21 @@ describe('pnpm LockFile utility', () => { '@jridgewell/gen-mapping@0.1.1' ] ).toBeDefined(); + expect( + parsedLockFile.dependencies['@jridgewell/gen-mapping'][ + '@jridgewell/gen-mapping@0.1.1' + ].rootVersion + ).toBeFalsy(); expect( parsedLockFile.dependencies['@jridgewell/gen-mapping'][ '@jridgewell/gen-mapping@0.3.2' ] ).toBeDefined(); + expect( + parsedLockFile.dependencies['@jridgewell/gen-mapping'][ + '@jridgewell/gen-mapping@0.3.2' + ].rootVersion + ).toBeTruthy(); }); it('should map various instances of the same version', () => { diff --git a/packages/nx/src/utils/lock-file/pnpm.ts b/packages/nx/src/utils/lock-file/pnpm.ts index aa8a6abea5d52..525ad3552d3e4 100644 --- a/packages/nx/src/utils/lock-file/pnpm.ts +++ b/packages/nx/src/utils/lock-file/pnpm.ts @@ -1,6 +1,6 @@ import { LockFileData, PackageDependency } from './lock-file-type'; import { load, dump } from '@zkochan/js-yaml'; -import { sortObject, hashString } from './utils'; +import { sortObject, hashString, isRootVersion } from './utils'; type PackageMeta = { key: string; @@ -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,13 +93,29 @@ function mapPackages( mappedPackages[packageName][newKey].packageMeta.push(meta); } else { mappedPackages[packageName][newKey] = { - resolution, - engines, + ...value, version, packageMeta: [meta], }; } }); + Object.keys(mappedPackages).forEach((packageName) => { + const versions = mappedPackages[packageName]; + const versionKeys = Object.keys(versions); + + if (versionKeys.length === 1) { + versions[versionKeys[0]].rootVersion = true; + } else { + const rootVersionKey = versionKeys.find((v) => + isRootVersion(packageName, versions[v].version) + ); + // this should never happen, but just in case + if (rootVersionKey) { + versions[rootVersionKey].rootVersion = true; + } + } + }); + return mappedPackages; } @@ -114,7 +128,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 +162,14 @@ function mapMetaInformation( isDependency, isDevDependency, specifier, - dependencyDetails, + dependencyDetails: { + ...(dependencyDetails.dependencies && { + dependencies: dependencyDetails.dependencies, + }), + ...(dependencyDetails.peerDependencies && { + peerDependencies: dependencyDetails.peerDependencies, + }), + }, }; } @@ -209,7 +230,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: _, rootVersion, ...rest }) => { (packageMeta as PackageMeta[]).forEach((meta) => { const { key, specifier } = meta; @@ -231,8 +252,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 78c61549acdf3..7b8e76d336659 100644 --- a/packages/nx/src/utils/lock-file/utils.ts +++ b/packages/nx/src/utils/lock-file/utils.ts @@ -1,4 +1,13 @@ +import { satisfies } from 'semver'; import { defaultHashing } from '../../hasher/hashing-impl'; +import { PackageDependency, PackageVersions } from './lock-file-type'; +import { workspaceRoot } from '../workspace-root'; +import { existsSync, readFileSync } from 'fs'; +import { + ProjectGraph, + ProjectGraphDependency, + ProjectGraphExternalNode, +} from '../../config/project-graph'; /** * Simple sort function to ensure keys are ordered alphabetically @@ -7,15 +16,19 @@ import { defaultHashing } from '../../hasher/hashing-impl'; */ export function sortObject( obj: Record, - valueTransformator: (value: T) => any = (value) => value + valueTransformator: (value: T) => any = (value) => value, + descending = false ): Record | undefined { const keys = Object.keys(obj); if (keys.length === 0) { return; } - + keys.sort(); + if (descending) { + keys.reverse(); + } const result: Record = {}; - keys.sort().forEach((key) => { + keys.forEach((key) => { result[key] = valueTransformator(obj[key]); }); return result; @@ -29,3 +42,153 @@ export function sortObject( export function hashString(fileContent: string): string { return defaultHashing.hashArray([fileContent]); } + +export function findMatchingVersion( + packageName: string, + packageVersions: PackageVersions, + version: string +): string { + // if it's fixed version, just return it + if (packageVersions[`${packageName}@${version}`]) { + return version; + } + // otherwise search for the matching version + return Object.values(packageVersions).find((v) => + satisfies(v.version, version) + )?.version; +} + +export function isRootVersion(packageName: string, version: string): boolean { + const fullPath = `${workspaceRoot}/node_modules/${packageName}/package.json`; + if (existsSync(fullPath)) { + const content = readFileSync(fullPath, 'utf-8'); + return JSON.parse(content).version === version; + } else { + return false; + } +} + +/** + * Returns node name depending whether it's root version or nested + */ +export function getNodeName( + dep: string, + version: string, + rootVersion: boolean +): `npm:${string}` { + return rootVersion ? `npm:${dep}` : `npm:${dep}@${version}`; +} + +export function mapExternalNodeDependencies( + nodeName: string, + packageVersion: PackageDependency, + dependencies: Record, + versionCache: Record +) { + const result = []; + + const combinedDependencies = + packageVersion.dependencies || packageVersion.peerDependencies + ? { + ...(packageVersion.dependencies || {}), + ...(packageVersion.peerDependencies || {}), + } + : undefined; + const transitiveDeps = mapTransitiveDependencies( + dependencies, + combinedDependencies, + versionCache + ); + // map transitive dependencies to dependencies hash map + transitiveDeps.forEach((dep) => { + result.push({ + type: 'static', + source: nodeName, + target: dep, + }); + }); + + return result; +} + +// Finds the maching version of each dependency of the package and +// maps each {package}:{versionRange} pair to "npm:{package}@{version}" (when transitive) or "npm:{package}" (when root) +function mapTransitiveDependencies( + packages: Record, + dependencies: Record, + versionCache: Record +): string[] { + if (!dependencies) { + return []; + } + const result: string[] = []; + + Object.keys(dependencies).forEach((packageName) => { + const cleanVersion = dependencies[packageName].split('_')[0]; + const key = `${packageName}@${cleanVersion}`; + + // some of the peer dependencies might not be installed, + // we don't have them as nodes in externalNodes + // so there's no need to map them as dependencies + if (!packages[packageName]) { + return; + } + + // if we already processed this dependency, use the version from the cache + if (versionCache[key]) { + result.push(versionCache[key]); + } else { + const versions = packages[packageName]; + const version = findMatchingVersion(packageName, versions, cleanVersion); + // for some peer dependencies, we won't find installed version so we'll just ignore those + if (version) { + const nodeName = getNodeName( + packageName, + version, + versions[`${packageName}@${version}`]?.rootVersion + ); + result.push(nodeName); + versionCache[key] = nodeName; + } + } + }); + + return result; +} + +export function hashExternalNodes(projectGraph: ProjectGraph) { + Object.keys(projectGraph.externalNodes).forEach((key) => { + if (!projectGraph.externalNodes[key].data.hash) { + // hash it using it's dependencies + hashExternalNode(projectGraph.externalNodes[key], projectGraph); + } + }); +} + +function hashExternalNode(node: ProjectGraphExternalNode, graph: ProjectGraph) { + const hashKey = `${node.data.packageName}@${node.data.version}`; + if (!graph.dependencies[node.name]) { + node.data.hash = hashString(hashKey); + } else { + const hashingInput = [hashKey]; + traverseExternalNodesDependencies(node.name, graph, hashingInput); + node.data.hash = defaultHashing.hashArray(hashingInput.sort()); + } +} + +function traverseExternalNodesDependencies( + projectName: string, + graph: ProjectGraph, + visited: string[] +) { + graph.dependencies[projectName].forEach((d) => { + const target = graph.externalNodes[d.target]; + const targetKey = `${target.data.packageName}@${target.data.version}`; + if (visited.indexOf(targetKey) === -1) { + visited.push(targetKey); + if (graph.dependencies[d.target]) { + traverseExternalNodesDependencies(d.target, graph, visited); + } + } + }); +} diff --git a/packages/nx/src/utils/lock-file/yarn.spec.ts b/packages/nx/src/utils/lock-file/yarn.spec.ts index e7b7d4d73a27d..64e142945461b 100644 --- a/packages/nx/src/utils/lock-file/yarn.spec.ts +++ b/packages/nx/src/utils/lock-file/yarn.spec.ts @@ -13,7 +13,9 @@ import { describe('yarn LockFile utility', () => { describe('classic', () => { + console.time('before'); const parsedLockFile = parseYarnLockFile(lockFile); + console.timeEnd('before'); it('should parse lockfile correctly', () => { expect(parsedLockFile.lockFileMetadata).toBeUndefined(); @@ -34,11 +36,21 @@ describe('yarn LockFile utility', () => { '@jridgewell/gen-mapping@0.1.1' ] ).toBeDefined(); + expect( + parsedLockFile.dependencies['@jridgewell/gen-mapping'][ + '@jridgewell/gen-mapping@0.1.1' + ].rootVersion + ).toBeFalsy(); expect( parsedLockFile.dependencies['@jridgewell/gen-mapping'][ '@jridgewell/gen-mapping@0.3.2' ] ).toBeDefined(); + expect( + parsedLockFile.dependencies['@jridgewell/gen-mapping'][ + '@jridgewell/gen-mapping@0.3.2' + ].rootVersion + ).toBeTruthy(); }); it('should map various instances of the same version', () => { diff --git a/packages/nx/src/utils/lock-file/yarn.ts b/packages/nx/src/utils/lock-file/yarn.ts index 8092eb56a71a9..98e2e62113553 100644 --- a/packages/nx/src/utils/lock-file/yarn.ts +++ b/packages/nx/src/utils/lock-file/yarn.ts @@ -5,7 +5,7 @@ import { PackageDependency, PackageVersions, } from './lock-file-type'; -import { sortObject, hashString } from './utils'; +import { sortObject, hashString, isRootVersion } from './utils'; type LockFileDependencies = Record< string, @@ -70,6 +70,22 @@ function mapPackages( } } }); + Object.keys(mappedPackages).forEach((packageName) => { + const versions = mappedPackages[packageName]; + const versionKeys = Object.keys(versions); + + if (versionKeys.length === 1) { + versions[versionKeys[0]].rootVersion = true; + } else { + const rootVersionKey = versionKeys.find((v) => + isRootVersion(packageName, versions[v].version) + ); + // this should never happen, but just in case + if (rootVersionKey) { + versions[rootVersionKey].rootVersion = true; + } + } + }); return [mappedPackages, workspacePackages]; } @@ -118,7 +134,7 @@ function unmapPackages( Object.values(dependencies).forEach((packageVersions) => { Object.values(packageVersions).forEach((value) => { - const { packageMeta, ...rest } = value; + const { packageMeta, rootVersion, ...rest } = value; if (isBerry) { // berry's `stringifySyml` does not combine packages // we have to do it manually diff --git a/packages/webpack/src/utils/versions.ts b/packages/webpack/src/utils/versions.ts index 12125d9969acb..c37ad2fb287e6 100644 --- a/packages/webpack/src/utils/versions.ts +++ b/packages/webpack/src/utils/versions.ts @@ -1,5 +1,5 @@ export const nxVersion = require('../../package.json').version; export const swcLoaderVersion = '0.1.15'; -export const swcHelpersVersion = '~0.3.3'; +export const swcHelpersVersion = '~0.4.11'; export const tsLibVersion = '^2.3.0';