@webpieces/nx-webpieces-rules 0.4.464 β†’ 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.464",
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.464",
25
- "@webpieces/code-rules": "0.4.464",
26
- "@webpieces/eslint-rules": "0.4.464",
27
- "@webpieces/pr-gate": "0.4.464",
28
- "@webpieces/rules-config": "0.4.464",
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":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;;AAEH,+CAAyB;AACzB,mDAA6B;AAC7B,wCAAqC;AAErC,2EAA2E;AAC3E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACtB,cAAc;IACd,MAAM;IACN,OAAO;IACP,SAAS;IACT,UAAU;IACV,KAAK;IACL,MAAM;IACN,KAAK;IACL,UAAU;CACb,CAAC,CAAC;AAEH,qDAAqD;AACrD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAC9B,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;CACT,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC3B,WAAW;IACX,WAAW;IACX,cAAc;IACd,MAAM;IACN,OAAO;IACP,KAAK;IACL,WAAW;CACd,CAAC,CAAC;AAEH,+EAA+E;AAC/E,MAAM,YAAY,GAAG,wEAAwE,CAAC;AAE9F,8DAA8D;AAC9D,MAAM,aAAa,GACf,yGAAyG,CAAC;AAE9G,+DAA+D;AAC/D,MAAM,YAAY,GAAG,+CAA+C,CAAC;AAErE,uFAAuF;AACvF,MAAM,SAAS,GACX,+EAA+E,CAAC;AAEpF;;;GAGG;AACH,MAAa,QAAQ;IACjB,YAAY,CAAc;IAC1B,YAAY,CAAc;IAE1B,YAAY,YAAyB,EAAE,YAAyB;QAC5D,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;IAED,4EAA4E;IAC5E,UAAU,CAAC,WAAmB;QAC1B,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACrF,CAAC;IAED,wFAAwF;IACxF,QAAQ,CAAC,WAAmB;QACxB,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACtF,CAAC;CACJ;AAlBD,4BAkBC;AAED,MAAa,eAAe;IACxB;;;OAGG;IACH,IAAI,CAAC,aAAqB;QACtB,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,IAAI,GAAG,EAAU,EAAE,IAAI,GAAG,EAAU,CAAC,CAAC;QACjE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC;QAC/C,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,OAAe;QACrB,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAE/C,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtC,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC;QAC7C,CAAC;QACD,IAAI,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7C,IAAI,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC9C,IAAI,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7C,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,SAAiB;QAC3B,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACxC,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACxE,IAAI,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC/C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC;YAClC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAEO,IAAI,CAAC,MAAc,EAAE,WAAmB,EAAE,KAAe;QAC7D,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAChE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC9C,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACtB,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,SAAS;gBACxC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;gBACvC,SAAS;YACb,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBAAE,SAAS;YAC9B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAAE,SAAS;YAC/D,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;QAC/C,CAAC;IACL,CAAC;IAEO,QAAQ,CAAC,OAAe,EAAE,WAAmB,EAAE,KAAe;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC;QACjF,KAAK,MAAM,WAAW,IAAI,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC5B,CAAC;IACL,CAAC;IAEO,mBAAmB,CAAC,MAAc;QACtC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC;QACxB,IAAI,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,OAAO,KAAK,KAAK,IAAI,EAAE,CAAC;YACpB,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACjD,IAAI,WAAW,KAAK,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAClD,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,QAAQ,CAAC,OAAe;QAC5B,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,gFAAgF;YAChF,gFAAgF;YAChF,OAAO,CAAC,IAAI,CAAC,kBAAkB,OAAO,4BAA4B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACnF,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;CACJ;AA/FD,0CA+FC","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"]}
@@ -88,16 +88,7 @@ function enrichGraph(graph, infos, workspaceRoot) {
88
88
  else if (roleResolution.role !== null) {
89
89
  entry.role = roleResolution.role;
90
90
  }
91
- // The runtime name clients address this app by β€” declared, never derived
92
- // (see service-name-resolver.ts). Most projects declare none.
93
- const serviceResolution = (0, service_name_resolver_1.resolveServiceName)(info, workspaceRoot);
94
- if (serviceResolution.problem !== null) {
95
- problems.push(serviceResolution.problem);
96
- }
97
- else if (serviceResolution.serviceName !== null) {
98
- entry.serviceName = serviceResolution.serviceName;
99
- serviceNames.set(projectName, serviceResolution.serviceName);
100
- }
91
+ enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);
101
92
  // Only persist the field when hidden (false); drawn projects (the
102
93
  // default) stay clean in dependencies.json with no drawOnGraph line.
103
94
  const drawResolution = (0, draw_on_graph_resolver_1.resolveDrawOnGraph)(info);
@@ -218,6 +209,31 @@ function validateRoleDependencies(graph, problems) {
218
209
  }
