diff --git a/docs/generated/packages/eslint-plugin/documents/dependency-checks.md b/docs/generated/packages/eslint-plugin/documents/dependency-checks.md index 7a3a3398f3..7972ee0f17 100644 --- a/docs/generated/packages/eslint-plugin/documents/dependency-checks.md +++ b/docs/generated/packages/eslint-plugin/documents/dependency-checks.md @@ -79,13 +79,14 @@ Sometimes we intentionally want to add or remove a dependency from our `package. ## Options -| Property | Type | Default | Description | -| ------------------------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| buildTargets | _Array_ | _["build"]_ | List of build target names | -| checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies | -| checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies | -| checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version | -| ignoredDependencies | _Array_ | _[]_ | List of dependencies to ignore for checks | -| ignoredFiles | _Array_ | N/A | List of files to ignore when collecting dependencies. The default value will be set based on the selected executor during the generation. | -| includeTransitiveDependencies | _boolean_ | _false_ | Enable to collect dependencies of children projects | -| useLocalPathsForWorkspaceDependencies | _boolean_ | _false_ | Set workspace dependencies as relative file:// paths. Useful for monorepos that link via file:// in package.json files. | +| Property | Type | Default | Description | +| ------------------------------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| buildTargets | _Array_ | _["build"]_ | List of build target names | +| checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies | +| checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies | +| checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version | +| ignoredDependencies | _Array_ | _[]_ | List of dependencies to ignore for checks | +| ignoredFiles | _Array_ | N/A | List of files to ignore when collecting dependencies. The default value will be set based on the selected executor during the generation. | +| includeTransitiveDependencies | _boolean_ | _false_ | Enable to collect dependencies of children projects | +| useLocalPathsForWorkspaceDependencies | _boolean_ | _false_ | Set workspace dependencies as relative file:// paths. Useful for monorepos that link via file:// in package.json files. | +| runtimeHelpers | _Array_ | _[]_ | List of helper packages used by the built output (e.g. `tslib` when using `tsc` and `importHelpers` is set to `true`). The rule already detects some of them in some scenarios, but this option can be used to detect them when it doesn't happen automatically. | diff --git a/docs/shared/packages/eslint/dependency-checks.md b/docs/shared/packages/eslint/dependency-checks.md index 7a3a3398f3..7972ee0f17 100644 --- a/docs/shared/packages/eslint/dependency-checks.md +++ b/docs/shared/packages/eslint/dependency-checks.md @@ -79,13 +79,14 @@ Sometimes we intentionally want to add or remove a dependency from our `package. ## Options -| Property | Type | Default | Description | -| ------------------------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| buildTargets | _Array_ | _["build"]_ | List of build target names | -| checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies | -| checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies | -| checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version | -| ignoredDependencies | _Array_ | _[]_ | List of dependencies to ignore for checks | -| ignoredFiles | _Array_ | N/A | List of files to ignore when collecting dependencies. The default value will be set based on the selected executor during the generation. | -| includeTransitiveDependencies | _boolean_ | _false_ | Enable to collect dependencies of children projects | -| useLocalPathsForWorkspaceDependencies | _boolean_ | _false_ | Set workspace dependencies as relative file:// paths. Useful for monorepos that link via file:// in package.json files. | +| Property | Type | Default | Description | +| ------------------------------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| buildTargets | _Array_ | _["build"]_ | List of build target names | +| checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies | +| checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies | +| checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version | +| ignoredDependencies | _Array_ | _[]_ | List of dependencies to ignore for checks | +| ignoredFiles | _Array_ | N/A | List of files to ignore when collecting dependencies. The default value will be set based on the selected executor during the generation. | +| includeTransitiveDependencies | _boolean_ | _false_ | Enable to collect dependencies of children projects | +| useLocalPathsForWorkspaceDependencies | _boolean_ | _false_ | Set workspace dependencies as relative file:// paths. Useful for monorepos that link via file:// in package.json files. | +| runtimeHelpers | _Array_ | _[]_ | List of helper packages used by the built output (e.g. `tslib` when using `tsc` and `importHelpers` is set to `true`). The rule already detects some of them in some scenarios, but this option can be used to detect them when it doesn't happen automatically. | diff --git a/packages/eslint-plugin/src/rules/dependency-checks.spec.ts b/packages/eslint-plugin/src/rules/dependency-checks.spec.ts index e2b13465b0..c30dbfe8a7 100644 --- a/packages/eslint-plugin/src/rules/dependency-checks.spec.ts +++ b/packages/eslint-plugin/src/rules/dependency-checks.spec.ts @@ -1699,6 +1699,113 @@ describe('Dependency checks (eslint)', () => { `); expect(failures[0].line).toEqual(3); }); + + it('should require packages in runtimeHelpers', () => { + const packageJson = { + name: '@mycompany/liba', + dependencies: { external1: '^16.0.0' }, + }; + + const swcrc = { jsc: { externalHelpers: true } }; + + const fileSys = { + './libs/liba/package.json': JSON.stringify(packageJson, null, 2), + './libs/liba/src/index.ts': '', + './libs/liba/.swcrc': JSON.stringify(swcrc, null, 2), + './package.json': JSON.stringify(rootPackageJson, null, 2), + }; + vol.fromJSON(fileSys, '/root'); + + const failures = runRule( + { runtimeHelpers: ['@swc/helpers'] }, + `/root/libs/liba/package.json`, + JSON.stringify(packageJson, null, 2), + { + nodes: { + liba: { + name: 'liba', + type: 'lib', + data: { + root: 'libs/liba', + targets: { + build: { + // custom executor that the rule wouldn't know about + executor: '@my-org/some-package:build', + }, + }, + }, + }, + }, + externalNodes, + dependencies: { + liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }], + }, + }, + { + liba: [ + createFile(`libs/liba/src/main.ts`, ['npm:external1']), + createFile(`libs/liba/package.json`, ['npm:external1']), + ], + } + ); + expect(failures.length).toEqual(1); + expect(failures[0].message).toMatchInlineSnapshot(` + "The "liba" project uses the following packages, but they are missing from "dependencies": + - @swc/helpers" + `); + expect(failures[0].line).toEqual(3); + }); + + it('should not report unused packages when specified in runtimeHelpers', () => { + const packageJson = { + name: '@mycompany/liba', + dependencies: { '@swc/helpers': '1.2.3', external1: '^16.0.0' }, + }; + + const swcrc = { jsc: { externalHelpers: true } }; + + const fileSys = { + './libs/liba/package.json': JSON.stringify(packageJson, null, 2), + './libs/liba/src/index.ts': '', + './libs/liba/.swcrc': JSON.stringify(swcrc, null, 2), + './package.json': JSON.stringify(rootPackageJson, null, 2), + }; + vol.fromJSON(fileSys, '/root'); + + const failures = runRule( + { runtimeHelpers: ['@swc/helpers'] }, + `/root/libs/liba/package.json`, + JSON.stringify(packageJson, null, 2), + { + nodes: { + liba: { + name: 'liba', + type: 'lib', + data: { + root: 'libs/liba', + targets: { + build: { + // custom executor that the rule wouldn't know about + executor: '@my-org/some-package:build', + }, + }, + }, + }, + }, + externalNodes, + dependencies: { + liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }], + }, + }, + { + liba: [ + createFile(`libs/liba/src/main.ts`, ['npm:external1']), + createFile(`libs/liba/package.json`, ['npm:external1']), + ], + } + ); + expect(failures.length).toEqual(0); + }); }); function createFile(f: string, deps?: FileDataDependency[]): FileData { diff --git a/packages/eslint-plugin/src/rules/dependency-checks.ts b/packages/eslint-plugin/src/rules/dependency-checks.ts index 7808a5c946..4b90c148e4 100644 --- a/packages/eslint-plugin/src/rules/dependency-checks.ts +++ b/packages/eslint-plugin/src/rules/dependency-checks.ts @@ -27,6 +27,7 @@ export type Options = [ ignoredFiles?: string[]; includeTransitiveDependencies?: boolean; useLocalPathsForWorkspaceDependencies?: boolean; + runtimeHelpers?: string[]; } ]; @@ -61,6 +62,7 @@ export default ESLintUtils.RuleCreator( checkVersionMismatches: { type: 'boolean' }, includeTransitiveDependencies: { type: 'boolean' }, useLocalPathsForWorkspaceDependencies: { type: 'boolean' }, + runtimeHelpers: { type: 'array', items: { type: 'string' } }, }, additionalProperties: false, }, @@ -82,6 +84,7 @@ export default ESLintUtils.RuleCreator( ignoredFiles: [], includeTransitiveDependencies: false, useLocalPathsForWorkspaceDependencies: false, + runtimeHelpers: [], }, ], create( @@ -96,6 +99,7 @@ export default ESLintUtils.RuleCreator( checkVersionMismatches, includeTransitiveDependencies, useLocalPathsForWorkspaceDependencies, + runtimeHelpers, }, ] ) { @@ -147,6 +151,7 @@ export default ESLintUtils.RuleCreator( includeTransitiveDependencies, ignoredFiles, useLocalPathsForWorkspaceDependencies, + runtimeHelpers, } ); const expectedDependencyNames = Object.keys(npmDependencies); diff --git a/packages/js/src/utils/find-npm-dependencies.spec.ts b/packages/js/src/utils/find-npm-dependencies.spec.ts index 95df84289b..680f6715e3 100644 --- a/packages/js/src/utils/find-npm-dependencies.spec.ts +++ b/packages/js/src/utils/find-npm-dependencies.spec.ts @@ -374,6 +374,104 @@ describe('findNpmDependencies', () => { expect(results).toEqual({}); }); + it('should pick up helper npm dependencies from runtimeHelpers', () => { + const libWithUnknownExecutor = { + name: 'my-lib', + type: 'lib' as const, + data: { + root: 'libs/my-lib', + targets: { + build: { executor: '@my-org/some-package:build' }, + }, + }, + }; + const projectGraph = { + nodes: { + 'my-lib': libWithUnknownExecutor, + }, + externalNodes: { + 'npm:tslib': { + name: 'npm:tslib' as const, + type: 'npm' as const, + data: { packageName: 'tslib', version: '2.6.0' }, + }, + 'npm:@swc/helpers': { + name: 'npm:@swc/helpers' as const, + type: 'npm' as const, + data: { packageName: '@swc/helpers', version: '0.5.0' }, + }, + }, + dependencies: {}, + }; + const projectFileMap = { 'my-lib': [] }; + + const results = findNpmDependencies( + '/root', + libWithUnknownExecutor, + projectGraph, + projectFileMap, + 'build', + { runtimeHelpers: ['tslib'] } + ); + + expect(results).toStrictEqual({ tslib: '2.6.0' }); + }); + + it('should not duplicate helper npm dependencies when additionally set in runtimeHelpers', () => { + vol.fromJSON( + { + './nx.json': JSON.stringify(nxJson), + './libs/my-lib/tsconfig.json': JSON.stringify({ + compilerOptions: { importHelpers: true }, + }), + }, + '/root' + ); + const libWithUnknownExecutor = { + name: 'my-lib', + type: 'lib' as const, + data: { + root: 'libs/my-lib', + targets: { + build: { + executor: '@nx/js:tsc', + options: { tsConfig: 'libs/my-lib/tsconfig.json' }, + }, + }, + }, + }; + const projectGraph = { + nodes: { + 'my-lib': libWithUnknownExecutor, + }, + externalNodes: { + 'npm:tslib': { + name: 'npm:tslib' as const, + type: 'npm' as const, + data: { packageName: 'tslib', version: '2.6.0' }, + }, + 'npm:@swc/helpers': { + name: 'npm:@swc/helpers' as const, + type: 'npm' as const, + data: { packageName: '@swc/helpers', version: '0.5.0' }, + }, + }, + dependencies: {}, + }; + const projectFileMap = { 'my-lib': [] }; + + const results = findNpmDependencies( + '/root', + libWithUnknownExecutor, + projectGraph, + projectFileMap, + 'build', + { runtimeHelpers: ['tslib'] } + ); + + expect(results).toStrictEqual({ tslib: '2.6.0' }); + }); + it('should support recursive collection of dependencies', () => { vol.fromJSON( { diff --git a/packages/js/src/utils/find-npm-dependencies.ts b/packages/js/src/utils/find-npm-dependencies.ts index 54d844921a..49b197e711 100644 --- a/packages/js/src/utils/find-npm-dependencies.ts +++ b/packages/js/src/utils/find-npm-dependencies.ts @@ -29,6 +29,7 @@ export function findNpmDependencies( includeTransitiveDependencies?: boolean; ignoredFiles?: string[]; useLocalPathsForWorkspaceDependencies?: boolean; + runtimeHelpers?: string[]; } = {} ): Record { let seen: null | Set = null; @@ -61,6 +62,7 @@ export function findNpmDependencies( currentProject, projectGraph, buildTarget, + options.runtimeHelpers, collectedDeps ); @@ -194,8 +196,23 @@ function collectHelperDependencies( sourceProject: ProjectGraphProjectNode, projectGraph: ProjectGraph, buildTarget: string, + runtimeHelpers: string[] | undefined, npmDeps: Record ): void { + if (runtimeHelpers?.length > 0) { + for (const helper of runtimeHelpers) { + if ( + !npmDeps[helper] && + projectGraph.externalNodes[`npm:${helper}`]?.type === 'npm' + ) { + npmDeps[helper] = + projectGraph.externalNodes[`npm:${helper}`].data.version; + } + } + + return; + } + const target = sourceProject.data.targets[buildTarget]; if (!target) return;