@webpieces/nx-webpieces-rules 0.4.465 ā 0.4.466
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/executors/validate-packagejson/executor.d.ts +7 -0
- package/src/executors/validate-packagejson/executor.js +7 -3
- package/src/executors/validate-packagejson/executor.js.map +1 -1
- package/src/executors/validate-packagejson/schema.json +9 -2
- package/src/lib/dep-usage-scanner.d.ts +54 -0
- package/src/lib/dep-usage-scanner.js +199 -0
- package/src/lib/dep-usage-scanner.js.map +1 -0
- package/src/lib/package-validator.d.ts +157 -21
- package/src/lib/package-validator.js +377 -153
- package/src/lib/package-validator.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.466",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
25
|
-
"@webpieces/code-rules": "0.4.
|
|
26
|
-
"@webpieces/eslint-rules": "0.4.
|
|
27
|
-
"@webpieces/pr-gate": "0.4.
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.4.466",
|
|
25
|
+
"@webpieces/code-rules": "0.4.466",
|
|
26
|
+
"@webpieces/eslint-rules": "0.4.466",
|
|
27
|
+
"@webpieces/pr-gate": "0.4.466",
|
|
28
|
+
"@webpieces/rules-config": "0.4.466",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* nx run architecture:validate-packagejson
|
|
9
9
|
*/
|
|
10
10
|
import type { ExecutorContext } from '@nx/devkit';
|
|
11
|
+
import { TestOnlyDepMode } from '../../lib/package-validator';
|
|
11
12
|
export interface ValidatePackageJsonOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Strictness for "a test-only package is listed in dependencies" (i.e. it lands in
|
|
15
|
+
* the `pnpm deploy --prod` closure and ships to production).
|
|
16
|
+
* 'error' (default) | 'warn' (migration) | 'off'.
|
|
17
|
+
*/
|
|
18
|
+
testOnlyDepMode?: TestOnlyDepMode;
|
|
12
19
|
}
|
|
13
20
|
export interface ExecutorResult {
|
|
14
21
|
success: boolean;
|
|
@@ -27,7 +27,8 @@ async function runExecutor(options, context) {
|
|
|
27
27
|
const enhancedGraph = (0, graph_sorter_1.sortGraphTopologically)(reducedGraph);
|
|
28
28
|
// Step 3: Validate package.json dependencies match
|
|
29
29
|
console.log('š¦ Validating package.json dependencies match the architecture graph...');
|
|
30
|
-
const
|
|
30
|
+
const validatorOptions = new package_validator_1.PackageValidatorOptions(options.testOnlyDepMode ?? 'error');
|
|
31
|
+
const packageValidation = await (0, package_validator_1.validatePackageJsonDependencies)(enhancedGraph, workspaceRoot, validatorOptions);
|
|
31
32
|
// Warnings never fail the build (e.g. runtime-only / peer deps).
|
|
32
33
|
if (packageValidation.warnings.length > 0) {
|
|
33
34
|
console.warn('\nā ļø Package.json notices (non-fatal):');
|
|
@@ -42,8 +43,11 @@ async function runExecutor(options, context) {
|
|
|
42
43
|
console.error(` ${error}`);
|
|
43
44
|
}
|
|
44
45
|
console.error('\nTo fix:');
|
|
45
|
-
console.error(' 1. Review the
|
|
46
|
-
console.error(' 2.
|
|
46
|
+
console.error(' 1. Review each error above ā it names the SECTION the dependency belongs in');
|
|
47
|
+
console.error(' 2. Imported by production source ā package.json "dependencies"');
|
|
48
|
+
console.error(' 3. Imported only by *.spec.ts / __tests__ / test configs ā "devDependencies"');
|
|
49
|
+
console.error(' (devDependencies are excluded from `pnpm deploy --prod`, so test-only');
|
|
50
|
+
console.error(' packages never reach the production image)');
|
|
47
51
|
return { success: false };
|
|
48
52
|
}
|
|
49
53
|
console.log('ā
Package.json dependencies cover the architecture graph');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-packagejson/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-packagejson/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Package.json Executor\n *\n * Validates that package.json dependencies match project.json build dependencies.\n * This ensures the two sources of truth don't drift apart.\n *\n * Usage:\n * nx run architecture:validate-packagejson\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport {\n PackageValidatorOptions,\n TestOnlyDepMode,\n validatePackageJsonDependencies,\n} from '../../lib/package-validator';\nimport { toError } from '../../toError';\n\nexport interface ValidatePackageJsonOptions {\n /**\n * Strictness for \"a test-only package is listed in dependencies\" (i.e. it lands in\n * the `pnpm deploy --prod` closure and ships to production).\n * 'error' (default) | 'warn' (migration) | 'off'.\n */\n testOnlyDepMode?: TestOnlyDepMode;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nexport default async function runExecutor(\n options: ValidatePackageJsonOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n\n console.log('\\nš¦ Validating Package.json Dependencies\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Build the full graph from nx, then transitively reduce it (the view)\n console.log(\"š Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 2: Topological sort (to get enhanced graph with levels)\n console.log('š Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n\n // Step 3: Validate package.json dependencies match\n console.log('š¦ Validating package.json dependencies match the architecture graph...');\n const validatorOptions = new PackageValidatorOptions(options.testOnlyDepMode ?? 'error');\n const packageValidation = await validatePackageJsonDependencies(\n enhancedGraph,\n workspaceRoot,\n validatorOptions\n );\n\n // Warnings never fail the build (e.g. runtime-only / peer deps).\n if (packageValidation.warnings.length > 0) {\n console.warn('\\nā ļø Package.json notices (non-fatal):');\n for (const warning of packageValidation.warnings) {\n console.warn(` ${warning}`);\n }\n }\n\n if (!packageValidation.valid) {\n console.error('\\nā Package.json validation failed!');\n console.error('\\nErrors:');\n for (const error of packageValidation.errors) {\n console.error(` ${error}`);\n }\n console.error('\\nTo fix:');\n console.error(' 1. Review each error above ā it names the SECTION the dependency belongs in');\n console.error(' 2. Imported by production source ā package.json \"dependencies\"');\n console.error(' 3. Imported only by *.spec.ts / __tests__ / test configs ā \"devDependencies\"');\n console.error(' (devDependencies are excluded from `pnpm deploy --prod`, so test-only');\n console.error(' packages never reach the production image)');\n return { success: false };\n }\n\n console.log('ā
Package.json dependencies cover the architecture graph');\n\n // Print summary\n const validProjects = packageValidation.projectResults.filter(r => r.valid).length;\n const totalProjects = packageValidation.projectResults.length;\n console.log(`\\nš Validation Summary:`);\n console.log(` Projects validated: ${totalProjects}`);\n console.log(` Valid: ${validProjects}`);\n console.log(` Invalid: ${totalProjects - validProjects}`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('ā Package.json validation failed:', error.message);\n return { success: false };\n }\n}\n"]}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/schema",
|
|
3
3
|
"title": "Validate Package.json Executor",
|
|
4
|
-
"description": "Validates that package.json dependencies match project.json build dependencies",
|
|
4
|
+
"description": "Validates that package.json dependencies match project.json build dependencies, and that each dependency sits in the correct section (dependencies vs devDependencies)",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"properties": {
|
|
6
|
+
"properties": {
|
|
7
|
+
"testOnlyDepMode": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"enum": ["error", "warn", "off"],
|
|
10
|
+
"default": "error",
|
|
11
|
+
"description": "What to do when a package imported ONLY by test/dev files is listed in \"dependencies\" (so `pnpm deploy --prod` ships it to production). 'error' fails the build (default), 'warn' reports without failing (migration path for existing repos), 'off' skips the check."
|
|
12
|
+
}
|
|
13
|
+
},
|
|
7
14
|
"required": []
|
|
8
15
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dep Usage Scanner
|
|
3
|
+
*
|
|
4
|
+
* Answers ONE question for a project: "is package X reached from production
|
|
5
|
+
* source, or ONLY from test/dev files?"
|
|
6
|
+
*
|
|
7
|
+
* WHY this exists: nx derives graph edges from ALL TypeScript sources, specs
|
|
8
|
+
* included. Without this scan, a package imported only by `*.spec.ts` looks
|
|
9
|
+
* identical to a package imported by a controller, so the validator forces it
|
|
10
|
+
* into `dependencies` ā and `pnpm deploy --prod` then ships test machinery
|
|
11
|
+
* (auth-bypass hooks, canned credentials, fakes) into the production image.
|
|
12
|
+
* Splitting the scan by file kind lets `devDependencies` be the REQUIRED home
|
|
13
|
+
* for test-only packages.
|
|
14
|
+
*
|
|
15
|
+
* The scan is deliberately conservative: a package is "test-only" ONLY when it
|
|
16
|
+
* is imported by at least one test/dev file and by ZERO production files. Any
|
|
17
|
+
* doubt (no import found at all, e.g. a runtime-only/reflection dependency)
|
|
18
|
+
* resolves to "production", so this can never push a runtime-required package
|
|
19
|
+
* out of `dependencies`.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Which packages a project reaches from production code vs. only from test/dev code.
|
|
23
|
+
* Data-only: no logic lives here (see CLAUDE.md ā data structures are classes).
|
|
24
|
+
*/
|
|
25
|
+
export declare class DepUsage {
|
|
26
|
+
prodPackages: Set<string>;
|
|
27
|
+
testPackages: Set<string>;
|
|
28
|
+
constructor(prodPackages: Set<string>, testPackages: Set<string>);
|
|
29
|
+
/** True when the package is imported by tests and by no production file. */
|
|
30
|
+
isTestOnly(packageName: string): boolean;
|
|
31
|
+
/** True when we saw the package in NO file at all (kind is unknown ā treat as prod). */
|
|
32
|
+
isUnseen(packageName: string): boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class DepUsageScanner {
|
|
35
|
+
/**
|
|
36
|
+
* Walk a project directory and record every bare import specifier, bucketed
|
|
37
|
+
* by whether the importing file is production or test/dev.
|
|
38
|
+
*/
|
|
39
|
+
scan(absProjectDir: string): DepUsage;
|
|
40
|
+
/**
|
|
41
|
+
* Is this path a test/dev file? Path is relative to the project root and
|
|
42
|
+
* uses either separator.
|
|
43
|
+
*/
|
|
44
|
+
isDevFile(relPath: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The package a bare specifier belongs to, or null for relative/absolute
|
|
47
|
+
* paths and node: builtins. `@scope/pkg/sub` ā `@scope/pkg`; `pkg/sub` ā `pkg`.
|
|
48
|
+
*/
|
|
49
|
+
toPackageName(specifier: string): string | null;
|
|
50
|
+
private walk;
|
|
51
|
+
private scanFile;
|
|
52
|
+
private extractPackageNames;
|
|
53
|
+
private readFile;
|
|
54
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dep Usage Scanner
|
|
4
|
+
*
|
|
5
|
+
* Answers ONE question for a project: "is package X reached from production
|
|
6
|
+
* source, or ONLY from test/dev files?"
|
|
7
|
+
*
|
|
8
|
+
* WHY this exists: nx derives graph edges from ALL TypeScript sources, specs
|
|
9
|
+
* included. Without this scan, a package imported only by `*.spec.ts` looks
|
|
10
|
+
* identical to a package imported by a controller, so the validator forces it
|
|
11
|
+
* into `dependencies` ā and `pnpm deploy --prod` then ships test machinery
|
|
12
|
+
* (auth-bypass hooks, canned credentials, fakes) into the production image.
|
|
13
|
+
* Splitting the scan by file kind lets `devDependencies` be the REQUIRED home
|
|
14
|
+
* for test-only packages.
|
|
15
|
+
*
|
|
16
|
+
* The scan is deliberately conservative: a package is "test-only" ONLY when it
|
|
17
|
+
* is imported by at least one test/dev file and by ZERO production files. Any
|
|
18
|
+
* doubt (no import found at all, e.g. a runtime-only/reflection dependency)
|
|
19
|
+
* resolves to "production", so this can never push a runtime-required package
|
|
20
|
+
* out of `dependencies`.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.DepUsageScanner = exports.DepUsage = void 0;
|
|
24
|
+
const tslib_1 = require("tslib");
|
|
25
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
26
|
+
const path = tslib_1.__importStar(require("path"));
|
|
27
|
+
const toError_1 = require("../toError");
|
|
28
|
+
/** Directories never worth scanning (build output, vendored code, VCS). */
|
|
29
|
+
const SKIP_DIRS = new Set([
|
|
30
|
+
'node_modules',
|
|
31
|
+
'dist',
|
|
32
|
+
'build',
|
|
33
|
+
'out-tsc',
|
|
34
|
+
'coverage',
|
|
35
|
+
'.nx',
|
|
36
|
+
'.git',
|
|
37
|
+
'tmp',
|
|
38
|
+
'.angular',
|
|
39
|
+
]);
|
|
40
|
+
/** Source extensions whose imports we understand. */
|
|
41
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
42
|
+
'.ts',
|
|
43
|
+
'.tsx',
|
|
44
|
+
'.mts',
|
|
45
|
+
'.cts',
|
|
46
|
+
'.js',
|
|
47
|
+
'.jsx',
|
|
48
|
+
'.mjs',
|
|
49
|
+
'.cjs',
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Directory names that make everything below them test/dev-only.
|
|
53
|
+
* Kept tight on purpose ā a false "this is a test dir" would let a real
|
|
54
|
+
* production import be classified as test-only.
|
|
55
|
+
*/
|
|
56
|
+
const TEST_DIR_NAMES = new Set([
|
|
57
|
+
'__tests__',
|
|
58
|
+
'__mocks__',
|
|
59
|
+
'__fixtures__',
|
|
60
|
+
'test',
|
|
61
|
+
'tests',
|
|
62
|
+
'e2e',
|
|
63
|
+
'e2e-tests',
|
|
64
|
+
]);
|
|
65
|
+
/** `foo.spec.ts`, `foo.test.tsx`, `foo-e2e.spec.mts`, `foo.testkit.ts`, ... */
|
|
66
|
+
const TEST_FILE_RE = /[.-](spec|test|e2e|testkit|mock|mocks|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
67
|
+
/** Tooling config/bootstrap files: dev-time by definition. */
|
|
68
|
+
const DEV_CONFIG_RE = /^(vitest|vite|jest|playwright|cypress|karma|webpack|rollup|eslint|prettier)\.[\w.-]*config\.[cm]?[jt]s$/;
|
|
69
|
+
/** `jest.setup.ts`, `vitest.setup.ts`, `test-setup.ts`, ... */
|
|
70
|
+
const DEV_SETUP_RE = /^([\w-]*[.-])?(setup|test-setup)\.[cm]?[jt]s$/;
|
|
71
|
+
/** Bare-import extraction: `from 'x'`, `import 'x'`, `import('x')`, `require('x')`. */
|
|
72
|
+
const IMPORT_RE = /(?:\bfrom\s*|\bimport\s*|\brequire\s*\(\s*|\bimport\s*\(\s*)['"]([^'"]+)['"]/g;
|
|
73
|
+
/**
|
|
74
|
+
* Which packages a project reaches from production code vs. only from test/dev code.
|
|
75
|
+
* Data-only: no logic lives here (see CLAUDE.md ā data structures are classes).
|
|
76
|
+
*/
|
|
77
|
+
class DepUsage {
|
|
78
|
+
prodPackages;
|
|
79
|
+
testPackages;
|
|
80
|
+
constructor(prodPackages, testPackages) {
|
|
81
|
+
this.prodPackages = prodPackages;
|
|
82
|
+
this.testPackages = testPackages;
|
|
83
|
+
}
|
|
84
|
+
/** True when the package is imported by tests and by no production file. */
|
|
85
|
+
isTestOnly(packageName) {
|
|
86
|
+
return this.testPackages.has(packageName) && !this.prodPackages.has(packageName);
|
|
87
|
+
}
|
|
88
|
+
/** True when we saw the package in NO file at all (kind is unknown ā treat as prod). */
|
|
89
|
+
isUnseen(packageName) {
|
|
90
|
+
return !this.testPackages.has(packageName) && !this.prodPackages.has(packageName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.DepUsage = DepUsage;
|
|
94
|
+
class DepUsageScanner {
|
|
95
|
+
/**
|
|
96
|
+
* Walk a project directory and record every bare import specifier, bucketed
|
|
97
|
+
* by whether the importing file is production or test/dev.
|
|
98
|
+
*/
|
|
99
|
+
scan(absProjectDir) {
|
|
100
|
+
const usage = new DepUsage(new Set(), new Set());
|
|
101
|
+
if (!fs.existsSync(absProjectDir))
|
|
102
|
+
return usage;
|
|
103
|
+
this.walk(absProjectDir, absProjectDir, usage);
|
|
104
|
+
return usage;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Is this path a test/dev file? Path is relative to the project root and
|
|
108
|
+
* uses either separator.
|
|
109
|
+
*/
|
|
110
|
+
isDevFile(relPath) {
|
|
111
|
+
const normalized = relPath.split(path.sep).join('/');
|
|
112
|
+
const segments = normalized.split('/');
|
|
113
|
+
const fileName = segments[segments.length - 1];
|
|
114
|
+
for (const dir of segments.slice(0, -1)) {
|
|
115
|
+
if (TEST_DIR_NAMES.has(dir))
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (TEST_FILE_RE.test(fileName))
|
|
119
|
+
return true;
|
|
120
|
+
if (DEV_CONFIG_RE.test(fileName))
|
|
121
|
+
return true;
|
|
122
|
+
if (DEV_SETUP_RE.test(fileName))
|
|
123
|
+
return true;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The package a bare specifier belongs to, or null for relative/absolute
|
|
128
|
+
* paths and node: builtins. `@scope/pkg/sub` ā `@scope/pkg`; `pkg/sub` ā `pkg`.
|
|
129
|
+
*/
|
|
130
|
+
toPackageName(specifier) {
|
|
131
|
+
if (specifier.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
if (specifier.startsWith('.') || specifier.startsWith('/'))
|
|
134
|
+
return null;
|
|
135
|
+
if (specifier.startsWith('node:'))
|
|
136
|
+
return null;
|
|
137
|
+
const parts = specifier.split('/');
|
|
138
|
+
if (specifier.startsWith('@')) {
|
|
139
|
+
if (parts.length < 2)
|
|
140
|
+
return null;
|
|
141
|
+
return `${parts[0]}/${parts[1]}`;
|
|
142
|
+
}
|
|
143
|
+
return parts[0];
|
|
144
|
+
}
|
|
145
|
+
walk(absDir, projectRoot, usage) {
|
|
146
|
+
const entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const absPath = path.join(absDir, entry.name);
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
if (SKIP_DIRS.has(entry.name))
|
|
151
|
+
continue;
|
|
152
|
+
this.walk(absPath, projectRoot, usage);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!entry.isFile())
|
|
156
|
+
continue;
|
|
157
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(entry.name)))
|
|
158
|
+
continue;
|
|
159
|
+
this.scanFile(absPath, projectRoot, usage);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
scanFile(absPath, projectRoot, usage) {
|
|
163
|
+
const source = this.readFile(absPath);
|
|
164
|
+
if (source === null)
|
|
165
|
+
return;
|
|
166
|
+
const relPath = path.relative(projectRoot, absPath);
|
|
167
|
+
const bucket = this.isDevFile(relPath) ? usage.testPackages : usage.prodPackages;
|
|
168
|
+
for (const packageName of this.extractPackageNames(source)) {
|
|
169
|
+
bucket.add(packageName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
extractPackageNames(source) {
|
|
173
|
+
const names = [];
|
|
174
|
+
IMPORT_RE.lastIndex = 0;
|
|
175
|
+
let match = IMPORT_RE.exec(source);
|
|
176
|
+
while (match !== null) {
|
|
177
|
+
const packageName = this.toPackageName(match[1]);
|
|
178
|
+
if (packageName !== null)
|
|
179
|
+
names.push(packageName);
|
|
180
|
+
match = IMPORT_RE.exec(source);
|
|
181
|
+
}
|
|
182
|
+
return names;
|
|
183
|
+
}
|
|
184
|
+
readFile(absPath) {
|
|
185
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
186
|
+
try {
|
|
187
|
+
return fs.readFileSync(absPath, 'utf-8');
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
const error = (0, toError_1.toError)(err);
|
|
191
|
+
// An unreadable file cannot change the classification of a package; skipping it
|
|
192
|
+
// only ever makes the scan MORE conservative (fewer test-only classifications).
|
|
193
|
+
console.warn(`Could not read ${absPath} while classifying deps: ${error.message}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.DepUsageScanner = DepUsageScanner;
|
|
199
|
+
//# sourceMappingURL=dep-usage-scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dep-usage-scanner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dep-usage-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dep Usage Scanner\n *\n * Answers ONE question for a project: \"is package X reached from production\n * source, or ONLY from test/dev files?\"\n *\n * WHY this exists: nx derives graph edges from ALL TypeScript sources, specs\n * included. Without this scan, a package imported only by `*.spec.ts` looks\n * identical to a package imported by a controller, so the validator forces it\n * into `dependencies` ā and `pnpm deploy --prod` then ships test machinery\n * (auth-bypass hooks, canned credentials, fakes) into the production image.\n * Splitting the scan by file kind lets `devDependencies` be the REQUIRED home\n * for test-only packages.\n *\n * The scan is deliberately conservative: a package is \"test-only\" ONLY when it\n * is imported by at least one test/dev file and by ZERO production files. Any\n * doubt (no import found at all, e.g. a runtime-only/reflection dependency)\n * resolves to \"production\", so this can never push a runtime-required package\n * out of `dependencies`.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../toError';\n\n/** Directories never worth scanning (build output, vendored code, VCS). */\nconst SKIP_DIRS = new Set([\n 'node_modules',\n 'dist',\n 'build',\n 'out-tsc',\n 'coverage',\n '.nx',\n '.git',\n 'tmp',\n '.angular',\n]);\n\n/** Source extensions whose imports we understand. */\nconst SOURCE_EXTENSIONS = new Set([\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n]);\n\n/**\n * Directory names that make everything below them test/dev-only.\n * Kept tight on purpose ā a false \"this is a test dir\" would let a real\n * production import be classified as test-only.\n */\nconst TEST_DIR_NAMES = new Set([\n '__tests__',\n '__mocks__',\n '__fixtures__',\n 'test',\n 'tests',\n 'e2e',\n 'e2e-tests',\n]);\n\n/** `foo.spec.ts`, `foo.test.tsx`, `foo-e2e.spec.mts`, `foo.testkit.ts`, ... */\nconst TEST_FILE_RE = /[.-](spec|test|e2e|testkit|mock|mocks|fixture|fixtures)\\.[cm]?[jt]sx?$/;\n\n/** Tooling config/bootstrap files: dev-time by definition. */\nconst DEV_CONFIG_RE =\n /^(vitest|vite|jest|playwright|cypress|karma|webpack|rollup|eslint|prettier)\\.[\\w.-]*config\\.[cm]?[jt]s$/;\n\n/** `jest.setup.ts`, `vitest.setup.ts`, `test-setup.ts`, ... */\nconst DEV_SETUP_RE = /^([\\w-]*[.-])?(setup|test-setup)\\.[cm]?[jt]s$/;\n\n/** Bare-import extraction: `from 'x'`, `import 'x'`, `import('x')`, `require('x')`. */\nconst IMPORT_RE =\n /(?:\\bfrom\\s*|\\bimport\\s*|\\brequire\\s*\\(\\s*|\\bimport\\s*\\(\\s*)['\"]([^'\"]+)['\"]/g;\n\n/**\n * Which packages a project reaches from production code vs. only from test/dev code.\n * Data-only: no logic lives here (see CLAUDE.md ā data structures are classes).\n */\nexport class DepUsage {\n prodPackages: Set<string>;\n testPackages: Set<string>;\n\n constructor(prodPackages: Set<string>, testPackages: Set<string>) {\n this.prodPackages = prodPackages;\n this.testPackages = testPackages;\n }\n\n /** True when the package is imported by tests and by no production file. */\n isTestOnly(packageName: string): boolean {\n return this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n\n /** True when we saw the package in NO file at all (kind is unknown ā treat as prod). */\n isUnseen(packageName: string): boolean {\n return !this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n}\n\nexport class DepUsageScanner {\n /**\n * Walk a project directory and record every bare import specifier, bucketed\n * by whether the importing file is production or test/dev.\n */\n scan(absProjectDir: string): DepUsage {\n const usage = new DepUsage(new Set<string>(), new Set<string>());\n if (!fs.existsSync(absProjectDir)) return usage;\n this.walk(absProjectDir, absProjectDir, usage);\n return usage;\n }\n\n /**\n * Is this path a test/dev file? Path is relative to the project root and\n * uses either separator.\n */\n isDevFile(relPath: string): boolean {\n const normalized = relPath.split(path.sep).join('/');\n const segments = normalized.split('/');\n const fileName = segments[segments.length - 1];\n\n for (const dir of segments.slice(0, -1)) {\n if (TEST_DIR_NAMES.has(dir)) return true;\n }\n if (TEST_FILE_RE.test(fileName)) return true;\n if (DEV_CONFIG_RE.test(fileName)) return true;\n if (DEV_SETUP_RE.test(fileName)) return true;\n return false;\n }\n\n /**\n * The package a bare specifier belongs to, or null for relative/absolute\n * paths and node: builtins. `@scope/pkg/sub` ā `@scope/pkg`; `pkg/sub` ā `pkg`.\n */\n toPackageName(specifier: string): string | null {\n if (specifier.length === 0) return null;\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null;\n if (specifier.startsWith('node:')) return null;\n const parts = specifier.split('/');\n if (specifier.startsWith('@')) {\n if (parts.length < 2) return null;\n return `${parts[0]}/${parts[1]}`;\n }\n return parts[0];\n }\n\n private walk(absDir: string, projectRoot: string, usage: DepUsage): void {\n const entries = fs.readdirSync(absDir, { withFileTypes: true });\n for (const entry of entries) {\n const absPath = path.join(absDir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n this.walk(absPath, projectRoot, usage);\n continue;\n }\n if (!entry.isFile()) continue;\n if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue;\n this.scanFile(absPath, projectRoot, usage);\n }\n }\n\n private scanFile(absPath: string, projectRoot: string, usage: DepUsage): void {\n const source = this.readFile(absPath);\n if (source === null) return;\n const relPath = path.relative(projectRoot, absPath);\n const bucket = this.isDevFile(relPath) ? usage.testPackages : usage.prodPackages;\n for (const packageName of this.extractPackageNames(source)) {\n bucket.add(packageName);\n }\n }\n\n private extractPackageNames(source: string): string[] {\n const names: string[] = [];\n IMPORT_RE.lastIndex = 0;\n let match = IMPORT_RE.exec(source);\n while (match !== null) {\n const packageName = this.toPackageName(match[1]);\n if (packageName !== null) names.push(packageName);\n match = IMPORT_RE.exec(source);\n }\n return names;\n }\n\n private readFile(absPath: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readFileSync(absPath, 'utf-8');\n } catch (err: unknown) {\n const error = toError(err);\n // An unreadable file cannot change the classification of a package; skipping it\n // only ever makes the scan MORE conservative (fewer test-only classifications).\n console.warn(`Could not read ${absPath} while classifying deps: ${error.message}`);\n return null;\n }\n }\n}\n"]}
|
|
@@ -1,50 +1,186 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package Validator
|
|
3
3
|
*
|
|
4
|
-
* Validates that package.json dependencies match the
|
|
5
|
-
*
|
|
4
|
+
* Validates that package.json dependencies match the architecture graph nx derived
|
|
5
|
+
* from the source, and ā critically ā that each dependency is declared in the RIGHT
|
|
6
|
+
* SECTION of package.json:
|
|
7
|
+
*
|
|
8
|
+
* - reached from production source ā `dependencies` (or `peerDependencies`)
|
|
9
|
+
* - reached ONLY from test/dev files ā `devDependencies`
|
|
10
|
+
*
|
|
11
|
+
* WHY the section matters: production images are built with
|
|
12
|
+
* `pnpm --filter=<svc> deploy --prod`, which installs exactly the `dependencies`
|
|
13
|
+
* closure. A test-support package parked in `dependencies` therefore ships test
|
|
14
|
+
* machinery (auth-bypass hooks, canned credentials, fake datastores) into the
|
|
15
|
+
* production container. Before this validator understood `devDependencies`, moving
|
|
16
|
+
* such a package to its correct home FAILED the build ā the tool enforced the
|
|
17
|
+
* insecure layout. Now `devDependencies` is the required home for test-only deps,
|
|
18
|
+
* and listing one in `dependencies` is itself a violation.
|
|
6
19
|
*/
|
|
20
|
+
import { DepUsage } from './dep-usage-scanner';
|
|
21
|
+
/**
|
|
22
|
+
* How hard to push back when a test-only package sits in `dependencies`
|
|
23
|
+
* (i.e. inside the production deploy closure).
|
|
24
|
+
*
|
|
25
|
+
* - 'error' (default): fails the build. This is the guardrail the security bug asked for.
|
|
26
|
+
* - 'warn': reports it without failing ā the migration setting for a repo that needs a
|
|
27
|
+
* few releases to clean up its package.json files.
|
|
28
|
+
* - 'off': skip the check entirely.
|
|
29
|
+
*
|
|
30
|
+
* Missing deps, and production imports declared only in `devDependencies`, ALWAYS error:
|
|
31
|
+
* their fix is unambiguous and the alternative is a broken runtime.
|
|
32
|
+
*/
|
|
33
|
+
export type TestOnlyDepMode = 'error' | 'warn' | 'off';
|
|
34
|
+
/**
|
|
35
|
+
* Options for {@link validatePackageJsonDependencies}. Data-only (CLAUDE.md: data
|
|
36
|
+
* structures are classes, never anonymous object literals).
|
|
37
|
+
*/
|
|
38
|
+
export declare class PackageValidatorOptions {
|
|
39
|
+
testOnlyDepMode: TestOnlyDepMode;
|
|
40
|
+
constructor(testOnlyDepMode?: TestOnlyDepMode);
|
|
41
|
+
}
|
|
7
42
|
/**
|
|
8
43
|
* Validation result for a single project
|
|
9
44
|
*/
|
|
10
|
-
export
|
|
45
|
+
export declare class ProjectValidationResult {
|
|
11
46
|
project: string;
|
|
12
47
|
valid: boolean;
|
|
13
48
|
missingInPackageJson: string[];
|
|
14
49
|
extraInPackageJson: string[];
|
|
50
|
+
/** Graph deps that are test-only but declared in `dependencies` (production closure). */
|
|
51
|
+
testOnlyInProdDependencies: string[];
|
|
52
|
+
constructor(project: string, valid: boolean, missingInPackageJson: string[], extraInPackageJson: string[], testOnlyInProdDependencies: string[]);
|
|
15
53
|
}
|
|
16
54
|
/**
|
|
17
55
|
* Overall validation result
|
|
18
56
|
*
|
|
19
|
-
* `errors` fail the build
|
|
20
|
-
*
|
|
57
|
+
* `errors` fail the build. Every error's fix is either ADDITIVE ("add it to package.json")
|
|
58
|
+
* or a MOVE between sections ("it belongs in devDependencies") ā never "delete a
|
|
59
|
+
* dependency", so no error can push a user toward removing a runtime-required package.
|
|
21
60
|
*
|
|
22
|
-
* `warnings` never fail the build. Workspace deps in package.json that the architecture
|
|
23
|
-
* can't reach are reported here, NOT as errors: a transitively-reachable or even
|
|
24
|
-
* entry can still be a real runtime dependency (e.g. a peerDependency or a
|
|
25
|
-
* that nx's import analysis doesn't traverse). Erroring on these is the
|
|
26
|
-
* that previously forced a bad package.json edit ā so we only warn.
|
|
61
|
+
* `warnings` never fail the build. Workspace deps in package.json that the architecture
|
|
62
|
+
* graph can't reach are reported here, NOT as errors: a transitively-reachable or even
|
|
63
|
+
* unreachable entry can still be a real runtime dependency (e.g. a peerDependency or a
|
|
64
|
+
* generated client that nx's import analysis doesn't traverse). Erroring on these is the
|
|
65
|
+
* "runtime-validity trap" that previously forced a bad package.json edit ā so we only warn.
|
|
27
66
|
*/
|
|
28
|
-
export
|
|
67
|
+
export declare class ValidationResult {
|
|
29
68
|
valid: boolean;
|
|
30
69
|
errors: string[];
|
|
31
70
|
warnings: string[];
|
|
32
71
|
projectResults: ProjectValidationResult[];
|
|
72
|
+
constructor(valid: boolean, errors: string[], warnings: string[], projectResults: ProjectValidationResult[]);
|
|
33
73
|
}
|
|
34
74
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
* The three package.json sections that matter, kept apart so the validator can say
|
|
76
|
+
* WHICH one a package belongs in (the old code merged them and lost that information ā
|
|
77
|
+
* and never even read devDependencies).
|
|
78
|
+
*/
|
|
79
|
+
export declare class DeclaredDeps {
|
|
80
|
+
dependencies: string[];
|
|
81
|
+
devDependencies: string[];
|
|
82
|
+
peerDependencies: string[];
|
|
83
|
+
constructor(dependencies: string[], devDependencies: string[], peerDependencies: string[]);
|
|
84
|
+
/** Every declared package name, deduped and sorted (all three sections). */
|
|
85
|
+
all(): string[];
|
|
86
|
+
/** Declared somewhere that survives `pnpm deploy --prod`. */
|
|
87
|
+
isProductionDeclared(packageName: string): boolean;
|
|
88
|
+
isDevDeclared(packageName: string): boolean;
|
|
89
|
+
isDeclared(packageName: string): boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Graph shape produced by graph-sorter (an input contract we never construct here).
|
|
44
93
|
*/
|
|
45
94
|
interface GraphEntry {
|
|
46
95
|
level: number;
|
|
47
96
|
dependsOn: string[];
|
|
48
97
|
}
|
|
49
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Per-project classification of graph deps against what package.json declares.
|
|
100
|
+
*/
|
|
101
|
+
declare class DepClassification {
|
|
102
|
+
/** Production-reached deps absent from `dependencies`/`peerDependencies`. */
|
|
103
|
+
missingInPackageJson: string[];
|
|
104
|
+
/** Production-reached deps declared ONLY in `devDependencies` (runtime would break). */
|
|
105
|
+
prodDepsOnlyInDev: string[];
|
|
106
|
+
/** Test-only deps declared in no section at all. */
|
|
107
|
+
missingTestOnlyDeps: string[];
|
|
108
|
+
/** Test-only deps sitting in `dependencies` ā i.e. shipped to production. */
|
|
109
|
+
testOnlyInProdDependencies: string[];
|
|
110
|
+
/** Non-workspace (third-party) package.json entries ā informational only. */
|
|
111
|
+
extraInPackageJson: string[];
|
|
112
|
+
/** Workspace entries the graph cannot reach at all ā warn-only drift. */
|
|
113
|
+
extraWorkspaceDeps: string[];
|
|
114
|
+
}
|
|
115
|
+
declare class SingleProjectValidation {
|
|
116
|
+
result: ProjectValidationResult;
|
|
117
|
+
errors: string[];
|
|
118
|
+
warnings: string[];
|
|
119
|
+
constructor(result: ProjectValidationResult, errors: string[], warnings: string[]);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The per-workspace lookups a single-project validation needs, passed as one object
|
|
123
|
+
* so method signatures stay readable.
|
|
124
|
+
*/
|
|
125
|
+
declare class ValidationContext {
|
|
126
|
+
graph: Record<string, GraphEntry>;
|
|
127
|
+
projectToPackage: Map<string, string>;
|
|
128
|
+
packageToProject: Map<string, string>;
|
|
129
|
+
options: PackageValidatorOptions;
|
|
130
|
+
constructor(graph: Record<string, GraphEntry>, projectToPackage: Map<string, string>, packageToProject: Map<string, string>, options: PackageValidatorOptions);
|
|
131
|
+
}
|
|
132
|
+
export declare class PackageValidator {
|
|
133
|
+
private readonly scanner;
|
|
134
|
+
/**
|
|
135
|
+
* Read the three dependency sections of a project's package.json.
|
|
136
|
+
* Returns null when there is no package.json (apps often have none) so the caller
|
|
137
|
+
* can skip the project entirely.
|
|
138
|
+
*/
|
|
139
|
+
readDeclaredDeps(workspaceRoot: string, projectRoot: string): DeclaredDeps | null;
|
|
140
|
+
private namesOf;
|
|
141
|
+
/**
|
|
142
|
+
* Build map of project names to their package names
|
|
143
|
+
* e.g., "core-util" ā "@webpieces/core-util"
|
|
144
|
+
*/
|
|
145
|
+
buildProjectToPackageMap(workspaceRoot: string, projectsConfig: any): Map<string, string>;
|
|
146
|
+
private readPackageName;
|
|
147
|
+
/**
|
|
148
|
+
* Compute the transitive closure of a project's dependencies in the graph.
|
|
149
|
+
* Example: server ā [core-meta, http-server]; the closure includes http-server and
|
|
150
|
+
* everything http-server reaches.
|
|
151
|
+
*
|
|
152
|
+
* Used to allow package.json entries for transitive deps (a legitimate pattern:
|
|
153
|
+
* npm install brings the whole dependency tree, so a consumer may list any reachable
|
|
154
|
+
* package directly).
|
|
155
|
+
*/
|
|
156
|
+
computeTransitiveClosure(projectName: string, graph: Record<string, GraphEntry>): Set<string>;
|
|
157
|
+
/**
|
|
158
|
+
* Split a project's graph deps into "declared correctly", "missing", and "declared in
|
|
159
|
+
* the wrong section", using the import scan to decide which section each dep belongs in.
|
|
160
|
+
*/
|
|
161
|
+
classifyDeps(declared: DeclaredDeps, usage: DepUsage, entry: GraphEntry, transitiveClosure: Set<string>, context: ValidationContext): DepClassification;
|
|
162
|
+
/**
|
|
163
|
+
* The heart of the fix: which SECTION does this dep belong in?
|
|
164
|
+
*
|
|
165
|
+
* A dep is test-only when the scan saw it imported by test/dev files and by NO
|
|
166
|
+
* production file. Anything else ā including a dep we never saw imported at all (it
|
|
167
|
+
* may be loaded reflectively at runtime) ā is treated as production, so this can
|
|
168
|
+
* never push a runtime-required package out of `dependencies`.
|
|
169
|
+
*/
|
|
170
|
+
private classifyOneDep;
|
|
171
|
+
validateSingleProject(projectName: string, entry: GraphEntry, projectRoot: string, declared: DeclaredDeps, usage: DepUsage, context: ValidationContext): SingleProjectValidation;
|
|
172
|
+
private buildErrors;
|
|
173
|
+
private buildWarnings;
|
|
174
|
+
private testOnlyInProdMessage;
|
|
175
|
+
validate(graph: Record<string, GraphEntry>, workspaceRoot: string, options: PackageValidatorOptions): Promise<ValidationResult>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Validate that package.json dependencies cover the dependency graph AND that each dep
|
|
179
|
+
* is declared in the correct section (dependencies vs devDependencies).
|
|
180
|
+
*
|
|
181
|
+
* @param graph - Enhanced graph with project dependencies (uses project names)
|
|
182
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
183
|
+
* @param options - Strictness of the "test-only dep in the production closure" check
|
|
184
|
+
*/
|
|
185
|
+
export declare function validatePackageJsonDependencies(graph: Record<string, GraphEntry>, workspaceRoot: string, options?: PackageValidatorOptions): Promise<ValidationResult>;
|
|
50
186
|
export {};
|
|
@@ -2,181 +2,405 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Package Validator
|
|
4
4
|
*
|
|
5
|
-
* Validates that package.json dependencies match the
|
|
6
|
-
*
|
|
5
|
+
* Validates that package.json dependencies match the architecture graph nx derived
|
|
6
|
+
* from the source, and ā critically ā that each dependency is declared in the RIGHT
|
|
7
|
+
* SECTION of package.json:
|
|
8
|
+
*
|
|
9
|
+
* - reached from production source ā `dependencies` (or `peerDependencies`)
|
|
10
|
+
* - reached ONLY from test/dev files ā `devDependencies`
|
|
11
|
+
*
|
|
12
|
+
* WHY the section matters: production images are built with
|
|
13
|
+
* `pnpm --filter=<svc> deploy --prod`, which installs exactly the `dependencies`
|
|
14
|
+
* closure. A test-support package parked in `dependencies` therefore ships test
|
|
15
|
+
* machinery (auth-bypass hooks, canned credentials, fake datastores) into the
|
|
16
|
+
* production container. Before this validator understood `devDependencies`, moving
|
|
17
|
+
* such a package to its correct home FAILED the build ā the tool enforced the
|
|
18
|
+
* insecure layout. Now `devDependencies` is the required home for test-only deps,
|
|
19
|
+
* and listing one in `dependencies` is itself a violation.
|
|
7
20
|
*/
|
|
8
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.PackageValidator = exports.DeclaredDeps = exports.ValidationResult = exports.ProjectValidationResult = exports.PackageValidatorOptions = void 0;
|
|
9
23
|
exports.validatePackageJsonDependencies = validatePackageJsonDependencies;
|
|
10
24
|
const tslib_1 = require("tslib");
|
|
11
25
|
const fs = tslib_1.__importStar(require("fs"));
|
|
12
26
|
const path = tslib_1.__importStar(require("path"));
|
|
13
27
|
const devkit_1 = require("@nx/devkit");
|
|
28
|
+
const dep_usage_scanner_1 = require("./dep-usage-scanner");
|
|
29
|
+
const toError_1 = require("../toError");
|
|
14
30
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
31
|
+
* Options for {@link validatePackageJsonDependencies}. Data-only (CLAUDE.md: data
|
|
32
|
+
* structures are classes, never anonymous object literals).
|
|
17
33
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
24
|
-
try {
|
|
25
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
26
|
-
const deps = [];
|
|
27
|
-
// Collect ALL dependencies from package.json
|
|
28
|
-
for (const depType of ['dependencies', 'peerDependencies']) {
|
|
29
|
-
const depObj = packageJson[depType] || {};
|
|
30
|
-
for (const depName of Object.keys(depObj)) {
|
|
31
|
-
if (!deps.includes(depName)) {
|
|
32
|
-
deps.push(depName);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return deps.sort();
|
|
34
|
+
class PackageValidatorOptions {
|
|
35
|
+
testOnlyDepMode;
|
|
36
|
+
constructor(testOnlyDepMode = 'error') {
|
|
37
|
+
this.testOnlyDepMode = testOnlyDepMode;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
}
|
|
40
|
+
exports.PackageValidatorOptions = PackageValidatorOptions;
|
|
41
|
+
/**
|
|
42
|
+
* Validation result for a single project
|
|
43
|
+
*/
|
|
44
|
+
class ProjectValidationResult {
|
|
45
|
+
project;
|
|
46
|
+
valid;
|
|
47
|
+
missingInPackageJson;
|
|
48
|
+
extraInPackageJson;
|
|
49
|
+
/** Graph deps that are test-only but declared in `dependencies` (production closure). */
|
|
50
|
+
testOnlyInProdDependencies;
|
|
51
|
+
constructor(project, valid, missingInPackageJson, extraInPackageJson, testOnlyInProdDependencies) {
|
|
52
|
+
this.project = project;
|
|
53
|
+
this.valid = valid;
|
|
54
|
+
this.missingInPackageJson = missingInPackageJson;
|
|
55
|
+
this.extraInPackageJson = extraInPackageJson;
|
|
56
|
+
this.testOnlyInProdDependencies = testOnlyInProdDependencies;
|
|
43
57
|
}
|
|
44
58
|
}
|
|
59
|
+
exports.ProjectValidationResult = ProjectValidationResult;
|
|
45
60
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
61
|
+
* Overall validation result
|
|
62
|
+
*
|
|
63
|
+
* `errors` fail the build. Every error's fix is either ADDITIVE ("add it to package.json")
|
|
64
|
+
* or a MOVE between sections ("it belongs in devDependencies") ā never "delete a
|
|
65
|
+
* dependency", so no error can push a user toward removing a runtime-required package.
|
|
66
|
+
*
|
|
67
|
+
* `warnings` never fail the build. Workspace deps in package.json that the architecture
|
|
68
|
+
* graph can't reach are reported here, NOT as errors: a transitively-reachable or even
|
|
69
|
+
* unreachable entry can still be a real runtime dependency (e.g. a peerDependency or a
|
|
70
|
+
* generated client that nx's import analysis doesn't traverse). Erroring on these is the
|
|
71
|
+
* "runtime-validity trap" that previously forced a bad package.json edit ā so we only warn.
|
|
72
|
+
*/
|
|
73
|
+
class ValidationResult {
|
|
74
|
+
valid;
|
|
75
|
+
errors;
|
|
76
|
+
warnings;
|
|
77
|
+
projectResults;
|
|
78
|
+
constructor(valid, errors, warnings, projectResults) {
|
|
79
|
+
this.valid = valid;
|
|
80
|
+
this.errors = errors;
|
|
81
|
+
this.warnings = warnings;
|
|
82
|
+
this.projectResults = projectResults;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.ValidationResult = ValidationResult;
|
|
86
|
+
/**
|
|
87
|
+
* The three package.json sections that matter, kept apart so the validator can say
|
|
88
|
+
* WHICH one a package belongs in (the old code merged them and lost that information ā
|
|
89
|
+
* and never even read devDependencies).
|
|
48
90
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
class DeclaredDeps {
|
|
92
|
+
dependencies;
|
|
93
|
+
devDependencies;
|
|
94
|
+
peerDependencies;
|
|
95
|
+
constructor(dependencies, devDependencies, peerDependencies) {
|
|
96
|
+
this.dependencies = dependencies;
|
|
97
|
+
this.devDependencies = devDependencies;
|
|
98
|
+
this.peerDependencies = peerDependencies;
|
|
99
|
+
}
|
|
100
|
+
/** Every declared package name, deduped and sorted (all three sections). */
|
|
101
|
+
all() {
|
|
102
|
+
const merged = new Set();
|
|
103
|
+
for (const name of this.dependencies)
|
|
104
|
+
merged.add(name);
|
|
105
|
+
for (const name of this.devDependencies)
|
|
106
|
+
merged.add(name);
|
|
107
|
+
for (const name of this.peerDependencies)
|
|
108
|
+
merged.add(name);
|
|
109
|
+
return Array.from(merged).sort();
|
|
110
|
+
}
|
|
111
|
+
/** Declared somewhere that survives `pnpm deploy --prod`. */
|
|
112
|
+
isProductionDeclared(packageName) {
|
|
113
|
+
return (this.dependencies.includes(packageName) ||
|
|
114
|
+
this.peerDependencies.includes(packageName));
|
|
115
|
+
}
|
|
116
|
+
isDevDeclared(packageName) {
|
|
117
|
+
return this.devDependencies.includes(packageName);
|
|
118
|
+
}
|
|
119
|
+
isDeclared(packageName) {
|
|
120
|
+
return this.isProductionDeclared(packageName) || this.isDevDeclared(packageName);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.DeclaredDeps = DeclaredDeps;
|
|
124
|
+
/**
|
|
125
|
+
* Per-project classification of graph deps against what package.json declares.
|
|
126
|
+
*/
|
|
127
|
+
class DepClassification {
|
|
128
|
+
/** Production-reached deps absent from `dependencies`/`peerDependencies`. */
|
|
129
|
+
missingInPackageJson = [];
|
|
130
|
+
/** Production-reached deps declared ONLY in `devDependencies` (runtime would break). */
|
|
131
|
+
prodDepsOnlyInDev = [];
|
|
132
|
+
/** Test-only deps declared in no section at all. */
|
|
133
|
+
missingTestOnlyDeps = [];
|
|
134
|
+
/** Test-only deps sitting in `dependencies` ā i.e. shipped to production. */
|
|
135
|
+
testOnlyInProdDependencies = [];
|
|
136
|
+
/** Non-workspace (third-party) package.json entries ā informational only. */
|
|
137
|
+
extraInPackageJson = [];
|
|
138
|
+
/** Workspace entries the graph cannot reach at all ā warn-only drift. */
|
|
139
|
+
extraWorkspaceDeps = [];
|
|
140
|
+
}
|
|
141
|
+
class SingleProjectValidation {
|
|
142
|
+
result;
|
|
143
|
+
errors;
|
|
144
|
+
warnings;
|
|
145
|
+
constructor(result, errors, warnings) {
|
|
146
|
+
this.result = result;
|
|
147
|
+
this.errors = errors;
|
|
148
|
+
this.warnings = warnings;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The per-workspace lookups a single-project validation needs, passed as one object
|
|
153
|
+
* so method signatures stay readable.
|
|
154
|
+
*/
|
|
155
|
+
class ValidationContext {
|
|
156
|
+
graph;
|
|
157
|
+
projectToPackage;
|
|
158
|
+
packageToProject;
|
|
159
|
+
options;
|
|
160
|
+
constructor(graph, projectToPackage, packageToProject, options) {
|
|
161
|
+
this.graph = graph;
|
|
162
|
+
this.projectToPackage = projectToPackage;
|
|
163
|
+
this.packageToProject = packageToProject;
|
|
164
|
+
this.options = options;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
class PackageValidator {
|
|
168
|
+
scanner = new dep_usage_scanner_1.DepUsageScanner();
|
|
169
|
+
/**
|
|
170
|
+
* Read the three dependency sections of a project's package.json.
|
|
171
|
+
* Returns null when there is no package.json (apps often have none) so the caller
|
|
172
|
+
* can skip the project entirely.
|
|
173
|
+
*/
|
|
174
|
+
readDeclaredDeps(workspaceRoot, projectRoot) {
|
|
175
|
+
const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');
|
|
176
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
180
|
+
try {
|
|
181
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
182
|
+
return new DeclaredDeps(this.namesOf(packageJson, 'dependencies'), this.namesOf(packageJson, 'devDependencies'), this.namesOf(packageJson, 'peerDependencies'));
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const error = (0, toError_1.toError)(err);
|
|
186
|
+
console.warn(`Could not read package.json at ${packageJsonPath}: ${error.message}`);
|
|
187
|
+
return new DeclaredDeps([], [], []);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// webpieces-disable no-any-unknown -- parsed JSON is inherently untyped
|
|
191
|
+
namesOf(packageJson, section) {
|
|
192
|
+
const depObj = packageJson[section] || {};
|
|
193
|
+
return Object.keys(depObj).sort();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build map of project names to their package names
|
|
197
|
+
* e.g., "core-util" ā "@webpieces/core-util"
|
|
198
|
+
*/
|
|
199
|
+
buildProjectToPackageMap(workspaceRoot,
|
|
200
|
+
// webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed
|
|
201
|
+
projectsConfig) {
|
|
202
|
+
const map = new Map();
|
|
203
|
+
// webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped
|
|
204
|
+
for (const configEntry of Object.entries(projectsConfig.projects)) {
|
|
205
|
+
const projectName = configEntry[0];
|
|
206
|
+
const packageJsonPath = path.join(workspaceRoot, configEntry[1].root, 'package.json');
|
|
207
|
+
if (!fs.existsSync(packageJsonPath))
|
|
208
|
+
continue;
|
|
209
|
+
const packageName = this.readPackageName(packageJsonPath);
|
|
210
|
+
if (packageName !== null)
|
|
211
|
+
map.set(projectName, packageName);
|
|
212
|
+
}
|
|
213
|
+
return map;
|
|
214
|
+
}
|
|
215
|
+
readPackageName(packageJsonPath) {
|
|
216
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
217
|
+
try {
|
|
218
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
219
|
+
return packageJson.name || null;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const error = (0, toError_1.toError)(err);
|
|
223
|
+
console.warn(`Could not parse ${packageJsonPath}: ${error.message}`);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Compute the transitive closure of a project's dependencies in the graph.
|
|
229
|
+
* Example: server ā [core-meta, http-server]; the closure includes http-server and
|
|
230
|
+
* everything http-server reaches.
|
|
231
|
+
*
|
|
232
|
+
* Used to allow package.json entries for transitive deps (a legitimate pattern:
|
|
233
|
+
* npm install brings the whole dependency tree, so a consumer may list any reachable
|
|
234
|
+
* package directly).
|
|
235
|
+
*/
|
|
236
|
+
computeTransitiveClosure(projectName, graph) {
|
|
237
|
+
const closure = new Set();
|
|
238
|
+
const stack = [projectName];
|
|
239
|
+
while (stack.length > 0) {
|
|
240
|
+
const current = stack.pop();
|
|
241
|
+
const entry = graph[current];
|
|
242
|
+
if (!entry)
|
|
243
|
+
continue;
|
|
244
|
+
for (const dep of entry.dependsOn) {
|
|
245
|
+
if (!closure.has(dep)) {
|
|
246
|
+
closure.add(dep);
|
|
247
|
+
stack.push(dep);
|
|
62
248
|
}
|
|
63
249
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
250
|
+
}
|
|
251
|
+
return closure;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Split a project's graph deps into "declared correctly", "missing", and "declared in
|
|
255
|
+
* the wrong section", using the import scan to decide which section each dep belongs in.
|
|
256
|
+
*/
|
|
257
|
+
classifyDeps(declared, usage, entry, transitiveClosure, context) {
|
|
258
|
+
const classification = new DepClassification();
|
|
259
|
+
for (const depProjectName of entry.dependsOn) {
|
|
260
|
+
const depPackageName = context.projectToPackage.get(depProjectName) || depProjectName;
|
|
261
|
+
this.classifyOneDep(depProjectName, depPackageName, declared, usage, classification);
|
|
262
|
+
}
|
|
263
|
+
// Workspace extras are OK if reachable via transitive closure (matches the ESLint
|
|
264
|
+
// enforce-architecture rule which also allows transitive imports). Only flag extras
|
|
265
|
+
// that are NOT reachable at all ā real graph drift.
|
|
266
|
+
for (const dep of declared.all()) {
|
|
267
|
+
const depProjectName = context.packageToProject.get(dep);
|
|
268
|
+
if (depProjectName === undefined) {
|
|
269
|
+
classification.extraInPackageJson.push(dep);
|
|
270
|
+
continue;
|
|
68
271
|
}
|
|
272
|
+
if (entry.dependsOn.includes(depProjectName))
|
|
273
|
+
continue;
|
|
274
|
+
if (transitiveClosure.has(depProjectName))
|
|
275
|
+
continue;
|
|
276
|
+
classification.extraWorkspaceDeps.push(dep);
|
|
69
277
|
}
|
|
278
|
+
return classification;
|
|
70
279
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const closure = new Set();
|
|
85
|
-
const stack = [projectName];
|
|
86
|
-
while (stack.length > 0) {
|
|
87
|
-
const current = stack.pop();
|
|
88
|
-
const entry = graph[current];
|
|
89
|
-
if (!entry)
|
|
90
|
-
continue;
|
|
91
|
-
for (const dep of entry.dependsOn) {
|
|
92
|
-
if (!closure.has(dep)) {
|
|
93
|
-
closure.add(dep);
|
|
94
|
-
stack.push(dep);
|
|
280
|
+
/**
|
|
281
|
+
* The heart of the fix: which SECTION does this dep belong in?
|
|
282
|
+
*
|
|
283
|
+
* A dep is test-only when the scan saw it imported by test/dev files and by NO
|
|
284
|
+
* production file. Anything else ā including a dep we never saw imported at all (it
|
|
285
|
+
* may be loaded reflectively at runtime) ā is treated as production, so this can
|
|
286
|
+
* never push a runtime-required package out of `dependencies`.
|
|
287
|
+
*/
|
|
288
|
+
classifyOneDep(depProjectName, depPackageName, declared, usage, classification) {
|
|
289
|
+
if (usage.isTestOnly(depPackageName)) {
|
|
290
|
+
if (!declared.isDeclared(depPackageName)) {
|
|
291
|
+
classification.missingTestOnlyDeps.push(depProjectName);
|
|
292
|
+
return;
|
|
95
293
|
}
|
|
294
|
+
if (declared.dependencies.includes(depPackageName)) {
|
|
295
|
+
classification.testOnlyInProdDependencies.push(depProjectName);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (declared.isProductionDeclared(depPackageName))
|
|
300
|
+
return;
|
|
301
|
+
if (declared.isDevDeclared(depPackageName)) {
|
|
302
|
+
classification.prodDepsOnlyInDev.push(depProjectName);
|
|
303
|
+
return;
|
|
96
304
|
}
|
|
305
|
+
classification.missingInPackageJson.push(depProjectName);
|
|
306
|
+
}
|
|
307
|
+
validateSingleProject(projectName, entry, projectRoot, declared, usage, context) {
|
|
308
|
+
const transitiveClosure = this.computeTransitiveClosure(projectName, context.graph);
|
|
309
|
+
const classification = this.classifyDeps(declared, usage, entry, transitiveClosure, context);
|
|
310
|
+
const where = `Project ${projectName} (${projectRoot}/package.json)`;
|
|
311
|
+
const errors = this.buildErrors(where, classification, context.options);
|
|
312
|
+
const warnings = this.buildWarnings(where, projectName, classification, context);
|
|
313
|
+
const missing = classification.missingInPackageJson.concat(classification.missingTestOnlyDeps);
|
|
314
|
+
const result = new ProjectValidationResult(projectName, errors.length === 0, missing, classification.extraInPackageJson, classification.testOnlyInProdDependencies);
|
|
315
|
+
return new SingleProjectValidation(result, errors, warnings);
|
|
316
|
+
}
|
|
317
|
+
buildErrors(where, classification, options) {
|
|
318
|
+
const errors = [];
|
|
319
|
+
if (classification.missingInPackageJson.length > 0) {
|
|
320
|
+
errors.push(`${where} is missing dependencies: ${classification.missingInPackageJson.join(', ')}\n` +
|
|
321
|
+
` These are imported by PRODUCTION source files.\n` +
|
|
322
|
+
` Fix: Add them to package.json "dependencies"`);
|
|
323
|
+
}
|
|
324
|
+
if (classification.missingTestOnlyDeps.length > 0) {
|
|
325
|
+
errors.push(`${where} is missing dependencies: ${classification.missingTestOnlyDeps.join(', ')}\n` +
|
|
326
|
+
` These are imported ONLY by test/dev files (*.spec.ts, __tests__/, test configs).\n` +
|
|
327
|
+
` Fix: Add them to package.json "devDependencies" ā NOT "dependencies", which would ship them to production`);
|
|
328
|
+
}
|
|
329
|
+
if (classification.prodDepsOnlyInDev.length > 0) {
|
|
330
|
+
errors.push(`${where} declares production imports only in devDependencies: ${classification.prodDepsOnlyInDev.join(', ')}\n` +
|
|
331
|
+
` Production source files import them, so \`pnpm deploy --prod\` would omit them and the runtime would break.\n` +
|
|
332
|
+
` Fix: Move them to package.json "dependencies"`);
|
|
333
|
+
}
|
|
334
|
+
const testOnlyInProd = classification.testOnlyInProdDependencies;
|
|
335
|
+
if (options.testOnlyDepMode === 'error' && testOnlyInProd.length > 0) {
|
|
336
|
+
errors.push(this.testOnlyInProdMessage(where, testOnlyInProd));
|
|
337
|
+
}
|
|
338
|
+
return errors;
|
|
339
|
+
}
|
|
340
|
+
buildWarnings(where, projectName, classification, context) {
|
|
341
|
+
const warnings = [];
|
|
342
|
+
const testOnlyInProd = classification.testOnlyInProdDependencies;
|
|
343
|
+
if (context.options.testOnlyDepMode === 'warn' && testOnlyInProd.length > 0) {
|
|
344
|
+
warnings.push(this.testOnlyInProdMessage(where, testOnlyInProd));
|
|
345
|
+
}
|
|
346
|
+
// Unreachable workspace extras are WARN-ONLY: they may be real runtime deps that nx's
|
|
347
|
+
// import analysis can't see (peerDependency / generated client). Never error ā that is
|
|
348
|
+
// the runtime-validity trap. We surface them so genuine drift is still visible.
|
|
349
|
+
for (const extraPkg of classification.extraWorkspaceDeps) {
|
|
350
|
+
const extraProject = context.packageToProject.get(extraPkg);
|
|
351
|
+
warnings.push(`${where} has "${extraPkg}" but the architecture graph has no path ${projectName} ā ${extraProject}.\n` +
|
|
352
|
+
` This is allowed (it may be a runtime-only/peer dependency). If it is genuinely unused, you may remove it.`);
|
|
353
|
+
}
|
|
354
|
+
return warnings;
|
|
355
|
+
}
|
|
356
|
+
testOnlyInProdMessage(where, deps) {
|
|
357
|
+
return (`${where} lists test-only packages in "dependencies": ${deps.join(', ')}\n` +
|
|
358
|
+
` No production source file imports them ā only test/dev files do ā yet \`pnpm deploy --prod\`\n` +
|
|
359
|
+
` installs the "dependencies" closure, so this ships test machinery (fakes, auth-bypass hooks,\n` +
|
|
360
|
+
` canned credentials) into the production image.\n` +
|
|
361
|
+
` Fix: Move them to package.json "devDependencies"`);
|
|
362
|
+
}
|
|
363
|
+
async validate(graph, workspaceRoot, options) {
|
|
364
|
+
const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
|
|
365
|
+
const projectsConfig = (0, devkit_1.readProjectsConfigurationFromProjectGraph)(projectGraph);
|
|
366
|
+
const projectToPackage = this.buildProjectToPackageMap(workspaceRoot, projectsConfig);
|
|
367
|
+
const packageToProject = new Map();
|
|
368
|
+
for (const pair of projectToPackage.entries()) {
|
|
369
|
+
packageToProject.set(pair[1], pair[0]);
|
|
370
|
+
}
|
|
371
|
+
const context = new ValidationContext(graph, projectToPackage, packageToProject, options);
|
|
372
|
+
const errors = [];
|
|
373
|
+
const warnings = [];
|
|
374
|
+
const projectResults = [];
|
|
375
|
+
for (const graphPair of Object.entries(graph)) {
|
|
376
|
+
const projectName = graphPair[0];
|
|
377
|
+
const entry = graphPair[1];
|
|
378
|
+
const projectConfig = projectsConfig.projects[projectName];
|
|
379
|
+
if (!projectConfig)
|
|
380
|
+
continue;
|
|
381
|
+
const declared = this.readDeclaredDeps(workspaceRoot, projectConfig.root);
|
|
382
|
+
if (declared === null)
|
|
383
|
+
continue;
|
|
384
|
+
const usage = this.scanner.scan(path.join(workspaceRoot, projectConfig.root));
|
|
385
|
+
const validation = this.validateSingleProject(projectName, entry, projectConfig.root, declared, usage, context);
|
|
386
|
+
projectResults.push(validation.result);
|
|
387
|
+
errors.push(...validation.errors);
|
|
388
|
+
warnings.push(...validation.warnings);
|
|
389
|
+
}
|
|
390
|
+
return new ValidationResult(errors.length === 0, errors, warnings, projectResults);
|
|
97
391
|
}
|
|
98
|
-
return closure;
|
|
99
|
-
}
|
|
100
|
-
function classifyDeps(packageJsonDeps, entry, transitiveClosure, projectToPackage, packageToProject) {
|
|
101
|
-
const missingInPackageJson = [];
|
|
102
|
-
for (const depProjectName of entry.dependsOn) {
|
|
103
|
-
const depPackageName = projectToPackage.get(depProjectName) || depProjectName;
|
|
104
|
-
if (!packageJsonDeps.includes(depPackageName)) {
|
|
105
|
-
missingInPackageJson.push(depProjectName);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Workspace extras are OK if reachable via transitive closure (matches the
|
|
109
|
-
// ESLint enforce-architecture rule which also allows transitive imports).
|
|
110
|
-
// Only flag extras that are NOT reachable at all ā real graph drift.
|
|
111
|
-
const extraInPackageJson = [];
|
|
112
|
-
const extraWorkspaceDeps = [];
|
|
113
|
-
for (const dep of packageJsonDeps) {
|
|
114
|
-
const depProjectName = packageToProject.get(dep);
|
|
115
|
-
if (depProjectName === undefined) {
|
|
116
|
-
extraInPackageJson.push(dep);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (entry.dependsOn.includes(depProjectName))
|
|
120
|
-
continue;
|
|
121
|
-
if (transitiveClosure.has(depProjectName))
|
|
122
|
-
continue;
|
|
123
|
-
extraWorkspaceDeps.push(dep);
|
|
124
|
-
}
|
|
125
|
-
return { missingInPackageJson, extraInPackageJson, extraWorkspaceDeps };
|
|
126
|
-
}
|
|
127
|
-
function validateSingleProject(projectName, entry, projectRoot, packageJsonDeps, graph, projectToPackage, packageToProject) {
|
|
128
|
-
const transitiveClosure = computeTransitiveClosure(projectName, graph);
|
|
129
|
-
const classification = classifyDeps(packageJsonDeps, entry, transitiveClosure, projectToPackage, packageToProject);
|
|
130
|
-
const errors = [];
|
|
131
|
-
if (classification.missingInPackageJson.length > 0) {
|
|
132
|
-
errors.push(`Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${classification.missingInPackageJson.join(', ')}\n` +
|
|
133
|
-
` Fix: Add these to package.json dependencies`);
|
|
134
|
-
}
|
|
135
|
-
// Unreachable workspace extras are WARN-ONLY: they may be real runtime deps that nx's
|
|
136
|
-
// import analysis can't see (peerDependency / generated client). Never error ā that is
|
|
137
|
-
// the runtime-validity trap. We surface them so genuine drift is still visible.
|
|
138
|
-
const warnings = [];
|
|
139
|
-
for (const extraPkg of classification.extraWorkspaceDeps) {
|
|
140
|
-
const extraProject = packageToProject.get(extraPkg);
|
|
141
|
-
warnings.push(`Project ${projectName} (${projectRoot}/package.json) has "${extraPkg}" but the architecture graph has no path ${projectName} ā ${extraProject}.\n` +
|
|
142
|
-
` This is allowed (it may be a runtime-only/peer dependency). If it is genuinely unused, you may remove it.`);
|
|
143
|
-
}
|
|
144
|
-
// extraWorkspaceDeps do NOT affect validity ā only missing (additive-fix) errors do.
|
|
145
|
-
const valid = classification.missingInPackageJson.length === 0;
|
|
146
|
-
return {
|
|
147
|
-
result: {
|
|
148
|
-
project: projectName,
|
|
149
|
-
valid,
|
|
150
|
-
missingInPackageJson: classification.missingInPackageJson,
|
|
151
|
-
extraInPackageJson: classification.extraInPackageJson,
|
|
152
|
-
},
|
|
153
|
-
errors,
|
|
154
|
-
warnings,
|
|
155
|
-
};
|
|
156
392
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const projectConfig = projectsConfig.projects[projectName];
|
|
170
|
-
if (!projectConfig)
|
|
171
|
-
continue;
|
|
172
|
-
const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectConfig.root);
|
|
173
|
-
if (packageJsonDeps === null)
|
|
174
|
-
continue;
|
|
175
|
-
const validation = validateSingleProject(projectName, entry, projectConfig.root, packageJsonDeps, graph, projectToPackage, packageToProject);
|
|
176
|
-
projectResults.push(validation.result);
|
|
177
|
-
errors.push(...validation.errors);
|
|
178
|
-
warnings.push(...validation.warnings);
|
|
179
|
-
}
|
|
180
|
-
return { valid: errors.length === 0, errors, warnings, projectResults };
|
|
393
|
+
exports.PackageValidator = PackageValidator;
|
|
394
|
+
/**
|
|
395
|
+
* Validate that package.json dependencies cover the dependency graph AND that each dep
|
|
396
|
+
* is declared in the correct section (dependencies vs devDependencies).
|
|
397
|
+
*
|
|
398
|
+
* @param graph - Enhanced graph with project dependencies (uses project names)
|
|
399
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
400
|
+
* @param options - Strictness of the "test-only dep in the production closure" check
|
|
401
|
+
*/
|
|
402
|
+
// webpieces-disable no-function-outside-class -- stable module entry point imported by the executor; it only delegates to PackageValidator
|
|
403
|
+
async function validatePackageJsonDependencies(graph, workspaceRoot, options = new PackageValidatorOptions()) {
|
|
404
|
+
return new PackageValidator().validate(graph, workspaceRoot, options);
|
|
181
405
|
}
|
|
182
406
|
//# sourceMappingURL=package-validator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-validator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/package-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Package Validator\n *\n * Validates that package.json dependencies match the project.json build.dependsOn\n * This ensures the two sources of truth don't drift apart.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n createProjectGraphAsync,\n readProjectsConfigurationFromProjectGraph,\n} from '@nx/devkit';\nimport { toError } from '../toError';\n\n/**\n * Validation result for a single project\n */\nexport interface ProjectValidationResult {\n project: string;\n valid: boolean;\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n}\n\n/**\n * Overall validation result\n *\n * `errors` fail the build; their only fix is ADDITIVE (\"add to package.json\"), so they can\n * never push a user toward removing a runtime-required dependency.\n *\n * `warnings` never fail the build. Workspace deps in package.json that the architecture graph\n * can't reach are reported here, NOT as errors: a transitively-reachable or even unreachable\n * entry can still be a real runtime dependency (e.g. a peerDependency or a generated client\n * that nx's import analysis doesn't traverse). Erroring on these is the \"runtime-validity trap\"\n * that previously forced a bad package.json edit ā so we only warn.\n */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n projectResults: ProjectValidationResult[];\n}\n\n/**\n * Read package.json dependencies for a project\n * Returns null if package.json doesn't exist (apps often don't have one)\n */\nfunction readPackageJsonDeps(workspaceRoot: string, projectRoot: string): string[] | null {\n const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');\n\n if (!fs.existsSync(packageJsonPath)) {\n return null; // No package.json - skip validation for this project\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const deps: string[] = [];\n\n // Collect ALL dependencies from package.json\n for (const depType of ['dependencies', 'peerDependencies']) {\n const depObj = packageJson[depType] || {};\n for (const depName of Object.keys(depObj)) {\n if (!deps.includes(depName)) {\n deps.push(depName);\n }\n }\n }\n\n return deps.sort();\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n console.warn(`Could not read package.json at ${packageJsonPath}`);\n return [];\n }\n}\n\n/**\n * Build map of project names to their package names\n * e.g., \"core-util\" ā \"@webpieces/core-util\"\n */\nfunction buildProjectToPackageMap(\n workspaceRoot: string,\n // webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed\n projectsConfig: any\n): Map<string, string> {\n const map = new Map<string, string>();\n\n // webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped\n for (const [projectName, config] of Object.entries<any>(projectsConfig.projects)) {\n const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');\n if (fs.existsSync(packageJsonPath)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n if (packageJson.name) {\n map.set(projectName, packageJson.name);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n // Ignore parse errors\n }\n }\n }\n\n return map;\n}\n\n/**\n * Validate that package.json dependencies match the dependency graph\n *\n * For each project in the graph:\n * - Check that all graph dependencies exist in package.json\n * - Maps project names to package names for accurate comparison\n *\n * @param graph - Enhanced graph with project dependencies (uses project names)\n * @param workspaceRoot - Absolute path to workspace root\n * @returns Validation result with errors if any\n */\ninterface GraphEntry {\n level: number;\n dependsOn: string[];\n}\n\ninterface DepClassification {\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n extraWorkspaceDeps: string[];\n}\n\n/**\n * Compute the transitive closure of a project's dependencies in the graph.\n * Example: server ā [core-meta, http-server]; transitive closure includes\n * http-server and everything http-server reaches (http-routing,\n * core-context, core-util, http-api).\n *\n * Used to allow package.json entries for transitive deps (a legitimate pattern:\n * npm install brings the whole dependency tree, so a consumer may list any\n * reachable package directly).\n */\nfunction computeTransitiveClosure(\n projectName: string,\n graph: Record<string, GraphEntry>\n): Set<string> {\n const closure = new Set<string>();\n const stack = [projectName];\n while (stack.length > 0) {\n const current = stack.pop()!;\n const entry = graph[current];\n if (!entry) continue;\n for (const dep of entry.dependsOn) {\n if (!closure.has(dep)) {\n closure.add(dep);\n stack.push(dep);\n }\n }\n }\n return closure;\n}\n\ninterface SingleProjectValidation {\n result: ProjectValidationResult;\n errors: string[];\n warnings: string[];\n}\n\nfunction classifyDeps(\n packageJsonDeps: string[],\n entry: GraphEntry,\n transitiveClosure: Set<string>,\n projectToPackage: Map<string, string>,\n packageToProject: Map<string, string>\n): DepClassification {\n const missingInPackageJson: string[] = [];\n for (const depProjectName of entry.dependsOn) {\n const depPackageName = projectToPackage.get(depProjectName) || depProjectName;\n if (!packageJsonDeps.includes(depPackageName)) {\n missingInPackageJson.push(depProjectName);\n }\n }\n\n // Workspace extras are OK if reachable via transitive closure (matches the\n // ESLint enforce-architecture rule which also allows transitive imports).\n // Only flag extras that are NOT reachable at all ā real graph drift.\n const extraInPackageJson: string[] = [];\n const extraWorkspaceDeps: string[] = [];\n for (const dep of packageJsonDeps) {\n const depProjectName = packageToProject.get(dep);\n if (depProjectName === undefined) {\n extraInPackageJson.push(dep);\n continue;\n }\n if (entry.dependsOn.includes(depProjectName)) continue;\n if (transitiveClosure.has(depProjectName)) continue;\n extraWorkspaceDeps.push(dep);\n }\n\n return { missingInPackageJson, extraInPackageJson, extraWorkspaceDeps };\n}\n\nfunction validateSingleProject(\n projectName: string,\n entry: GraphEntry,\n projectRoot: string,\n packageJsonDeps: string[],\n graph: Record<string, GraphEntry>,\n projectToPackage: Map<string, string>,\n packageToProject: Map<string, string>\n): SingleProjectValidation {\n const transitiveClosure = computeTransitiveClosure(projectName, graph);\n const classification = classifyDeps(\n packageJsonDeps,\n entry,\n transitiveClosure,\n projectToPackage,\n packageToProject\n );\n\n const errors: string[] = [];\n if (classification.missingInPackageJson.length > 0) {\n errors.push(\n `Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${classification.missingInPackageJson.join(', ')}\\n` +\n ` Fix: Add these to package.json dependencies`\n );\n }\n\n // Unreachable workspace extras are WARN-ONLY: they may be real runtime deps that nx's\n // import analysis can't see (peerDependency / generated client). Never error ā that is\n // the runtime-validity trap. We surface them so genuine drift is still visible.\n const warnings: string[] = [];\n for (const extraPkg of classification.extraWorkspaceDeps) {\n const extraProject = packageToProject.get(extraPkg);\n warnings.push(\n `Project ${projectName} (${projectRoot}/package.json) has \"${extraPkg}\" but the architecture graph has no path ${projectName} ā ${extraProject}.\\n` +\n ` This is allowed (it may be a runtime-only/peer dependency). If it is genuinely unused, you may remove it.`\n );\n }\n\n // extraWorkspaceDeps do NOT affect validity ā only missing (additive-fix) errors do.\n const valid = classification.missingInPackageJson.length === 0;\n return {\n result: {\n project: projectName,\n valid,\n missingInPackageJson: classification.missingInPackageJson,\n extraInPackageJson: classification.extraInPackageJson,\n },\n errors,\n warnings,\n };\n}\n\nexport async function validatePackageJsonDependencies(\n graph: Record<string, GraphEntry>,\n workspaceRoot: string\n): Promise<ValidationResult> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);\n const packageToProject = new Map<string, string>();\n for (const [projName, pkgName] of projectToPackage.entries()) {\n packageToProject.set(pkgName, projName);\n }\n\n const errors: string[] = [];\n const warnings: string[] = [];\n const projectResults: ProjectValidationResult[] = [];\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const projectConfig = projectsConfig.projects[projectName];\n if (!projectConfig) continue;\n\n const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectConfig.root);\n if (packageJsonDeps === null) continue;\n\n const validation = validateSingleProject(\n projectName,\n entry,\n projectConfig.root,\n packageJsonDeps,\n graph,\n projectToPackage,\n packageToProject\n );\n projectResults.push(validation.result);\n errors.push(...validation.errors);\n warnings.push(...validation.warnings);\n }\n\n return { valid: errors.length === 0, errors, warnings, projectResults };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"package-validator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/package-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Package Validator\n *\n * Validates that package.json dependencies match the architecture graph nx derived\n * from the source, and ā critically ā that each dependency is declared in the RIGHT\n * SECTION of package.json:\n *\n * - reached from production source ā `dependencies` (or `peerDependencies`)\n * - reached ONLY from test/dev files ā `devDependencies`\n *\n * WHY the section matters: production images are built with\n * `pnpm --filter=<svc> deploy --prod`, which installs exactly the `dependencies`\n * closure. A test-support package parked in `dependencies` therefore ships test\n * machinery (auth-bypass hooks, canned credentials, fake datastores) into the\n * production container. Before this validator understood `devDependencies`, moving\n * such a package to its correct home FAILED the build ā the tool enforced the\n * insecure layout. Now `devDependencies` is the required home for test-only deps,\n * and listing one in `dependencies` is itself a violation.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n createProjectGraphAsync,\n readProjectsConfigurationFromProjectGraph,\n} from '@nx/devkit';\nimport { DepUsage, DepUsageScanner } from './dep-usage-scanner';\nimport { toError } from '../toError';\n\n/**\n * How hard to push back when a test-only package sits in `dependencies`\n * (i.e. inside the production deploy closure).\n *\n * - 'error' (default): fails the build. This is the guardrail the security bug asked for.\n * - 'warn': reports it without failing ā the migration setting for a repo that needs a\n * few releases to clean up its package.json files.\n * - 'off': skip the check entirely.\n *\n * Missing deps, and production imports declared only in `devDependencies`, ALWAYS error:\n * their fix is unambiguous and the alternative is a broken runtime.\n */\nexport type TestOnlyDepMode = 'error' | 'warn' | 'off';\n\n/**\n * Options for {@link validatePackageJsonDependencies}. Data-only (CLAUDE.md: data\n * structures are classes, never anonymous object literals).\n */\nexport class PackageValidatorOptions {\n testOnlyDepMode: TestOnlyDepMode;\n\n constructor(testOnlyDepMode: TestOnlyDepMode = 'error') {\n this.testOnlyDepMode = testOnlyDepMode;\n }\n}\n\n/**\n * Validation result for a single project\n */\nexport class ProjectValidationResult {\n project: string;\n valid: boolean;\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n /** Graph deps that are test-only but declared in `dependencies` (production closure). */\n testOnlyInProdDependencies: string[];\n\n constructor(\n project: string,\n valid: boolean,\n missingInPackageJson: string[],\n extraInPackageJson: string[],\n testOnlyInProdDependencies: string[]\n ) {\n this.project = project;\n this.valid = valid;\n this.missingInPackageJson = missingInPackageJson;\n this.extraInPackageJson = extraInPackageJson;\n this.testOnlyInProdDependencies = testOnlyInProdDependencies;\n }\n}\n\n/**\n * Overall validation result\n *\n * `errors` fail the build. Every error's fix is either ADDITIVE (\"add it to package.json\")\n * or a MOVE between sections (\"it belongs in devDependencies\") ā never \"delete a\n * dependency\", so no error can push a user toward removing a runtime-required package.\n *\n * `warnings` never fail the build. Workspace deps in package.json that the architecture\n * graph can't reach are reported here, NOT as errors: a transitively-reachable or even\n * unreachable entry can still be a real runtime dependency (e.g. a peerDependency or a\n * generated client that nx's import analysis doesn't traverse). Erroring on these is the\n * \"runtime-validity trap\" that previously forced a bad package.json edit ā so we only warn.\n */\nexport class ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n projectResults: ProjectValidationResult[];\n\n constructor(\n valid: boolean,\n errors: string[],\n warnings: string[],\n projectResults: ProjectValidationResult[]\n ) {\n this.valid = valid;\n this.errors = errors;\n this.warnings = warnings;\n this.projectResults = projectResults;\n }\n}\n\n/**\n * The three package.json sections that matter, kept apart so the validator can say\n * WHICH one a package belongs in (the old code merged them and lost that information ā\n * and never even read devDependencies).\n */\nexport class DeclaredDeps {\n dependencies: string[];\n devDependencies: string[];\n peerDependencies: string[];\n\n constructor(dependencies: string[], devDependencies: string[], peerDependencies: string[]) {\n this.dependencies = dependencies;\n this.devDependencies = devDependencies;\n this.peerDependencies = peerDependencies;\n }\n\n /** Every declared package name, deduped and sorted (all three sections). */\n all(): string[] {\n const merged = new Set<string>();\n for (const name of this.dependencies) merged.add(name);\n for (const name of this.devDependencies) merged.add(name);\n for (const name of this.peerDependencies) merged.add(name);\n return Array.from(merged).sort();\n }\n\n /** Declared somewhere that survives `pnpm deploy --prod`. */\n isProductionDeclared(packageName: string): boolean {\n return (\n this.dependencies.includes(packageName) ||\n this.peerDependencies.includes(packageName)\n );\n }\n\n isDevDeclared(packageName: string): boolean {\n return this.devDependencies.includes(packageName);\n }\n\n isDeclared(packageName: string): boolean {\n return this.isProductionDeclared(packageName) || this.isDevDeclared(packageName);\n }\n}\n\n/**\n * Graph shape produced by graph-sorter (an input contract we never construct here).\n */\ninterface GraphEntry {\n level: number;\n dependsOn: string[];\n}\n\n/**\n * Per-project classification of graph deps against what package.json declares.\n */\nclass DepClassification {\n /** Production-reached deps absent from `dependencies`/`peerDependencies`. */\n missingInPackageJson: string[] = [];\n /** Production-reached deps declared ONLY in `devDependencies` (runtime would break). */\n prodDepsOnlyInDev: string[] = [];\n /** Test-only deps declared in no section at all. */\n missingTestOnlyDeps: string[] = [];\n /** Test-only deps sitting in `dependencies` ā i.e. shipped to production. */\n testOnlyInProdDependencies: string[] = [];\n /** Non-workspace (third-party) package.json entries ā informational only. */\n extraInPackageJson: string[] = [];\n /** Workspace entries the graph cannot reach at all ā warn-only drift. */\n extraWorkspaceDeps: string[] = [];\n}\n\nclass SingleProjectValidation {\n result: ProjectValidationResult;\n errors: string[];\n warnings: string[];\n\n constructor(result: ProjectValidationResult, errors: string[], warnings: string[]) {\n this.result = result;\n this.errors = errors;\n this.warnings = warnings;\n }\n}\n\n/**\n * The per-workspace lookups a single-project validation needs, passed as one object\n * so method signatures stay readable.\n */\nclass ValidationContext {\n graph: Record<string, GraphEntry>;\n projectToPackage: Map<string, string>;\n packageToProject: Map<string, string>;\n options: PackageValidatorOptions;\n\n constructor(\n graph: Record<string, GraphEntry>,\n projectToPackage: Map<string, string>,\n packageToProject: Map<string, string>,\n options: PackageValidatorOptions\n ) {\n this.graph = graph;\n this.projectToPackage = projectToPackage;\n this.packageToProject = packageToProject;\n this.options = options;\n }\n}\n\nexport class PackageValidator {\n private readonly scanner = new DepUsageScanner();\n\n /**\n * Read the three dependency sections of a project's package.json.\n * Returns null when there is no package.json (apps often have none) so the caller\n * can skip the project entirely.\n */\n readDeclaredDeps(workspaceRoot: string, projectRoot: string): DeclaredDeps | null {\n const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return null;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n return new DeclaredDeps(\n this.namesOf(packageJson, 'dependencies'),\n this.namesOf(packageJson, 'devDependencies'),\n this.namesOf(packageJson, 'peerDependencies')\n );\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`Could not read package.json at ${packageJsonPath}: ${error.message}`);\n return new DeclaredDeps([], [], []);\n }\n }\n\n // webpieces-disable no-any-unknown -- parsed JSON is inherently untyped\n private namesOf(packageJson: any, section: string): string[] {\n const depObj = packageJson[section] || {};\n return Object.keys(depObj).sort();\n }\n\n /**\n * Build map of project names to their package names\n * e.g., \"core-util\" ā \"@webpieces/core-util\"\n */\n buildProjectToPackageMap(\n workspaceRoot: string,\n // webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed\n projectsConfig: any\n ): Map<string, string> {\n const map = new Map<string, string>();\n\n // webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped\n for (const configEntry of Object.entries<any>(projectsConfig.projects)) {\n const projectName = configEntry[0];\n const packageJsonPath = path.join(workspaceRoot, configEntry[1].root, 'package.json');\n if (!fs.existsSync(packageJsonPath)) continue;\n const packageName = this.readPackageName(packageJsonPath);\n if (packageName !== null) map.set(projectName, packageName);\n }\n\n return map;\n }\n\n private readPackageName(packageJsonPath: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n return packageJson.name || null;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`Could not parse ${packageJsonPath}: ${error.message}`);\n return null;\n }\n }\n\n /**\n * Compute the transitive closure of a project's dependencies in the graph.\n * Example: server ā [core-meta, http-server]; the closure includes http-server and\n * everything http-server reaches.\n *\n * Used to allow package.json entries for transitive deps (a legitimate pattern:\n * npm install brings the whole dependency tree, so a consumer may list any reachable\n * package directly).\n */\n computeTransitiveClosure(\n projectName: string,\n graph: Record<string, GraphEntry>\n ): Set<string> {\n const closure = new Set<string>();\n const stack = [projectName];\n while (stack.length > 0) {\n const current = stack.pop()!;\n const entry = graph[current];\n if (!entry) continue;\n for (const dep of entry.dependsOn) {\n if (!closure.has(dep)) {\n closure.add(dep);\n stack.push(dep);\n }\n }\n }\n return closure;\n }\n\n /**\n * Split a project's graph deps into \"declared correctly\", \"missing\", and \"declared in\n * the wrong section\", using the import scan to decide which section each dep belongs in.\n */\n classifyDeps(\n declared: DeclaredDeps,\n usage: DepUsage,\n entry: GraphEntry,\n transitiveClosure: Set<string>,\n context: ValidationContext\n ): DepClassification {\n const classification = new DepClassification();\n\n for (const depProjectName of entry.dependsOn) {\n const depPackageName = context.projectToPackage.get(depProjectName) || depProjectName;\n this.classifyOneDep(depProjectName, depPackageName, declared, usage, classification);\n }\n\n // Workspace extras are OK if reachable via transitive closure (matches the ESLint\n // enforce-architecture rule which also allows transitive imports). Only flag extras\n // that are NOT reachable at all ā real graph drift.\n for (const dep of declared.all()) {\n const depProjectName = context.packageToProject.get(dep);\n if (depProjectName === undefined) {\n classification.extraInPackageJson.push(dep);\n continue;\n }\n if (entry.dependsOn.includes(depProjectName)) continue;\n if (transitiveClosure.has(depProjectName)) continue;\n classification.extraWorkspaceDeps.push(dep);\n }\n\n return classification;\n }\n\n /**\n * The heart of the fix: which SECTION does this dep belong in?\n *\n * A dep is test-only when the scan saw it imported by test/dev files and by NO\n * production file. Anything else ā including a dep we never saw imported at all (it\n * may be loaded reflectively at runtime) ā is treated as production, so this can\n * never push a runtime-required package out of `dependencies`.\n */\n private classifyOneDep(\n depProjectName: string,\n depPackageName: string,\n declared: DeclaredDeps,\n usage: DepUsage,\n classification: DepClassification\n ): void {\n if (usage.isTestOnly(depPackageName)) {\n if (!declared.isDeclared(depPackageName)) {\n classification.missingTestOnlyDeps.push(depProjectName);\n return;\n }\n if (declared.dependencies.includes(depPackageName)) {\n classification.testOnlyInProdDependencies.push(depProjectName);\n }\n return;\n }\n\n if (declared.isProductionDeclared(depPackageName)) return;\n if (declared.isDevDeclared(depPackageName)) {\n classification.prodDepsOnlyInDev.push(depProjectName);\n return;\n }\n classification.missingInPackageJson.push(depProjectName);\n }\n\n validateSingleProject(\n projectName: string,\n entry: GraphEntry,\n projectRoot: string,\n declared: DeclaredDeps,\n usage: DepUsage,\n context: ValidationContext\n ): SingleProjectValidation {\n const transitiveClosure = this.computeTransitiveClosure(projectName, context.graph);\n const classification = this.classifyDeps(\n declared,\n usage,\n entry,\n transitiveClosure,\n context\n );\n\n const where = `Project ${projectName} (${projectRoot}/package.json)`;\n const errors = this.buildErrors(where, classification, context.options);\n const warnings = this.buildWarnings(where, projectName, classification, context);\n\n const missing = classification.missingInPackageJson.concat(\n classification.missingTestOnlyDeps\n );\n const result = new ProjectValidationResult(\n projectName,\n errors.length === 0,\n missing,\n classification.extraInPackageJson,\n classification.testOnlyInProdDependencies\n );\n return new SingleProjectValidation(result, errors, warnings);\n }\n\n private buildErrors(\n where: string,\n classification: DepClassification,\n options: PackageValidatorOptions\n ): string[] {\n const errors: string[] = [];\n if (classification.missingInPackageJson.length > 0) {\n errors.push(\n `${where} is missing dependencies: ${classification.missingInPackageJson.join(', ')}\\n` +\n ` These are imported by PRODUCTION source files.\\n` +\n ` Fix: Add them to package.json \"dependencies\"`\n );\n }\n if (classification.missingTestOnlyDeps.length > 0) {\n errors.push(\n `${where} is missing dependencies: ${classification.missingTestOnlyDeps.join(', ')}\\n` +\n ` These are imported ONLY by test/dev files (*.spec.ts, __tests__/, test configs).\\n` +\n ` Fix: Add them to package.json \"devDependencies\" ā NOT \"dependencies\", which would ship them to production`\n );\n }\n if (classification.prodDepsOnlyInDev.length > 0) {\n errors.push(\n `${where} declares production imports only in devDependencies: ${classification.prodDepsOnlyInDev.join(', ')}\\n` +\n ` Production source files import them, so \\`pnpm deploy --prod\\` would omit them and the runtime would break.\\n` +\n ` Fix: Move them to package.json \"dependencies\"`\n );\n }\n const testOnlyInProd = classification.testOnlyInProdDependencies;\n if (options.testOnlyDepMode === 'error' && testOnlyInProd.length > 0) {\n errors.push(this.testOnlyInProdMessage(where, testOnlyInProd));\n }\n return errors;\n }\n\n private buildWarnings(\n where: string,\n projectName: string,\n classification: DepClassification,\n context: ValidationContext\n ): string[] {\n const warnings: string[] = [];\n\n const testOnlyInProd = classification.testOnlyInProdDependencies;\n if (context.options.testOnlyDepMode === 'warn' && testOnlyInProd.length > 0) {\n warnings.push(this.testOnlyInProdMessage(where, testOnlyInProd));\n }\n\n // Unreachable workspace extras are WARN-ONLY: they may be real runtime deps that nx's\n // import analysis can't see (peerDependency / generated client). Never error ā that is\n // the runtime-validity trap. We surface them so genuine drift is still visible.\n for (const extraPkg of classification.extraWorkspaceDeps) {\n const extraProject = context.packageToProject.get(extraPkg);\n warnings.push(\n `${where} has \"${extraPkg}\" but the architecture graph has no path ${projectName} ā ${extraProject}.\\n` +\n ` This is allowed (it may be a runtime-only/peer dependency). If it is genuinely unused, you may remove it.`\n );\n }\n return warnings;\n }\n\n private testOnlyInProdMessage(where: string, deps: string[]): string {\n return (\n `${where} lists test-only packages in \"dependencies\": ${deps.join(', ')}\\n` +\n ` No production source file imports them ā only test/dev files do ā yet \\`pnpm deploy --prod\\`\\n` +\n ` installs the \"dependencies\" closure, so this ships test machinery (fakes, auth-bypass hooks,\\n` +\n ` canned credentials) into the production image.\\n` +\n ` Fix: Move them to package.json \"devDependencies\"`\n );\n }\n\n async validate(\n graph: Record<string, GraphEntry>,\n workspaceRoot: string,\n options: PackageValidatorOptions\n ): Promise<ValidationResult> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n const projectToPackage = this.buildProjectToPackageMap(workspaceRoot, projectsConfig);\n const packageToProject = new Map<string, string>();\n for (const pair of projectToPackage.entries()) {\n packageToProject.set(pair[1], pair[0]);\n }\n const context = new ValidationContext(graph, projectToPackage, packageToProject, options);\n\n const errors: string[] = [];\n const warnings: string[] = [];\n const projectResults: ProjectValidationResult[] = [];\n\n for (const graphPair of Object.entries(graph)) {\n const projectName = graphPair[0];\n const entry = graphPair[1];\n const projectConfig = projectsConfig.projects[projectName];\n if (!projectConfig) continue;\n\n const declared = this.readDeclaredDeps(workspaceRoot, projectConfig.root);\n if (declared === null) continue;\n\n const usage = this.scanner.scan(path.join(workspaceRoot, projectConfig.root));\n const validation = this.validateSingleProject(\n projectName,\n entry,\n projectConfig.root,\n declared,\n usage,\n context\n );\n projectResults.push(validation.result);\n errors.push(...validation.errors);\n warnings.push(...validation.warnings);\n }\n\n return new ValidationResult(errors.length === 0, errors, warnings, projectResults);\n }\n}\n\n/**\n * Validate that package.json dependencies cover the dependency graph AND that each dep\n * is declared in the correct section (dependencies vs devDependencies).\n *\n * @param graph - Enhanced graph with project dependencies (uses project names)\n * @param workspaceRoot - Absolute path to workspace root\n * @param options - Strictness of the \"test-only dep in the production closure\" check\n */\n// webpieces-disable no-function-outside-class -- stable module entry point imported by the executor; it only delegates to PackageValidator\nexport async function validatePackageJsonDependencies(\n graph: Record<string, GraphEntry>,\n workspaceRoot: string,\n options: PackageValidatorOptions = new PackageValidatorOptions()\n): Promise<ValidationResult> {\n return new PackageValidator().validate(graph, workspaceRoot, options);\n}\n"]}
|