219
210
  }
220
211
  }
212
+ /**
213
+ * Fill the two symmetric client-addressing fields β€” `serviceName` (the name clients address THIS app
214
+ * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the
215
+ * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).
216
+ */
217
+ // webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities
218
+ function enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems) {
219
+ const serviceResolution = (0, service_name_resolver_1.resolveServiceName)(info, workspaceRoot);
220
+ if (serviceResolution.problem !== null) {
221
+ problems.push(serviceResolution.problem);
222
+ }
223
+ else if (serviceResolution.serviceName !== null) {
224
+ entry.serviceName = serviceResolution.serviceName;
225
+ serviceNames.set(projectName, serviceResolution.serviceName);
226
+ }
227
+ // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site
228
+ // literal and the fan-out fallback.
229
+ const callsResolution = (0, service_name_resolver_1.resolveCallsService)(info, workspaceRoot);
230
+ if (callsResolution.problem !== null) {
231
+ problems.push(callsResolution.problem);
232
+ }
233
+ else if (callsResolution.callsService !== null) {
234
+ entry.callsService = callsResolution.callsService;
235
+ }
236
+ }
221
237
  function enrichResponsibilities(entry, info, workspaceRoot, problems) {
222
238
  const responsibilitiesFile = toRepoRelative(info.root, exports.RESPONSIBILITIES_FILE_NAME);
223
239
  entry.responsibilitiesFile = responsibilitiesFile;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework β€” from `framework:<x>` nx tag or package.json inference\n * serviceName β€” from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription β€” first paragraph of the project's responsibilities.md\n * responsibilitiesFile β€” repo-relative path to the required responsibilities.md\n * designFile β€” repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // The runtime name clients address this app by β€” declared, never derived\n // (see service-name-resolver.ts). Most projects declare none.\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. Every\n // project.json project gets a design.json written, but plain libs get an\n // empty `{ designs: [] }` β€” those must NOT become clickable in the arch\n // viz (designHtmlHref keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * Roles that are terminal APPS β€” nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice β€” the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child β†’ parent: react β†’ browser, angular β†’ browser, express β†’ node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType β€” the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express). For a dependency edge Consumer C β†’ Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set β‰  βˆ… β€” i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] β€” the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angularβ†’browser, expressβ†’node). Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal β€” libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` β€” the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle β†’ app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps β€” it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) β€” ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} β€” create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design β€” a committed design.json\n * whose `designs[]` is non-empty (i.e. it has β‰₯1 @DocumentDesign root). Plain\n * libs get a `{ designs: [] }` file written, which must read as \"no design\" so\n * the arch viz does not render them as clickable. A missing/unparseable file is\n * treated as \"no design\".\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework β€” from `framework:<x>` nx tag or package.json inference\n * serviceName β€” from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription β€” first paragraph of the project's responsibilities.md\n * responsibilitiesFile β€” repo-relative path to the required responsibilities.md\n * designFile β€” repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. Every\n // project.json project gets a design.json written, but plain libs get an\n // empty `{ designs: [] }` β€” those must NOT become clickable in the arch\n // viz (designHtmlHref keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * Roles that are terminal APPS β€” nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice β€” the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child β†’ parent: react β†’ browser, angular β†’ browser, express β†’ node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType β€” the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express). For a dependency edge Consumer C β†’ Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set β‰  βˆ… β€” i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] β€” the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angularβ†’browser, expressβ†’node). Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal β€” libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` β€” the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle β†’ app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps β€” it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) β€” ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields β€” `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} β€” create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design β€” a committed design.json\n * whose `designs[]` is non-empty (i.e. it has β‰₯1 @DocumentDesign root). Plain\n * libs get a `{ designs: [] }` file written, which must read as \"no design\" so\n * the arch viz does not render them as clickable. A missing/unparseable file is\n * treated as \"no design\".\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
@@ -33,6 +33,15 @@ export interface GraphEntry {
33
33
  * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.
34
34
  */
35
35
  serviceName?: string;
36
+ /**
37
+ * The service(s) this project's clients call when the call site cannot carry a literal
38
+ * `ClientConfig` β€” the symmetric half of `serviceName`. DECLARED in project.json as
39
+ * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims
40
+ * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a
41
+ * project that never calls anything, or one whose call sites all carry literals.
42
+ * See service-name-resolver.ts.
43
+ */
44
+ callsService?: string | Record<string, string>;
36
45
  /**
37
46
  * When false, the project is hidden from the rendered architecture graphs
38
47
  * (its box AND every edge touching it are omitted from dependencies.html and
@@ -1 +1 @@
1
- {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType β€” the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists β€” the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) β€” its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
1
+ {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType β€” the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists β€” the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) β€” its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` β€” the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}