feat(linter): add runtimeHelpers option to @nx/dependency-checks rule (#29954)

This commit is contained in:
Leosvel Pérez Espinosa 2025-02-11 12:30:07 +01:00 committed by GitHub
parent 9672949f82
commit 713b9fbaaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 249 additions and 20 deletions

View File

@ -79,13 +79,14 @@ Sometimes we intentionally want to add or remove a dependency from our `package.
## Options ## Options
| Property | Type | Default | Description | | Property | Type | Default | Description |
| ------------------------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| buildTargets | _Array<string>_ | _["build"]_ | List of build target names | | buildTargets | _Array<string>_ | _["build"]_ | List of build target names |
| checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies | | checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies |
| checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies | | checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies |
| checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version | | checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version |
| ignoredDependencies | _Array<string>_ | _[]_ | List of dependencies to ignore for checks | | ignoredDependencies | _Array<string>_ | _[]_ | List of dependencies to ignore for checks |
| ignoredFiles | _Array<string>_ | N/A | List of files to ignore when collecting dependencies. The default value will be set based on the selected executor during the generation. | | ignoredFiles | _Array<string>_ | 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 | | 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. | | useLocalPathsForWorkspaceDependencies | _boolean_ | _false_ | Set workspace dependencies as relative file:// paths. Useful for monorepos that link via file:// in package.json files. |
| runtimeHelpers | _Array<string>_ | _[]_ | 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. |

View File

@ -79,13 +79,14 @@ Sometimes we intentionally want to add or remove a dependency from our `package.
## Options ## Options
| Property | Type | Default | Description | | Property | Type | Default | Description |
| ------------------------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| buildTargets | _Array<string>_ | _["build"]_ | List of build target names | | buildTargets | _Array<string>_ | _["build"]_ | List of build target names |
| checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies | | checkMissingDependencies | _boolean_ | _true_ | Disable to skip checking for missing dependencies |
| checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies | | checkObsoleteDependencies | _boolean_ | _true_ | Disable to skip checking for unused dependencies |
| checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version | | checkVersionMismatches | _boolean_ | _true_ | Disable to skip checking if version specifier matches installed version |
| ignoredDependencies | _Array<string>_ | _[]_ | List of dependencies to ignore for checks | | ignoredDependencies | _Array<string>_ | _[]_ | List of dependencies to ignore for checks |
| ignoredFiles | _Array<string>_ | N/A | List of files to ignore when collecting dependencies. The default value will be set based on the selected executor during the generation. | | ignoredFiles | _Array<string>_ | 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 | | 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. | | useLocalPathsForWorkspaceDependencies | _boolean_ | _false_ | Set workspace dependencies as relative file:// paths. Useful for monorepos that link via file:// in package.json files. |
| runtimeHelpers | _Array<string>_ | _[]_ | 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. |

View File

@ -1699,6 +1699,113 @@ describe('Dependency checks (eslint)', () => {
`); `);
expect(failures[0].line).toEqual(3); 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 { function createFile(f: string, deps?: FileDataDependency[]): FileData {

View File

@ -27,6 +27,7 @@ export type Options = [
ignoredFiles?: string[]; ignoredFiles?: string[];
includeTransitiveDependencies?: boolean; includeTransitiveDependencies?: boolean;
useLocalPathsForWorkspaceDependencies?: boolean; useLocalPathsForWorkspaceDependencies?: boolean;
runtimeHelpers?: string[];
} }
]; ];
@ -61,6 +62,7 @@ export default ESLintUtils.RuleCreator(
checkVersionMismatches: { type: 'boolean' }, checkVersionMismatches: { type: 'boolean' },
includeTransitiveDependencies: { type: 'boolean' }, includeTransitiveDependencies: { type: 'boolean' },
useLocalPathsForWorkspaceDependencies: { type: 'boolean' }, useLocalPathsForWorkspaceDependencies: { type: 'boolean' },
runtimeHelpers: { type: 'array', items: { type: 'string' } },
}, },
additionalProperties: false, additionalProperties: false,
}, },
@ -82,6 +84,7 @@ export default ESLintUtils.RuleCreator(
ignoredFiles: [], ignoredFiles: [],
includeTransitiveDependencies: false, includeTransitiveDependencies: false,
useLocalPathsForWorkspaceDependencies: false, useLocalPathsForWorkspaceDependencies: false,
runtimeHelpers: [],
}, },
], ],
create( create(
@ -96,6 +99,7 @@ export default ESLintUtils.RuleCreator(
checkVersionMismatches, checkVersionMismatches,
includeTransitiveDependencies, includeTransitiveDependencies,
useLocalPathsForWorkspaceDependencies, useLocalPathsForWorkspaceDependencies,
runtimeHelpers,
}, },
] ]
) { ) {
@ -147,6 +151,7 @@ export default ESLintUtils.RuleCreator(
includeTransitiveDependencies, includeTransitiveDependencies,
ignoredFiles, ignoredFiles,
useLocalPathsForWorkspaceDependencies, useLocalPathsForWorkspaceDependencies,
runtimeHelpers,
} }
); );
const expectedDependencyNames = Object.keys(npmDependencies); const expectedDependencyNames = Object.keys(npmDependencies);

View File

@ -374,6 +374,104 @@ describe('findNpmDependencies', () => {
expect(results).toEqual({}); 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', () => { it('should support recursive collection of dependencies', () => {
vol.fromJSON( vol.fromJSON(
{ {

View File

@ -29,6 +29,7 @@ export function findNpmDependencies(
includeTransitiveDependencies?: boolean; includeTransitiveDependencies?: boolean;
ignoredFiles?: string[]; ignoredFiles?: string[];
useLocalPathsForWorkspaceDependencies?: boolean; useLocalPathsForWorkspaceDependencies?: boolean;
runtimeHelpers?: string[];
} = {} } = {}
): Record<string, string> { ): Record<string, string> {
let seen: null | Set<string> = null; let seen: null | Set<string> = null;
@ -61,6 +62,7 @@ export function findNpmDependencies(
currentProject, currentProject,
projectGraph, projectGraph,
buildTarget, buildTarget,
options.runtimeHelpers,
collectedDeps collectedDeps
); );
@ -194,8 +196,23 @@ function collectHelperDependencies(
sourceProject: ProjectGraphProjectNode, sourceProject: ProjectGraphProjectNode,
projectGraph: ProjectGraph, projectGraph: ProjectGraph,
buildTarget: string, buildTarget: string,
runtimeHelpers: string[] | undefined,
npmDeps: Record<string, string> npmDeps: Record<string, string>
): void { ): 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]; const target = sourceProject.data.targets[buildTarget];
if (!target) return; if (!target) return;