@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.465",
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.465",
25
- "@webpieces/code-rules": "0.4.465",
26
- "@webpieces/eslint-rules": "0.4.465",
27
- "@webpieces/pr-gate": "0.4.465",
28
- "@webpieces/rules-config": "0.4.465",
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 packageValidation = await (0, package_validator_1.validatePackageJsonDependencies)(enhancedGraph, workspaceRoot);
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 missing dependencies above');
46
- console.error(' 2. Add the missing dependencies to the respective package.json files');
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;;AAgBH,8BA0DC;AAvED,+DAAiE;AACjE,yDAAgE;AAChE,mEAA8E;AAC9E,2CAAwC;AAUzB,KAAK,UAAU,WAAW,CACrC,OAAmC,EACnC,OAAwB;IAExB,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;IAE3D,8DAA8D;IAC9D,IAAI,CAAC;QACD,+EAA+E;QAC/E,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;QACzE,MAAM,YAAY,GAAG,MAAM,IAAA,sCAAoB,GAAE,CAAC;QAElD,+DAA+D;QAC/D,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QAClD,MAAM,aAAa,GAAG,IAAA,qCAAsB,EAAC,YAAY,CAAC,CAAC;QAE3D,mDAAmD;QACnD,OAAO,CAAC,GAAG,CAAC,yEAAyE,CAAC,CAAC;QACvF,MAAM,iBAAiB,GAAG,MAAM,IAAA,mDAA+B,EAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QAE9F,iEAAiE;QACjE,IAAI,iBAAiB,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;YACxD,KAAK,MAAM,OAAO,IAAI,iBAAiB,CAAC,QAAQ,EAAE,CAAC;gBAC/C,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;YACjC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;YACrD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,iBAAiB,CAAC,MAAM,EAAE,CAAC;gBAC3C,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;YAChC,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAC5D,OAAO,CAAC,KAAK,CAAC,wEAAwE,CAAC,CAAC;YACxF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,0DAA0D,CAAC,CAAC;QAExE,gBAAgB;QAChB,MAAM,aAAa,GAAG,iBAAiB,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;QACnF,MAAM,aAAa,GAAG,iBAAiB,CAAC,cAAc,CAAC,MAAM,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,0BAA0B,aAAa,EAAE,CAAC,CAAC;QACvD,OAAO,CAAC,GAAG,CAAC,aAAa,aAAa,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,eAAe,aAAa,GAAG,aAAa,EAAE,CAAC,CAAC;QAE5D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAClE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","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 { validatePackageJsonDependencies } from '../../lib/package-validator';\nimport { toError } from '../../toError';\n\nexport interface ValidatePackageJsonOptions {\n // No options needed for now\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 packageValidation = await validatePackageJsonDependencies(enhancedGraph, workspaceRoot);\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 the missing dependencies above');\n console.error(' 2. Add the missing dependencies to the respective package.json files');\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
+ {"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 project.json build.dependsOn
5
- * This ensures the two sources of truth don't drift apart.
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 interface ProjectValidationResult {
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; their only fix is ADDITIVE ("add to package.json"), so they can
20
- * never push a user toward removing a runtime-required dependency.
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 graph
23
- * can't reach are reported here, NOT as errors: a transitively-reachable or even unreachable
24
- * entry can still be a real runtime dependency (e.g. a peerDependency or a generated client
25
- * that nx's import analysis doesn't traverse). Erroring on these is the "runtime-validity trap"
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 interface ValidationResult {
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
- * Validate that package.json dependencies match the dependency graph
36
- *
37
- * For each project in the graph:
38
- * - Check that all graph dependencies exist in package.json
39
- * - Maps project names to package names for accurate comparison
40
- *
41
- * @param graph - Enhanced graph with project dependencies (uses project names)
42
- * @param workspaceRoot - Absolute path to workspace root
43
- * @returns Validation result with errors if any
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
- export declare function validatePackageJsonDependencies(graph: Record<string, GraphEntry>, workspaceRoot: string): Promise<ValidationResult>;
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 project.json build.dependsOn
6
- * This ensures the two sources of truth don't drift apart.
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
- * Read package.json dependencies for a project
16
- * Returns null if package.json doesn't exist (apps often don't have one)
31
+ * Options for {@link validatePackageJsonDependencies}. Data-only (CLAUDE.md: data
32
+ * structures are classes, never anonymous object literals).
17
33
  */
18
- function readPackageJsonDeps(workspaceRoot, projectRoot) {
19
- const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');
20
- if (!fs.existsSync(packageJsonPath)) {
21
- return null; // No package.json - skip validation for this project
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
- catch (err) {
39
- //const error = toError(err);
40
- void err;
41
- console.warn(`Could not read package.json at ${packageJsonPath}`);
42
- return [];
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
- * Build map of project names to their package names
47
- * e.g., "core-util" → "@webpieces/core-util"
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
- function buildProjectToPackageMap(workspaceRoot,
50
- // webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed
51
- projectsConfig) {
52
- const map = new Map();
53
- // webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped
54
- for (const [projectName, config] of Object.entries(projectsConfig.projects)) {
55
- const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');
56
- if (fs.existsSync(packageJsonPath)) {
57
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
58
- try {
59
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
60
- if (packageJson.name) {
61
- map.set(projectName, packageJson.name);
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
- catch (err) {
65
- //const error = toError(err);
66
- void err;
67
- // Ignore parse errors
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
- return map;
72
- }
73
- /**
74
- * Compute the transitive closure of a project's dependencies in the graph.
75
- * Example: server → [core-meta, http-server]; transitive closure includes
76
- * http-server and everything http-server reaches (http-routing,
77
- * core-context, core-util, http-api).
78
- *
79
- * Used to allow package.json entries for transitive deps (a legitimate pattern:
80
- * npm install brings the whole dependency tree, so a consumer may list any
81
- * reachable package directly).
82
- */
83
- function computeTransitiveClosure(projectName, graph) {
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
- async function validatePackageJsonDependencies(graph, workspaceRoot) {
158
- const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
159
- const projectsConfig = (0, devkit_1.readProjectsConfigurationFromProjectGraph)(projectGraph);
160
- const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);
161
- const packageToProject = new Map();
162
- for (const [projName, pkgName] of projectToPackage.entries()) {
163
- packageToProject.set(pkgName, projName);
164
- }
165
- const errors = [];
166
- const warnings = [];
167
- const projectResults = [];
168
- for (const [projectName, entry] of Object.entries(graph)) {
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"]}