fix(linter): skip verifying deps of deps by default in package.json (#18058)

This commit is contained in:
Jack Hsu 2023-07-12 20:59:36 -04:00 committed by GitHub
parent da2674ded6
commit ae773d547e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 739 additions and 133 deletions

View File

@ -452,6 +452,12 @@ describe('Linter', () => {
];
return json;
});
// Set this to false for now until the `@nx/js:lib` generator is updated to include ts/swc helpers by default.
// TODO(jack): Remove this once the above is addressed in another PR.
updateJson(`libs/${mylib}/tsconfig.lib.json`, (json) => {
json.compilerOptions.importHelpers = false;
return json;
});
updateJson(`libs/${mylib}/project.json`, (json) => {
json.targets.lint.options.lintFilePatterns = [
`libs/${mylib}/**/*.ts`,
@ -465,8 +471,7 @@ describe('Linter', () => {
it('should report dependency check issues', () => {
const rootPackageJson = readJson('package.json');
const nxVersion = rootPackageJson.devDependencies.nx;
const swcCoreVersion = rootPackageJson.devDependencies['@swc/core'];
const swcHelpersVersion = rootPackageJson.dependencies['@swc/helpers'];
const tslibVersion = rootPackageJson.devDependencies['tslib'];
let out = runCLI(`lint ${mylib}`, { silenceError: true });
expect(out).toContain('All files pass linting');
@ -495,9 +500,6 @@ describe('Linter', () => {
{
"dependencies": {
"@nx/devkit": "${nxVersion}",
"@swc/core": "${swcCoreVersion}",
"@swc/helpers": "${swcHelpersVersion}",
"nx": "${nxVersion}",
},
"name": "@proj/${mylib}",
"type": "commonjs",

View File

@ -104,7 +104,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -134,6 +134,74 @@ describe('Dependency checks (eslint)', () => {
expect(failures.length).toEqual(0);
});
it('should exclude files not matching input of the build target', () => {
const packageJson = {
name: '@mycompany/liba',
dependencies: {},
};
const fileSys = {
'./libs/liba/package.json': JSON.stringify(packageJson, null, 2),
'./libs/liba/src/index.ts': '',
'./libs/liba/project.json': JSON.stringify(
{
name: 'liba',
targets: {
build: {
command: 'tsc -p tsconfig.lib.json',
},
},
},
null,
2
),
'./nx.json': JSON.stringify({
targetDefaults: {
build: {
inputs: [
'{projectRoot}/**/*',
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
],
},
},
}),
'./package.json': JSON.stringify(rootPackageJson, null, 2),
};
vol.fromJSON(fileSys, '/root');
const failures = runRule(
{},
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
liba: {
name: 'liba',
type: 'lib',
data: {
root: 'libs/liba',
targets: {
build: {},
},
},
},
},
externalNodes,
dependencies: {
liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }],
},
},
{
liba: [
createFile(`libs/liba/src/main.ts`, []),
createFile(`libs/liba/src/main.spec.ts`, ['npm:external1']),
createFile(`libs/liba/package.json`, []),
],
}
);
expect(failures.length).toEqual(0);
});
it('should report missing dependencies section and fix it', () => {
const packageJson = {
name: '@mycompany/liba',
@ -148,7 +216,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -191,7 +259,6 @@ describe('Dependency checks (eslint)', () => {
"{
"name": "@mycompany/liba",
"dependencies": {
"external1": "~16.1.2"
}
}"
`);
@ -211,7 +278,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -255,7 +322,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -299,7 +366,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -344,7 +411,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -416,7 +483,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -473,7 +540,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -534,7 +601,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ buildTargets: ['notbuild'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -590,7 +657,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ checkMissingDependencies: false },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -646,7 +713,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external2'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -706,7 +773,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -778,7 +845,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -857,7 +924,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -936,7 +1003,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1015,7 +1082,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ checkObsoleteDependencies: false },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1068,7 +1135,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['unneeded'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1119,7 +1186,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1204,7 +1271,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ checkVersionMismatches: false },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1261,7 +1328,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1330,7 +1397,7 @@ describe('Dependency checks (eslint)', () => {
include: ['**/*.ts'],
};
const tsConfiogBaseJson = {
const tsConfigBaseJson = {
compilerOptions: {
target: 'es2015',
importHelpers: true,
@ -1353,15 +1420,15 @@ describe('Dependency checks (eslint)', () => {
const fileSys = {
'./libs/liba/package.json': JSON.stringify(packageJson, null, 2),
'./libs/liba/src/index.ts': '',
'./libs/libb/tsconfig.json': JSON.stringify(tsConfigJson, null, 2),
'./libs/liba/tsconfig.json': JSON.stringify(tsConfigJson, null, 2),
'./package.json': JSON.stringify(rootPackageJson, null, 2),
'./tsconfig.base.json': JSON.stringify(tsConfiogBaseJson, null, 2),
'./tsconfig.base.json': JSON.stringify(tsConfigBaseJson, null, 2),
};
vol.fromJSON(fileSys, '/root');
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1370,21 +1437,11 @@ describe('Dependency checks (eslint)', () => {
type: 'lib',
data: {
root: 'libs/liba',
targets: {
build: {},
},
},
},
libb: {
name: 'libb',
type: 'lib',
data: {
root: 'libs/libb',
targets: {
build: {
executor: '@nx/js:tsc',
options: {
tsConfig: 'libs/libb/tsconfig.json',
tsConfig: 'libs/liba/tsconfig.json',
},
},
},
@ -1393,26 +1450,20 @@ describe('Dependency checks (eslint)', () => {
},
externalNodes,
dependencies: {
liba: [
{ source: 'liba', target: 'npm:external1', type: 'static' },
{ source: 'liba', target: 'libb', type: 'static' },
],
libb: [{ source: 'libb', target: 'npm:external2', type: 'static' }],
liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }],
},
},
{
liba: [
createFile(`libs/liba/src/main.ts`, ['npm:external1']),
createFile(`libs/liba/package.json`, ['npm:external1']),
createFile(`libs/libb/src/main.ts`, ['npm:external2']),
],
}
);
expect(failures.length).toEqual(1);
expect(failures[0].message).toMatchInlineSnapshot(`
"The "liba" uses the following packages, but they are missing from the "dependencies":
- tslib
- external2"
- tslib"
`);
expect(failures[0].line).toEqual(3);
});
@ -1435,14 +1486,14 @@ it('should require swc if @nx/js:swc executor', () => {
const fileSys = {
'./libs/liba/package.json': JSON.stringify(packageJson, null, 2),
'./libs/liba/src/index.ts': '',
'./libs/libb/.swcrc': JSON.stringify(swcrc, null, 2),
'./libs/liba/.swcrc': JSON.stringify(swcrc, null, 2),
'./package.json': JSON.stringify(rootPackageJson, null, 2),
};
vol.fromJSON(fileSys, '/root');
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@ -1451,22 +1502,10 @@ it('should require swc if @nx/js:swc executor', () => {
type: 'lib',
data: {
root: 'libs/liba',
targets: {
build: {},
},
},
},
libb: {
name: 'libb',
type: 'lib',
data: {
root: 'libs/libb',
targets: {
build: {
executor: '@nx/js:swc',
options: {
tsConfig: 'libs/libb/tsconfig.json',
},
options: {},
},
},
},
@ -1474,26 +1513,21 @@ it('should require swc if @nx/js:swc executor', () => {
},
externalNodes,
dependencies: {
liba: [
{ source: 'liba', target: 'npm:external1', type: 'static' },
{ source: 'liba', target: 'libb', type: 'static' },
],
libb: [],
liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }],
},
},
{
liba: [
createFile(`libs/liba/src/main.ts`, ['npm:external1']),
createFile(`libs/liba/package.json`, ['npm:external1']),
createFile(`libs/libb/src/main.ts`),
],
}
);
expect(failures.length).toEqual(1);
expect(failures[0].message).toMatchInlineSnapshot(`
"The "liba" uses the following packages, but they are missing from the "dependencies":
- @swc/helpers"
`);
"The "liba" uses the following packages, but they are missing from the "dependencies":
- @swc/helpers"
`);
expect(failures[0].line).toEqual(3);
});
@ -1518,7 +1552,6 @@ function runRule(
projectGraph: ProjectGraph,
projectFileMap: ProjectFileMap
): Linter.LintMessage[] {
globalThis.projectPath = `${process.cwd()}/proj`;
globalThis.projectGraph = projectGraph;
globalThis.projectFileMap = projectFileMap;
globalThis.projectRootMappings = createProjectRootMappings(

View File

@ -1,18 +1,22 @@
import { join } from 'path';
import { satisfies } from 'semver';
import { AST } from 'jsonc-eslint-parser';
import { normalizePath, workspaceRoot } from '@nx/devkit';
import { type JSONLiteral } from 'jsonc-eslint-parser/lib/parser/ast';
import {
normalizePath,
ProjectGraphProjectNode,
FileData,
workspaceRoot,
} from '@nx/devkit';
import { findNpmDependencies } from '@nx/js/src/utils/find-npm-dependencies';
import { createESLintRule } from '../utils/create-eslint-rule';
import { readProjectGraph } from '../utils/project-graph-utils';
import { findProject, getSourceFilePath } from '../utils/runtime-lint-utils';
import { join } from 'path';
import { findProjectsNpmDependencies } from '@nx/js/src/internal';
import { satisfies } from 'semver';
import { getHelperDependenciesFromProjectGraph } from '@nx/js';
import {
getAllDependencies,
getPackageJson,
removePackageJsonFromFileMap,
} from '../utils/package-json-utils';
import { JSONLiteral } from 'jsonc-eslint-parser/lib/parser/ast';
export type Options = [
{
@ -22,6 +26,7 @@ export type Options = [
checkVersionMismatches?: boolean;
checkMissingPackageJson?: boolean;
ignoredDependencies?: string[];
includeTransitiveDependencies?: boolean;
}
];
@ -51,6 +56,7 @@ export default createESLintRule<Options, MessageIds>({
checkMissingDependencies: { type: 'boolean' },
checkObsoleteDependencies: { type: 'boolean' },
checkVersionMismatches: { type: 'boolean' },
includeTransitiveDependencies: { type: 'boolean' },
},
additionalProperties: false,
},
@ -69,6 +75,7 @@ export default createESLintRule<Options, MessageIds>({
checkObsoleteDependencies: true,
checkVersionMismatches: true,
ignoredDependencies: [],
includeTransitiveDependencies: false,
},
],
create(
@ -80,6 +87,7 @@ export default createESLintRule<Options, MessageIds>({
checkMissingDependencies,
checkObsoleteDependencies,
checkVersionMismatches,
includeTransitiveDependencies,
},
]
) {
@ -92,8 +100,7 @@ export default createESLintRule<Options, MessageIds>({
return {};
}
const projectPath = normalizePath(globalThis.projectPath || workspaceRoot);
const sourceFilePath = getSourceFilePath(fileName, projectPath);
const sourceFilePath = getSourceFilePath(fileName, workspaceRoot);
const { projectGraph, projectRootMappings, projectFileMap } =
readProjectGraph(RULE_NAME);
@ -120,32 +127,19 @@ export default createESLintRule<Options, MessageIds>({
return {};
}
// gather helper dependencies for @nx/js executors
const helperDependencies = getHelperDependenciesFromProjectGraph(
workspaceRoot,
sourceProject.name,
projectGraph
);
const rootPackageJson = getPackageJson(join(workspaceRoot, 'package.json'));
// find all dependencies for the project
const npmDeps = findProjectsNpmDependencies(
const npmDependencies = findNpmDependencies(
workspaceRoot,
sourceProject,
projectGraph,
buildTarget,
rootPackageJson,
projectFileMap,
buildTarget, // TODO: What if child library has a build target different from the parent?
{
helperDependencies: helperDependencies.map((dep) => dep.target),
isProduction: true,
},
removePackageJsonFromFileMap(projectFileMap)
includeTransitiveDependencies,
}
);
const projDependencies = {
...npmDeps.dependencies,
...npmDeps.peerDependencies,
};
const expectedDependencyNames = Object.keys(projDependencies);
const expectedDependencyNames = Object.keys(npmDependencies);
const projPackageJsonPath = join(
workspaceRoot,
@ -180,7 +174,7 @@ export default createESLintRule<Options, MessageIds>({
fix(fixer) {
missingDeps.forEach((d) => {
projPackageJsonDeps[d] =
rootPackageJsonDeps[d] || projDependencies[d];
rootPackageJsonDeps[d] || npmDependencies[d];
});
const deps = (node.value as AST.JSONObjectExpression).properties;
@ -213,8 +207,9 @@ export default createESLintRule<Options, MessageIds>({
return;
}
if (
projDependencies[packageName] === '*' ||
satisfies(projDependencies[packageName], packageRange)
npmDependencies[packageName] === '*' ||
packageRange === '*' ||
satisfies(npmDependencies[packageName], packageRange)
) {
return;
}
@ -224,13 +219,13 @@ export default createESLintRule<Options, MessageIds>({
messageId: 'versionMismatch',
data: {
packageName: packageName,
version: projDependencies[packageName],
version: npmDependencies[packageName],
},
fix: (fixer) =>
fixer.replaceText(
node as any,
`"${packageName}": "${
rootPackageJsonDeps[packageName] || projDependencies[packageName]
rootPackageJsonDeps[packageName] || npmDependencies[packageName]
}"`
),
});
@ -308,15 +303,15 @@ export default createESLintRule<Options, MessageIds>({
.join(),
},
fix: (fixer) => {
expectedDependencyNames.sort().reduce((acc, d) => {
acc[d] = rootPackageJsonDeps[d] || projDependencies[d];
return acc;
}, projPackageJsonDeps);
const dependencies = Object.keys(projPackageJsonDeps)
.map((d) => `\n "${d}": "${projPackageJsonDeps[d]}"`)
.join(',');
expectedDependencyNames.sort().reduce((acc, d) => {
acc[d] = rootPackageJsonDeps[d] || dependencies[d];
return acc;
}, projPackageJsonDeps);
if (!node.properties.length) {
return fixer.replaceText(
node as any,
@ -337,7 +332,7 @@ export default createESLintRule<Options, MessageIds>({
['JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=/^(dev|peer|optional)?dependencies$/i]'](
node: AST.JSONProperty
) {
return validateMissingDependencies(node);
validateMissingDependencies(node);
},
['JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=/^(dev|peer|optional)?dependencies$/i] > JSONObjectExpression > JSONProperty'](
node: AST.JSONProperty
@ -350,19 +345,15 @@ export default createESLintRule<Options, MessageIds>({
}
if (expectedDependencyNames.includes(packageName)) {
return validateVersionMatchesInstalled(
node,
packageName,
packageRange
);
validateVersionMatchesInstalled(node, packageName, packageRange);
} else {
return reportObsoleteDependency(node, packageName);
reportObsoleteDependency(node, packageName);
}
},
['JSONExpressionStatement > JSONObjectExpression'](
node: AST.JSONObjectExpression
) {
return validateDependenciesSectionExistance(node);
validateDependenciesSectionExistance(node);
},
};
},

View File

@ -9,6 +9,7 @@ export function getAllDependencies(
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
...packageJson.optionalDependencies,
};
}
@ -18,15 +19,3 @@ export function getPackageJson(path: string): PackageJson {
}
return {} as PackageJson;
}
export function removePackageJsonFromFileMap(
projectFileMap: ProjectFileMap
): ProjectFileMap {
const newFileMap = {};
Object.keys(projectFileMap).forEach((key) => {
newFileMap[key] = projectFileMap[key].filter(
(f) => !f.file.endsWith('/package.json')
);
});
return newFileMap;
}

View File

@ -0,0 +1,400 @@
import 'nx/src/utils/testing/mock-fs';
import { vol } from 'memfs';
import { findNpmDependencies } from './find-npm-dependencies';
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
workspaceRoot: '/root',
}));
jest.mock('nx/src/utils/workspace-root', () => ({
workspaceRoot: '/root',
}));
describe('findNpmDependencies', () => {
const nxJson = {
targetDefaults: {
build: {
inputs: [
'{projectRoot}/**/*',
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
],
},
},
};
afterEach(() => {
vol.reset();
});
it('should pick up external npm dependencies and their versions', () => {
vol.fromJSON(
{
'./nx.json': JSON.stringify(nxJson),
},
'/root'
);
const libWithExternalDeps = {
name: 'my-lib',
type: 'lib' as const,
data: {
root: 'libs/my-lib',
targets: { build: {} },
},
};
const projectGraph = {
nodes: {
'my-lib': libWithExternalDeps,
},
externalNodes: {
'npm:foo': {
name: 'npm:foo' as const,
type: 'npm' as const,
data: {
packageName: 'foo',
version: '1.0.0',
},
},
},
dependencies: {},
};
const projectFileMap = {
'my-lib': [
{
file: 'libs/my-lib/index.ts',
hash: '123',
deps: ['npm:foo'],
},
],
};
const results = findNpmDependencies(
'/root',
libWithExternalDeps,
projectGraph,
projectFileMap,
'build'
);
expect(results).toEqual({
foo: '1.0.0',
});
});
it('should pick up helper npm dependencies if required', () => {
vol.fromJSON(
{
'./nx.json': JSON.stringify(nxJson),
'./libs/my-lib/tsconfig.json': JSON.stringify({
compilerOptions: {
importHelpers: true,
},
}),
'./libs/my-lib/.swcrc': JSON.stringify({
jsc: {
externalHelpers: true,
},
}),
},
'/root'
);
const libWithHelpers = {
name: 'my-lib',
type: 'lib' as const,
data: {
root: 'libs/my-lib',
targets: {
build1: {
executor: '@nx/js:tsc',
options: {
tsConfig: 'libs/my-lib/tsconfig.json',
},
},
build2: {
executor: '@nx/js:swc',
options: {},
},
},
},
};
const projectGraph = {
nodes: {
'my-lib': libWithHelpers,
},
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': [],
};
expect(
findNpmDependencies(
'/root',
libWithHelpers,
projectGraph,
projectFileMap,
'build1'
)
).toEqual({
tslib: '2.6.0',
});
expect(
findNpmDependencies(
'/root',
libWithHelpers,
projectGraph,
projectFileMap,
'build2'
)
).toEqual({
'@swc/helpers': '0.5.0',
});
});
it('should not pick up helper npm dependencies if not required', () => {
vol.fromJSON(
{
'./libs/my-lib/tsconfig.json': JSON.stringify({
compilerOptions: {
importHelpers: false,
},
}),
'./libs/my-lib/.swcrc': JSON.stringify({
jsc: {
externalHelpers: false,
},
}),
},
'/root'
);
const libWithInlinedHelpers = {
name: 'my-lib',
type: 'lib' as const,
data: {
root: 'libs/my-lib',
targets: {
build1: {
executor: '@nx/js:tsc',
options: {
tsConfig: 'libs/my-lib/tsconfig.json',
},
},
build2: {
executor: '@nx/js:swc',
options: {},
},
},
},
};
const projectGraph = {
nodes: {
'my-lib': libWithInlinedHelpers,
},
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',
libWithInlinedHelpers,
projectGraph,
projectFileMap,
'build'
);
expect(results).toEqual({});
});
it('should support recursive collection of dependencies', () => {
vol.fromJSON(
{
'./nx.json': JSON.stringify(nxJson),
},
'/root'
);
const parentLib = {
name: 'parent',
type: 'lib' as const,
data: {
root: 'libs/parent',
targets: { build: {} },
},
};
const projectGraph = {
nodes: {
parent: parentLib,
child1: {
name: 'child1',
type: 'lib' as const,
data: {
root: 'libs/child1',
targets: { build: {} },
},
},
child2: {
name: 'child2',
type: 'lib' as const,
data: {
root: 'libs/child2',
targets: { build: {} },
},
},
},
externalNodes: {
'npm:foo': {
name: 'npm:foo' as const,
type: 'npm' as const,
data: {
packageName: 'foo',
version: '1.0.0',
},
},
},
dependencies: {
parent: [
{
type: 'static',
source: 'parent',
target: 'child1',
},
],
child1: [
{
type: 'static',
source: 'child1',
target: 'child2',
},
],
child2: [
{
type: 'static',
source: 'child2',
target: 'npm:foo',
},
],
},
};
const projectFileMap = {
parent: [{ file: 'libs/parent/index.ts', hash: '123', deps: ['child1'] }],
child1: [{ file: 'libs/child1/index.ts', hash: '123', deps: ['child2'] }],
child2: [
{ file: 'libs/child2/index.ts', hash: '123', deps: ['npm:foo'] },
],
};
const results = findNpmDependencies(
'/root',
parentLib,
projectGraph,
projectFileMap,
'build',
{
includeTransitiveDependencies: true,
}
);
expect(results).toEqual({
foo: '1.0.0',
});
});
it('should find workspace dependencies', () => {
vol.fromJSON(
{
'./libs/lib3/package.json': JSON.stringify({
name: '@acme/lib3',
version: '0.0.1',
}),
'./nx.json': JSON.stringify(nxJson),
},
'/root'
);
const lib1 = {
name: 'lib1',
type: 'lib' as const,
data: {
root: 'libs/lib1',
targets: { build: {} },
},
};
const lib2 = {
name: 'lib2',
type: 'lib' as const,
data: {
root: 'libs/lib2',
targets: { build: {} },
},
};
const lib3 = {
name: 'lib3',
type: 'lib' as const,
data: {
root: 'libs/lib3',
targets: { build: {} },
},
};
const projectGraph = {
nodes: {
lib1: lib1,
lib2: lib2,
lib3: lib3,
},
externalNodes: {},
dependencies: {},
};
const projectFileMap = {
lib1: [{ file: 'libs/lib1/index.ts', hash: '123', deps: ['lib3'] }],
lib2: [{ file: 'libs/lib1/index.ts', hash: '123', deps: ['lib3'] }],
lib3: [],
};
expect(
findNpmDependencies('/root', lib1, projectGraph, projectFileMap, 'build')
).toEqual({
'@acme/lib3': '*',
});
expect(
findNpmDependencies('/root', lib2, projectGraph, projectFileMap, 'build')
).toEqual({
'@acme/lib3': '*',
});
});
});

View File

@ -0,0 +1,191 @@
import { join } from 'path';
import { readNxJson } from 'nx/src/project-graph/file-utils';
import {
getTargetInputs,
filterUsingGlobPatterns,
} from 'nx/src/hasher/task-hasher';
import {
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';
import { readTsConfig } from './typescript/ts-config';
/**
* Finds all npm dependencies and their expected versions for a given project.
*/
export function findNpmDependencies(
workspaceRoot: string,
sourceProject: ProjectGraphProjectNode,
projectGraph: ProjectGraph,
projectFileMap: ProjectFileMap,
buildTarget: string,
options: {
includeTransitiveDependencies?: boolean;
} = {}
): Record<string, string> {
let seen: null | Set<string> = null;
if (options.includeTransitiveDependencies) {
seen = new Set<string>();
}
const results: Record<string, string> = {};
function collectAll(
currentProject: ProjectGraphProjectNode,
collectedDeps: Record<string, string>
): void {
if (seen?.has(currentProject.name)) return;
collectDependenciesFromFileMap(
workspaceRoot,
currentProject,
projectGraph,
projectFileMap,
buildTarget,
collectedDeps
);
collectHelperDependencies(
workspaceRoot,
currentProject,
projectGraph,
buildTarget,
collectedDeps
);
if (options.includeTransitiveDependencies) {
const projectDeps = projectGraph.dependencies[currentProject.name];
for (const dep of projectDeps) {
const projectDep = projectGraph.nodes[dep.target];
if (projectDep) collectAll(projectDep, collectedDeps);
}
}
}
collectAll(sourceProject, results);
return results;
}
// Keep track of workspace libs we already read package.json for so we don't read from disk again.
const seenWorkspaceDeps: Record<string, { name: string; version: string }> = {};
function collectDependenciesFromFileMap(
workspaceRoot: string,
sourceProject: ProjectGraphProjectNode,
projectGraph: ProjectGraph,
projectFileMap: ProjectFileMap,
buildTarget: string,
npmDeps: Record<string, string>
): void {
const rawFiles = projectFileMap[sourceProject.name];
if (!rawFiles) return;
// Cannot read inputs if the target does not exist on the project.
if (!sourceProject.data.targets[buildTarget]) return;
const inputs = getTargetInputs(
readNxJson(),
sourceProject,
buildTarget
).selfInputs;
const files = filterUsingGlobPatterns(
sourceProject.data.root,
projectFileMap[sourceProject.name] || [],
inputs
);
for (const fileData of files) {
if (
!fileData.deps ||
fileData.file ===
joinPathFragments(sourceProject.data.root, 'package.json')
) {
continue;
}
for (const dep of fileData.deps) {
const target = fileDataDepTarget(dep);
// If the node is external, then read package info from `data`.
const externalDep = projectGraph.externalNodes[target];
if (externalDep?.type === 'npm') {
npmDeps[externalDep.data.packageName] = externalDep.data.version;
continue;
}
// If node is internal, then try reading package info from `package.json` (which must exist for this to work).
const workspaceDep = projectGraph.nodes[target];
if (!workspaceDep) continue;
const cached = seenWorkspaceDeps[workspaceDep.name];
if (cached) {
npmDeps[cached.name] = cached.version;
} else {
const packageJson = readPackageJson(workspaceDep, workspaceRoot);
if (packageJson) {
// 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] = '*';
seenWorkspaceDeps[workspaceDep.name] = {
name: packageJson.name,
version: '*',
};
}
}
}
}
}
function readPackageJson(
project: ProjectGraphProjectNode,
workspaceRoot: string
): null | {
name: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
} {
const packageJsonPath = join(
workspaceRoot,
project.data.root,
'package.json'
);
if (fileExists(packageJsonPath)) return readJsonFile(packageJsonPath);
return null;
}
function collectHelperDependencies(
workspaceRoot: string,
sourceProject: ProjectGraphProjectNode,
projectGraph: ProjectGraph,
buildTarget: string,
npmDeps: Record<string, string>
): void {
const target = sourceProject.data.targets[buildTarget];
if (!target) return;
if (target.executor === '@nx/js:tsc' && target.options?.tsConfig) {
const tsConfig = readTsConfig(join(workspaceRoot, target.options.tsConfig));
if (tsConfig?.options['importHelpers']) {
npmDeps['tslib'] = projectGraph.externalNodes['npm:tslib']?.data.version;
}
}
if (target.executor === '@nx/js:swc') {
const swcConfigPath = target.options.swcrc
? join(workspaceRoot, target.options.swcrc)
: join(workspaceRoot, sourceProject.data.root, '.swcrc');
const swcConfig = fileExists(swcConfigPath)
? readJsonFile(swcConfigPath)
: {};
if (swcConfig?.jsc?.externalHelpers) {
npmDeps['@swc/helpers'] =
projectGraph.externalNodes['npm:@swc/helpers']?.data.version;
}
}
}