From b99b1c9c675d813b65106dd2cca37faffcc1b70a Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 9 Nov 2023 12:51:33 -0500 Subject: [PATCH] feat(linter): add option for @nx/dependency-checks to update workspace dependencies using local file paths --- .../src/rules/dependency-checks.ts | 7 ++ .../src/utils/find-npm-dependencies.spec.ts | 70 ++++++++++++++++++- .../js/src/utils/find-npm-dependencies.ts | 36 +++++++--- 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/dependency-checks.ts b/packages/eslint-plugin/src/rules/dependency-checks.ts index 15c09105417bec..d5edd435e5a123 100644 --- a/packages/eslint-plugin/src/rules/dependency-checks.ts +++ b/packages/eslint-plugin/src/rules/dependency-checks.ts @@ -22,6 +22,7 @@ export type Options = [ ignoredDependencies?: string[]; ignoredFiles?: string[]; includeTransitiveDependencies?: boolean; + useLocalPathsForWorkspaceDependencies?: boolean; } ]; @@ -53,6 +54,7 @@ export default ESLintUtils.RuleCreator(() => ``)({ checkObsoleteDependencies: { type: 'boolean' }, checkVersionMismatches: { type: 'boolean' }, includeTransitiveDependencies: { type: 'boolean' }, + useLocalPathsForWorkspaceDependencies: { type: 'boolean' }, }, additionalProperties: false, }, @@ -73,6 +75,7 @@ export default ESLintUtils.RuleCreator(() => ``)({ ignoredDependencies: [], ignoredFiles: [], includeTransitiveDependencies: false, + useLocalPathsForWorkspaceDependencies: false, }, ], create( @@ -86,6 +89,7 @@ export default ESLintUtils.RuleCreator(() => ``)({ checkObsoleteDependencies, checkVersionMismatches, includeTransitiveDependencies, + useLocalPathsForWorkspaceDependencies, }, ] ) { @@ -136,6 +140,7 @@ export default ESLintUtils.RuleCreator(() => ``)({ { includeTransitiveDependencies, ignoredFiles, + useLocalPathsForWorkspaceDependencies, } ); const expectedDependencyNames = Object.keys(npmDependencies); @@ -203,6 +208,8 @@ export default ESLintUtils.RuleCreator(() => ``)({ return; } if ( + npmDependencies[packageName].startsWith('file:') || + packageRange.startsWith('file:') || npmDependencies[packageName] === '*' || packageRange === '*' || satisfies(npmDependencies[packageName], packageRange, { diff --git a/packages/js/src/utils/find-npm-dependencies.spec.ts b/packages/js/src/utils/find-npm-dependencies.spec.ts index 08088ee72419df..3746a6b109421e 100644 --- a/packages/js/src/utils/find-npm-dependencies.spec.ts +++ b/packages/js/src/utils/find-npm-dependencies.spec.ts @@ -389,12 +389,78 @@ describe('findNpmDependencies', () => { expect( findNpmDependencies('/root', lib1, projectGraph, projectFileMap, 'build') ).toEqual({ - '@acme/lib3': '*', + '@acme/lib3': '0.0.1', }); expect( findNpmDependencies('/root', lib2, projectGraph, projectFileMap, 'build') ).toEqual({ - '@acme/lib3': '*', + '@acme/lib3': '0.0.1', + }); + }); + + it('should support local path for workspace dependencies', () => { + vol.fromJSON( + { + './libs/c/package.json': JSON.stringify({ + name: '@acme/c', + version: '0.0.1', + }), + './nx.json': JSON.stringify(nxJson), + }, + '/root' + ); + const a = { + name: 'a', + type: 'lib' as const, + data: { + root: 'libs/a', + targets: { build: {} }, + }, + }; + const b = { + name: 'b', + type: 'lib' as const, + data: { + root: 'libs/b', + targets: { build: {} }, + }, + }; + const c = { + name: 'c', + type: 'lib' as const, + data: { + root: 'libs/c', + targets: { build: {} }, + }, + }; + const projectGraph = { + nodes: { + a: a, + b: b, + c: c, + }, + externalNodes: {}, + dependencies: {}, + }; + const projectFileMap = { + a: [{ file: 'libs/a/index.ts', hash: '123', deps: ['c'] }], + b: [{ file: 'libs/a/index.ts', hash: '123', deps: ['c'] }], + c: [], + }; + + expect( + findNpmDependencies('/root', a, projectGraph, projectFileMap, 'build', { + useLocalPathsForWorkspaceDependencies: true, + }) + ).toEqual({ + '@acme/c': 'file:../c', + }); + expect( + findNpmDependencies('/root', b, projectGraph, projectFileMap, 'build', { + useLocalPathsForWorkspaceDependencies: true, + }) + ).toEqual({ + '@acme/c': 'file:../c', }); }); diff --git a/packages/js/src/utils/find-npm-dependencies.ts b/packages/js/src/utils/find-npm-dependencies.ts index b913138e7f32e6..4ee0b911bf9f8e 100644 --- a/packages/js/src/utils/find-npm-dependencies.ts +++ b/packages/js/src/utils/find-npm-dependencies.ts @@ -1,16 +1,16 @@ -import { join } from 'path'; +import { join, relative } from 'path'; import { readNxJson } from 'nx/src/config/configuration'; import { - getTargetInputs, filterUsingGlobPatterns, + getTargetInputs, } from 'nx/src/hasher/task-hasher'; import { + joinPathFragments, + normalizePath, + type ProjectFileMap, type ProjectGraph, type ProjectGraphProjectNode, - type ProjectFileMap, readJsonFile, - FileData, - joinPathFragments, } from '@nx/devkit'; import { fileExists } from 'nx/src/utils/fileutils'; import { fileDataDepTarget } from 'nx/src/config/project-graph'; @@ -28,6 +28,7 @@ export function findNpmDependencies( options: { includeTransitiveDependencies?: boolean; ignoredFiles?: string[]; + useLocalPathsForWorkspaceDependencies?: boolean; } = {} ): Record { let seen: null | Set = null; @@ -51,6 +52,7 @@ export function findNpmDependencies( projectFileMap, buildTarget, options.ignoredFiles, + options.useLocalPathsForWorkspaceDependencies, collectedDeps ); @@ -86,6 +88,7 @@ function collectDependenciesFromFileMap( projectFileMap: ProjectFileMap, buildTarget: string, ignoredFiles: string[], + useLocalPathsForWorkspaceDependencies: boolean, npmDeps: Record ): void { const rawFiles = projectFileMap[sourceProject.name]; @@ -140,12 +143,26 @@ function collectDependenciesFromFileMap( // Make sure package.json exists and has a valid name. packageJson?.name ) { - // This is a workspace lib so we can't reliably read in a specific version since it depends on how the workspace is set up. - // ASSUMPTION: Most users will use '*' for workspace lib versions. Otherwise, they can manually update it. - npmDeps[packageJson.name] = '*'; + let version: string; + if (useLocalPathsForWorkspaceDependencies) { + // Find the relative `file:...` path and use that as the version value. + // This is useful for monorepos like Nx where the release will handle setting the correct version in dist. + const depRoot = join(workspaceRoot, workspaceDep.data.root); + const ownRoot = join(workspaceRoot, sourceProject.data.root); + const relativePath = relative(ownRoot, depRoot); + const filePath = normalizePath(relativePath); // normalize slashes for windows + version = `file:${filePath}`; + } else { + // Otherwise, read the version from the dependencies `package.json` file. + // This is useful for monorepos that commit release versions. + // Users can also set version as "*" in source `package.json` files, which will be the value set here. + // This is useful if they use custom scripts to update them in dist. + version = packageJson.version ?? '*'; // fallback in case version is missing + } + npmDeps[packageJson.name] = version; seenWorkspaceDeps[workspaceDep.name] = { name: packageJson.name, - version: '*', + version, }; } } @@ -158,6 +175,7 @@ function readPackageJson( workspaceRoot: string ): null | { name: string; + version?: string; dependencies?: Record; optionalDependencies?: Record; peerDependencies?: Record;