@webpieces/nx-webpieces-rules 0.3.235 → 0.3.237
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/executors.json +10 -0
- package/package.json +6 -6
- package/src/di-graph-targets.d.ts +17 -0
- package/src/di-graph-targets.js +41 -0
- package/src/di-graph-targets.js.map +1 -0
- package/src/executors/di-graph-generate/executor.d.ts +25 -0
- package/src/executors/di-graph-generate/executor.js +107 -0
- package/src/executors/di-graph-generate/executor.js.map +1 -0
- package/src/executors/di-graph-generate/schema.json +8 -0
- package/src/executors/validate-di-graph-unchanged/executor.d.ts +25 -0
- package/src/executors/validate-di-graph-unchanged/executor.js +84 -0
- package/src/executors/validate-di-graph-unchanged/executor.js.map +1 -0
- package/src/executors/validate-di-graph-unchanged/schema.json +8 -0
- package/src/executors/validate-nx-wiring/executor.js +1 -0
- package/src/executors/validate-nx-wiring/executor.js.map +1 -1
- package/src/lib/di-graph/analyzer.d.ts +22 -0
- package/src/lib/di-graph/analyzer.js +0 -0
- package/src/lib/di-graph/analyzer.js.map +1 -0
- package/src/lib/di-graph/bindings.d.ts +34 -0
- package/src/lib/di-graph/bindings.js +194 -0
- package/src/lib/di-graph/bindings.js.map +1 -0
- package/src/lib/di-graph/mermaid.d.ts +12 -0
- package/src/lib/di-graph/mermaid.js +80 -0
- package/src/lib/di-graph/mermaid.js.map +1 -0
- package/src/lib/di-graph/model.d.ts +78 -0
- package/src/lib/di-graph/model.js +114 -0
- package/src/lib/di-graph/model.js.map +1 -0
- package/src/lib/di-graph/program.d.ts +15 -0
- package/src/lib/di-graph/program.js +46 -0
- package/src/lib/di-graph/program.js.map +1 -0
- package/src/lib/di-graph/serializer.d.ts +12 -0
- package/src/lib/di-graph/serializer.js +0 -0
- package/src/lib/di-graph/serializer.js.map +1 -0
- package/src/lib/di-graph/token-resolver.d.ts +28 -0
- package/src/lib/di-graph/token-resolver.js +104 -0
- package/src/lib/di-graph/token-resolver.js.map +1 -0
- package/src/lib/graph-visualizer.js +3 -3
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/plugin.d.ts +1 -0
- package/src/plugin.js +31 -16
- package/src/plugin.js.map +1 -1
package/executors.json
CHANGED
|
@@ -140,6 +140,16 @@
|
|
|
140
140
|
"schema": "./src/executors/validate-eslint-sync/schema.json",
|
|
141
141
|
"description": "Validate that workspace and template eslint.webpieces.config.mjs files have identical rules"
|
|
142
142
|
},
|
|
143
|
+
"di-graph-generate": {
|
|
144
|
+
"implementation": "./src/executors/di-graph-generate/executor",
|
|
145
|
+
"schema": "./src/executors/di-graph-generate/schema.json",
|
|
146
|
+
"description": "Per-project: generate the Inversify DI dependency DAG into design.json + design.md at the project root"
|
|
147
|
+
},
|
|
148
|
+
"validate-di-graph-unchanged": {
|
|
149
|
+
"implementation": "./src/executors/validate-di-graph-unchanged/executor",
|
|
150
|
+
"schema": "./src/executors/validate-di-graph-unchanged/schema.json",
|
|
151
|
+
"description": "Per-project: fail when the regenerated design.json/design.md differ from the committed copies"
|
|
152
|
+
},
|
|
143
153
|
"validate-nx-wiring": {
|
|
144
154
|
"implementation": "./src/executors/validate-nx-wiring/executor",
|
|
145
155
|
"schema": "./src/executors/validate-nx-wiring/schema.json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.237",
|
|
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",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@webpieces/ai-hook-rules": "0.3.
|
|
22
|
-
"@webpieces/code-rules": "0.3.
|
|
23
|
-
"@webpieces/eslint-rules": "0.3.
|
|
24
|
-
"@webpieces/pr-gate": "0.3.
|
|
25
|
-
"@webpieces/rules-config": "0.3.
|
|
21
|
+
"@webpieces/ai-hook-rules": "0.3.237",
|
|
22
|
+
"@webpieces/code-rules": "0.3.237",
|
|
23
|
+
"@webpieces/eslint-rules": "0.3.237",
|
|
24
|
+
"@webpieces/pr-gate": "0.3.237",
|
|
25
|
+
"@webpieces/rules-config": "0.3.237",
|
|
26
26
|
"madge": "8.0.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project DI design graph targets (design.json + design.md), attached to every
|
|
3
|
+
* project.json project by the inference plugin. Split out of plugin.ts the same way
|
|
4
|
+
* as runtime-targets.ts.
|
|
5
|
+
*/
|
|
6
|
+
import type { TargetConfiguration } from '@nx/devkit';
|
|
7
|
+
/**
|
|
8
|
+
* Create per-project DI graph generation target. cache:false because the point
|
|
9
|
+
* is to regenerate design.json/design.md on EVERY build so the committed DI
|
|
10
|
+
* design DAG can never silently drift from the code.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createDiGraphGenerateTarget(): TargetConfiguration;
|
|
13
|
+
/**
|
|
14
|
+
* Create per-project DI graph staleness gate — regenerates first (dependsOn),
|
|
15
|
+
* then fails if the regenerated files differ from the committed copies.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createValidateDiGraphUnchangedTarget(): TargetConfiguration;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-project DI design graph targets (design.json + design.md), attached to every
|
|
4
|
+
* project.json project by the inference plugin. Split out of plugin.ts the same way
|
|
5
|
+
* as runtime-targets.ts.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createDiGraphGenerateTarget = createDiGraphGenerateTarget;
|
|
9
|
+
exports.createValidateDiGraphUnchangedTarget = createValidateDiGraphUnchangedTarget;
|
|
10
|
+
/**
|
|
11
|
+
* Create per-project DI graph generation target. cache:false because the point
|
|
12
|
+
* is to regenerate design.json/design.md on EVERY build so the committed DI
|
|
13
|
+
* design DAG can never silently drift from the code.
|
|
14
|
+
*/
|
|
15
|
+
function createDiGraphGenerateTarget() {
|
|
16
|
+
return {
|
|
17
|
+
executor: '@webpieces/nx-webpieces-rules:di-graph-generate',
|
|
18
|
+
cache: false,
|
|
19
|
+
outputs: ['{projectRoot}/design.json', '{projectRoot}/design.md'],
|
|
20
|
+
metadata: {
|
|
21
|
+
technologies: ['nx'],
|
|
22
|
+
description: 'Generate the Inversify DI dependency DAG into design.json + design.md',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create per-project DI graph staleness gate — regenerates first (dependsOn),
|
|
28
|
+
* then fails if the regenerated files differ from the committed copies.
|
|
29
|
+
*/
|
|
30
|
+
function createValidateDiGraphUnchangedTarget() {
|
|
31
|
+
return {
|
|
32
|
+
executor: '@webpieces/nx-webpieces-rules:validate-di-graph-unchanged',
|
|
33
|
+
cache: false, // Depends on git state
|
|
34
|
+
dependsOn: ['di-graph-generate'],
|
|
35
|
+
metadata: {
|
|
36
|
+
technologies: ['nx'],
|
|
37
|
+
description: 'Validate the committed design.json/design.md match the regenerated DI graph',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=di-graph-targets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"di-graph-targets.js","sourceRoot":"","sources":["../../../../../packages/tooling/nx-webpieces-rules/src/di-graph-targets.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AASH,kEAUC;AAMD,oFAUC;AA/BD;;;;GAIG;AACH,SAAgB,2BAA2B;IACvC,OAAO;QACH,QAAQ,EAAE,iDAAiD;QAC3D,KAAK,EAAE,KAAK;QACZ,OAAO,EAAE,CAAC,2BAA2B,EAAE,yBAAyB,CAAC;QACjE,QAAQ,EAAE;YACN,YAAY,EAAE,CAAC,IAAI,CAAC;YACpB,WAAW,EAAE,uEAAuE;SACvF;KACJ,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,SAAgB,oCAAoC;IAChD,OAAO;QACH,QAAQ,EAAE,2DAA2D;QACrE,KAAK,EAAE,KAAK,EAAE,uBAAuB;QACrC,SAAS,EAAE,CAAC,mBAAmB,CAAC;QAChC,QAAQ,EAAE;YACN,YAAY,EAAE,CAAC,IAAI,CAAC;YACpB,WAAW,EAAE,6EAA6E;SAC7F;KACJ,CAAC;AACN,CAAC","sourcesContent":["/**\n * Per-project DI design graph targets (design.json + design.md), attached to every\n * project.json project by the inference plugin. Split out of plugin.ts the same way\n * as runtime-targets.ts.\n */\n\nimport type { TargetConfiguration } from '@nx/devkit';\n\n/**\n * Create per-project DI graph generation target. cache:false because the point\n * is to regenerate design.json/design.md on EVERY build so the committed DI\n * design DAG can never silently drift from the code.\n */\nexport function createDiGraphGenerateTarget(): TargetConfiguration {\n return {\n executor: '@webpieces/nx-webpieces-rules:di-graph-generate',\n cache: false,\n outputs: ['{projectRoot}/design.json', '{projectRoot}/design.md'],\n metadata: {\n technologies: ['nx'],\n description: 'Generate the Inversify DI dependency DAG into design.json + design.md',\n },\n };\n}\n\n/**\n * Create per-project DI graph staleness gate — regenerates first (dependsOn),\n * then fails if the regenerated files differ from the committed copies.\n */\nexport function createValidateDiGraphUnchangedTarget(): TargetConfiguration {\n return {\n executor: '@webpieces/nx-webpieces-rules:validate-di-graph-unchanged',\n cache: false, // Depends on git state\n dependsOn: ['di-graph-generate'],\n metadata: {\n technologies: ['nx'],\n description: 'Validate the committed design.json/design.md match the regenerated DI graph',\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI Graph Generate Executor
|
|
3
|
+
*
|
|
4
|
+
* Per-project: statically analyzes the project's Inversify dependency DAG
|
|
5
|
+
* (constructor injection from controllers — or library top-of-DAG classes —
|
|
6
|
+
* down to leaves) and writes two checked-in files at the project root:
|
|
7
|
+
*
|
|
8
|
+
* design.json — machine-readable graph (deterministic, sorted)
|
|
9
|
+
* design.md — Mermaid diagram rendered by GitHub/IDEs in PRs
|
|
10
|
+
*
|
|
11
|
+
* Runs on every build (cache:false; the build gates on
|
|
12
|
+
* validate-di-graph-unchanged which dependsOn this target). Unrecognized DI
|
|
13
|
+
* patterns become "unresolved" nodes rather than failing the build.
|
|
14
|
+
*
|
|
15
|
+
* Config (webpieces.config.json, rule key `di-graph`): mode RUN_EVERY_TIME | OFF.
|
|
16
|
+
*
|
|
17
|
+
* Usage: nx run <project>:di-graph-generate
|
|
18
|
+
*/
|
|
19
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
20
|
+
export interface DiGraphGenerateOptions {
|
|
21
|
+
}
|
|
22
|
+
export interface ExecutorResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
}
|
|
25
|
+
export default function runExecutor(_options: DiGraphGenerateOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DI Graph Generate Executor
|
|
4
|
+
*
|
|
5
|
+
* Per-project: statically analyzes the project's Inversify dependency DAG
|
|
6
|
+
* (constructor injection from controllers — or library top-of-DAG classes —
|
|
7
|
+
* down to leaves) and writes two checked-in files at the project root:
|
|
8
|
+
*
|
|
9
|
+
* design.json — machine-readable graph (deterministic, sorted)
|
|
10
|
+
* design.md — Mermaid diagram rendered by GitHub/IDEs in PRs
|
|
11
|
+
*
|
|
12
|
+
* Runs on every build (cache:false; the build gates on
|
|
13
|
+
* validate-di-graph-unchanged which dependsOn this target). Unrecognized DI
|
|
14
|
+
* patterns become "unresolved" nodes rather than failing the build.
|
|
15
|
+
*
|
|
16
|
+
* Config (webpieces.config.json, rule key `di-graph`): mode RUN_EVERY_TIME | OFF.
|
|
17
|
+
*
|
|
18
|
+
* Usage: nx run <project>:di-graph-generate
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.default = runExecutor;
|
|
22
|
+
const tslib_1 = require("tslib");
|
|
23
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
24
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
25
|
+
const path = tslib_1.__importStar(require("path"));
|
|
26
|
+
const analyzer_1 = require("../../lib/di-graph/analyzer");
|
|
27
|
+
const program_1 = require("../../lib/di-graph/program");
|
|
28
|
+
const serializer_1 = require("../../lib/di-graph/serializer");
|
|
29
|
+
const mermaid_1 = require("../../lib/di-graph/mermaid");
|
|
30
|
+
const model_1 = require("../../lib/di-graph/model");
|
|
31
|
+
const toError_1 = require("../../toError");
|
|
32
|
+
const RULE_NAME = 'di-graph';
|
|
33
|
+
// Cheap substring pre-scan: a project whose source never mentions any DI marker
|
|
34
|
+
// gets an empty graph without paying for a ts.Program.
|
|
35
|
+
const DI_MARKERS = [
|
|
36
|
+
'@Controller(',
|
|
37
|
+
'@provideSingleton',
|
|
38
|
+
'@provideTransient',
|
|
39
|
+
'@injectable',
|
|
40
|
+
'new ContainerModule',
|
|
41
|
+
'@inject(',
|
|
42
|
+
];
|
|
43
|
+
function sourceHasDiMarkers(dir) {
|
|
44
|
+
if (!fs.existsSync(dir))
|
|
45
|
+
return false;
|
|
46
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
47
|
+
const full = path.join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
if (entry.name === 'node_modules' || entry.name === 'dist')
|
|
50
|
+
continue;
|
|
51
|
+
if (sourceHasDiMarkers(full))
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
55
|
+
const content = fs.readFileSync(full, 'utf-8');
|
|
56
|
+
if (DI_MARKERS.some((marker) => content.includes(marker)))
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function writeDesignFiles(projectRootAbs, graph) {
|
|
63
|
+
fs.writeFileSync(path.join(projectRootAbs, 'design.json'), (0, serializer_1.toDesignJson)(graph));
|
|
64
|
+
fs.writeFileSync(path.join(projectRootAbs, 'design.md'), (0, mermaid_1.toDesignMarkdown)(graph));
|
|
65
|
+
}
|
|
66
|
+
async function runExecutor(_options, context) {
|
|
67
|
+
const shared = (0, rules_config_1.loadAndValidate)(context.root).resolved;
|
|
68
|
+
const rule = shared.rules.get(RULE_NAME);
|
|
69
|
+
if (rule && rule.isOff) {
|
|
70
|
+
console.log(`\n⏭️ Skipping ${RULE_NAME} generation (mode: OFF)\n`);
|
|
71
|
+
return { success: true };
|
|
72
|
+
}
|
|
73
|
+
const projectName = context.projectName ?? 'project';
|
|
74
|
+
const projectConfig = context.projectsConfigurations?.projects[projectName];
|
|
75
|
+
const projectRoot = projectConfig?.root ?? '.';
|
|
76
|
+
const projectRootAbs = path.join(context.root, projectRoot);
|
|
77
|
+
console.log(`\n🧬 Generating DI design graph for ${projectName}\n`);
|
|
78
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- chokepoint: a generator crash must produce an actionable failure, not a stack trace mid-build
|
|
79
|
+
try {
|
|
80
|
+
if (!sourceHasDiMarkers(path.join(projectRootAbs, 'src'))) {
|
|
81
|
+
console.log(' No DI markers found — writing empty design graph');
|
|
82
|
+
writeDesignFiles(projectRootAbs, new model_1.DiGraph(projectName));
|
|
83
|
+
return { success: true };
|
|
84
|
+
}
|
|
85
|
+
const program = (0, program_1.createProjectProgram)(projectRootAbs);
|
|
86
|
+
if (!program) {
|
|
87
|
+
console.log(' No usable tsconfig/source — writing empty design graph');
|
|
88
|
+
writeDesignFiles(projectRootAbs, new model_1.DiGraph(projectName));
|
|
89
|
+
return { success: true };
|
|
90
|
+
}
|
|
91
|
+
const graph = (0, analyzer_1.buildDiGraph)(program, context.root, projectRoot, projectName);
|
|
92
|
+
writeDesignFiles(projectRootAbs, graph);
|
|
93
|
+
console.log(`✅ Wrote ${projectRoot}/design.json + design.md ` +
|
|
94
|
+
`(${graph.roots.length} root(s), ${graph.nodes.length} node(s), ${graph.edges.length} edge(s))`);
|
|
95
|
+
if (graph.unresolved.length > 0) {
|
|
96
|
+
console.warn(`⚠️ ${graph.unresolved.length} unresolved token(s)/type(s): ${graph.unresolved.join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
return { success: true };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const error = (0, toError_1.toError)(err);
|
|
102
|
+
console.error(`❌ DI graph generation failed for ${projectName}: ${error.message}`);
|
|
103
|
+
console.error(` To unblock builds, set rules["${RULE_NAME}"].mode="OFF" in webpieces.config.json.`);
|
|
104
|
+
return { success: false };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/di-graph-generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Graph Generate Executor\n *\n * Per-project: statically analyzes the project's Inversify dependency DAG\n * (constructor injection from controllers — or library top-of-DAG classes —\n * down to leaves) and writes two checked-in files at the project root:\n *\n * design.json — machine-readable graph (deterministic, sorted)\n * design.md — Mermaid diagram rendered by GitHub/IDEs in PRs\n *\n * Runs on every build (cache:false; the build gates on\n * validate-di-graph-unchanged which dependsOn this target). Unrecognized DI\n * patterns become \"unresolved\" nodes rather than failing the build.\n *\n * Config (webpieces.config.json, rule key `di-graph`): mode RUN_EVERY_TIME | OFF.\n *\n * Usage: nx run <project>:di-graph-generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { buildDiGraph } from '../../lib/di-graph/analyzer';\nimport { createProjectProgram } from '../../lib/di-graph/program';\nimport { toDesignJson } from '../../lib/di-graph/serializer';\nimport { toDesignMarkdown } from '../../lib/di-graph/mermaid';\nimport { DiGraph } from '../../lib/di-graph/model';\nimport { toError } from '../../toError';\n\nexport interface DiGraphGenerateOptions {\n // No options here — config comes from webpieces.config.json at runtime.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst RULE_NAME = 'di-graph';\n\n// Cheap substring pre-scan: a project whose source never mentions any DI marker\n// gets an empty graph without paying for a ts.Program.\nconst DI_MARKERS = [\n '@Controller(',\n '@provideSingleton',\n '@provideTransient',\n '@injectable',\n 'new ContainerModule',\n '@inject(',\n];\n\nfunction sourceHasDiMarkers(dir: string): boolean {\n if (!fs.existsSync(dir)) return false;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name === 'node_modules' || entry.name === 'dist') continue;\n if (sourceHasDiMarkers(full)) return true;\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n const content = fs.readFileSync(full, 'utf-8');\n if (DI_MARKERS.some((marker: string) => content.includes(marker))) return true;\n }\n }\n return false;\n}\n\nfunction writeDesignFiles(projectRootAbs: string, graph: DiGraph): void {\n fs.writeFileSync(path.join(projectRootAbs, 'design.json'), toDesignJson(graph));\n fs.writeFileSync(path.join(projectRootAbs, 'design.md'), toDesignMarkdown(graph));\n}\n\nexport default async function runExecutor(\n _options: DiGraphGenerateOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get(RULE_NAME);\n if (rule && rule.isOff) {\n console.log(`\\n⏭️ Skipping ${RULE_NAME} generation (mode: OFF)\\n`);\n return { success: true };\n }\n\n const projectName = context.projectName ?? 'project';\n const projectConfig = context.projectsConfigurations?.projects[projectName];\n const projectRoot = projectConfig?.root ?? '.';\n const projectRootAbs = path.join(context.root, projectRoot);\n\n console.log(`\\n🧬 Generating DI design graph for ${projectName}\\n`);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- chokepoint: a generator crash must produce an actionable failure, not a stack trace mid-build\n try {\n if (!sourceHasDiMarkers(path.join(projectRootAbs, 'src'))) {\n console.log(' No DI markers found — writing empty design graph');\n writeDesignFiles(projectRootAbs, new DiGraph(projectName));\n return { success: true };\n }\n\n const program = createProjectProgram(projectRootAbs);\n if (!program) {\n console.log(' No usable tsconfig/source — writing empty design graph');\n writeDesignFiles(projectRootAbs, new DiGraph(projectName));\n return { success: true };\n }\n\n const graph = buildDiGraph(program, context.root, projectRoot, projectName);\n writeDesignFiles(projectRootAbs, graph);\n\n console.log(\n `✅ Wrote ${projectRoot}/design.json + design.md ` +\n `(${graph.roots.length} root(s), ${graph.nodes.length} node(s), ${graph.edges.length} edge(s))`,\n );\n if (graph.unresolved.length > 0) {\n console.warn(`⚠️ ${graph.unresolved.length} unresolved token(s)/type(s): ${graph.unresolved.join(', ')}`);\n }\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`❌ DI graph generation failed for ${projectName}: ${error.message}`);\n console.error(` To unblock builds, set rules[\"${RULE_NAME}\"].mode=\"OFF\" in webpieces.config.json.`);\n return { success: false };\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"title": "DI Graph Generate Executor",
|
|
4
|
+
"description": "Generates the project's Inversify DI dependency DAG into design.json + design.md at the project root. On/off comes from webpieces.config.json (rule: di-graph).",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {},
|
|
7
|
+
"required": []
|
|
8
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate DI Graph Unchanged Executor
|
|
3
|
+
*
|
|
4
|
+
* Per-project gate that runs AFTER di-graph-generate (via target dependsOn):
|
|
5
|
+
* once the generator has rewritten design.json/design.md, this executor asks
|
|
6
|
+
* git whether the files differ from the committed copies. Any difference —
|
|
7
|
+
* modified OR brand-new/untracked — fails the build with a "commit the
|
|
8
|
+
* regenerated files" remediation, so the checked-in DI design DAG can never
|
|
9
|
+
* go stale on main.
|
|
10
|
+
*
|
|
11
|
+
* Uses `git status --porcelain` (not `git diff --quiet`) so first-time
|
|
12
|
+
* untracked design files also fail CI instead of slipping through.
|
|
13
|
+
*
|
|
14
|
+
* Config (webpieces.config.json, rule key `di-graph`): mode OFF disables;
|
|
15
|
+
* ignoreModifiedUntilEpoch / ignoreRuleWhileOnBranch report but pass.
|
|
16
|
+
*
|
|
17
|
+
* Usage: nx run <project>:validate-di-graph-unchanged
|
|
18
|
+
*/
|
|
19
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
20
|
+
export interface ValidateDiGraphUnchangedOptions {
|
|
21
|
+
}
|
|
22
|
+
export interface ExecutorResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
}
|
|
25
|
+
export default function runExecutor(_options: ValidateDiGraphUnchangedOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validate DI Graph Unchanged Executor
|
|
4
|
+
*
|
|
5
|
+
* Per-project gate that runs AFTER di-graph-generate (via target dependsOn):
|
|
6
|
+
* once the generator has rewritten design.json/design.md, this executor asks
|
|
7
|
+
* git whether the files differ from the committed copies. Any difference —
|
|
8
|
+
* modified OR brand-new/untracked — fails the build with a "commit the
|
|
9
|
+
* regenerated files" remediation, so the checked-in DI design DAG can never
|
|
10
|
+
* go stale on main.
|
|
11
|
+
*
|
|
12
|
+
* Uses `git status --porcelain` (not `git diff --quiet`) so first-time
|
|
13
|
+
* untracked design files also fail CI instead of slipping through.
|
|
14
|
+
*
|
|
15
|
+
* Config (webpieces.config.json, rule key `di-graph`): mode OFF disables;
|
|
16
|
+
* ignoreModifiedUntilEpoch / ignoreRuleWhileOnBranch report but pass.
|
|
17
|
+
*
|
|
18
|
+
* Usage: nx run <project>:validate-di-graph-unchanged
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.default = runExecutor;
|
|
22
|
+
const tslib_1 = require("tslib");
|
|
23
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
24
|
+
const child_process_1 = require("child_process");
|
|
25
|
+
const path = tslib_1.__importStar(require("path"));
|
|
26
|
+
const toError_1 = require("../../toError");
|
|
27
|
+
const RULE_NAME = 'di-graph';
|
|
28
|
+
const DESIGN_FILES = ['design.json', 'design.md'];
|
|
29
|
+
function gitStatusPorcelain(workspaceRoot, projectRoot) {
|
|
30
|
+
const paths = DESIGN_FILES.map((file) => path.posix.join(projectRoot, file));
|
|
31
|
+
const output = (0, child_process_1.execFileSync)('git', ['status', '--porcelain', '--', ...paths], {
|
|
32
|
+
cwd: workspaceRoot,
|
|
33
|
+
encoding: 'utf-8',
|
|
34
|
+
});
|
|
35
|
+
return output.trim();
|
|
36
|
+
}
|
|
37
|
+
function reportStale(projectName, projectRoot, status) {
|
|
38
|
+
console.error(`\n❌ DI design graph is stale for ${projectName}!`);
|
|
39
|
+
console.error('\nThe regenerated design files differ from the committed copies:');
|
|
40
|
+
for (const line of status.split('\n')) {
|
|
41
|
+
console.error(` ${line}`);
|
|
42
|
+
}
|
|
43
|
+
console.error('\nThe DI dependency DAG changed (a constructor/binding was added, removed,');
|
|
44
|
+
console.error('or rewired). To fix:');
|
|
45
|
+
console.error(` 1. Review the diff: git diff -- ${projectRoot}/design.json ${projectRoot}/design.md`);
|
|
46
|
+
console.error(' 2. If intentional, commit the regenerated design.json + design.md');
|
|
47
|
+
console.error(` 3. To turn this gate off, set rules["${RULE_NAME}"].mode="OFF" in webpieces.config.json\n`);
|
|
48
|
+
}
|
|
49
|
+
async function runExecutor(_options, context) {
|
|
50
|
+
const shared = (0, rules_config_1.loadAndValidate)(context.root).resolved;
|
|
51
|
+
const rule = shared.rules.get(RULE_NAME);
|
|
52
|
+
if (rule && rule.isOff) {
|
|
53
|
+
console.log(`\n⏭️ Skipping validate-di-graph-unchanged (mode: OFF)\n`);
|
|
54
|
+
return { success: true };
|
|
55
|
+
}
|
|
56
|
+
const projectName = context.projectName ?? 'project';
|
|
57
|
+
const projectConfig = context.projectsConfigurations?.projects[projectName];
|
|
58
|
+
const projectRoot = projectConfig?.root ?? '.';
|
|
59
|
+
console.log(`\n🔍 Validating DI design graph unchanged for ${projectName}\n`);
|
|
60
|
+
let status;
|
|
61
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- chokepoint: outside a git repo the gate warns and passes instead of crashing the build
|
|
62
|
+
try {
|
|
63
|
+
status = gitStatusPorcelain(context.root, projectRoot);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const error = (0, toError_1.toError)(err);
|
|
67
|
+
console.warn(`⚠️ Could not check git status (${error.message}) — skipping gate`);
|
|
68
|
+
return { success: true };
|
|
69
|
+
}
|
|
70
|
+
if (status === '') {
|
|
71
|
+
console.log('✅ DI design graph matches the committed design.json/design.md\n');
|
|
72
|
+
return { success: true };
|
|
73
|
+
}
|
|
74
|
+
reportStale(projectName, projectRoot, status);
|
|
75
|
+
const epoch = rule?.options['ignoreModifiedUntilEpoch'];
|
|
76
|
+
const branch = rule?.options['ignoreRuleWhileOnBranch'];
|
|
77
|
+
const skip = (0, rules_config_1.shouldSkipRule)(epoch, branch);
|
|
78
|
+
if (skip.skip) {
|
|
79
|
+
console.log(`⏳ ${RULE_NAME}: ${skip.reason}. Staleness reported but NOT failing the build.\n`);
|
|
80
|
+
return { success: true };
|
|
81
|
+
}
|
|
82
|
+
return { success: false };
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-di-graph-unchanged/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate DI Graph Unchanged Executor\n *\n * Per-project gate that runs AFTER di-graph-generate (via target dependsOn):\n * once the generator has rewritten design.json/design.md, this executor asks\n * git whether the files differ from the committed copies. Any difference —\n * modified OR brand-new/untracked — fails the build with a \"commit the\n * regenerated files\" remediation, so the checked-in DI design DAG can never\n * go stale on main.\n *\n * Uses `git status --porcelain` (not `git diff --quiet`) so first-time\n * untracked design files also fail CI instead of slipping through.\n *\n * Config (webpieces.config.json, rule key `di-graph`): mode OFF disables;\n * ignoreModifiedUntilEpoch / ignoreRuleWhileOnBranch report but pass.\n *\n * Usage: nx run <project>:validate-di-graph-unchanged\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate, shouldSkipRule } from '@webpieces/rules-config';\nimport { execFileSync } from 'child_process';\nimport * as path from 'path';\nimport { toError } from '../../toError';\n\nexport interface ValidateDiGraphUnchangedOptions {\n // No options here — config comes from webpieces.config.json at runtime.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst RULE_NAME = 'di-graph';\nconst DESIGN_FILES = ['design.json', 'design.md'];\n\nfunction gitStatusPorcelain(workspaceRoot: string, projectRoot: string): string {\n const paths = DESIGN_FILES.map((file: string) => path.posix.join(projectRoot, file));\n const output = execFileSync('git', ['status', '--porcelain', '--', ...paths], {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n });\n return output.trim();\n}\n\nfunction reportStale(projectName: string, projectRoot: string, status: string): void {\n console.error(`\\n❌ DI design graph is stale for ${projectName}!`);\n console.error('\\nThe regenerated design files differ from the committed copies:');\n for (const line of status.split('\\n')) {\n console.error(` ${line}`);\n }\n console.error('\\nThe DI dependency DAG changed (a constructor/binding was added, removed,');\n console.error('or rewired). To fix:');\n console.error(` 1. Review the diff: git diff -- ${projectRoot}/design.json ${projectRoot}/design.md`);\n console.error(' 2. If intentional, commit the regenerated design.json + design.md');\n console.error(` 3. To turn this gate off, set rules[\"${RULE_NAME}\"].mode=\"OFF\" in webpieces.config.json\\n`);\n}\n\nexport default async function runExecutor(\n _options: ValidateDiGraphUnchangedOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get(RULE_NAME);\n if (rule && rule.isOff) {\n console.log(`\\n⏭️ Skipping validate-di-graph-unchanged (mode: OFF)\\n`);\n return { success: true };\n }\n\n const projectName = context.projectName ?? 'project';\n const projectConfig = context.projectsConfigurations?.projects[projectName];\n const projectRoot = projectConfig?.root ?? '.';\n\n console.log(`\\n🔍 Validating DI design graph unchanged for ${projectName}\\n`);\n\n let status: string;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- chokepoint: outside a git repo the gate warns and passes instead of crashing the build\n try {\n status = gitStatusPorcelain(context.root, projectRoot);\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not check git status (${error.message}) — skipping gate`);\n return { success: true };\n }\n\n if (status === '') {\n console.log('✅ DI design graph matches the committed design.json/design.md\\n');\n return { success: true };\n }\n\n reportStale(projectName, projectRoot, status);\n\n const epoch = rule?.options['ignoreModifiedUntilEpoch'] as number | undefined;\n const branch = rule?.options['ignoreRuleWhileOnBranch'] as string | undefined;\n const skip = shouldSkipRule(epoch, branch);\n if (skip.skip) {\n console.log(`⏳ ${RULE_NAME}: ${skip.reason}. Staleness reported but NOT failing the build.\\n`);\n return { success: true };\n }\n return { success: false };\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"title": "Validate DI Graph Unchanged Executor",
|
|
4
|
+
"description": "Fails when the regenerated design.json/design.md differ from the committed copies (runs after di-graph-generate). On/off and grace window come from webpieces.config.json (rule: di-graph).",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {},
|
|
7
|
+
"required": []
|
|
8
|
+
}
|
|
@@ -34,6 +34,7 @@ const path = tslib_1.__importStar(require("path"));
|
|
|
34
34
|
const DEFAULT_REQUIRED_DEPS = [
|
|
35
35
|
'architecture:validate-complete',
|
|
36
36
|
'validate-no-file-import-cycles',
|
|
37
|
+
'validate-di-graph-unchanged',
|
|
37
38
|
];
|
|
38
39
|
// Per-project build targets must ALSO keep `^build` so build order follows nx's full graph.
|
|
39
40
|
const BUILD_ORDER_DEP = '^build';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-nx-wiring/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;AA8LH,8BA4CC;;AAnOD,uCAAgG;AAChG,0DAA0D;AAC1D,+CAAyB;AACzB,mDAA6B;AAW7B,MAAM,qBAAqB,GAAa;IACpC,gCAAgC;IAChC,gCAAgC;CACnC,CAAC;AAEF,4FAA4F;AAC5F,MAAM,eAAe,GAAG,QAAQ,CAAC;AAEjC,MAAM,yBAAyB,GAAa;IACxC,YAAY;IACZ,4BAA4B;CAC/B,CAAC;AAEF,MAAM,aAAa;IACf,YAAY,CAAS;IACrB,OAAO,CAAW;IAClB,KAAK,CAAW;IAEhB,YAAY,YAAoB,EAAE,OAAiB,EAAE,KAAe;QAChE,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAED,MAAM,kBAAkB;IACpB,OAAO,CAAS;IAChB,YAAY,CAAS;IACrB,OAAO,CAAW;IAClB,KAAK,CAAW;IAEhB,YAAY,OAAe,EAAE,YAAoB,EAAE,OAAiB,EAAE,KAAe;QACjF,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAUD,SAAS,kBAAkB,CAAC,aAAqB;IAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,EAAE,CAAC;IAC1C,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAc,CAAC;QAC5C,OAAO,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;QACnC,8GAA8G;IAClH,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6DAA6D;QAC7D,KAAK,GAAG,CAAC;QACT,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC;AAED,SAAS,yBAAyB,CAC9B,cAAsC,EACtC,gBAA0B;IAE1B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;QAClC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1C,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC;YACrC,IAAI,YAAY,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC1C,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YAC5B,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,YAAY,CACjB,iBAA2B,EAC3B,cAAkD,EAClD,YAAsB;IAEtB,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,YAAY,IAAI,iBAAiB,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,QAAQ,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,YAAY,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QACvE,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,IAAyC;IAC5D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACpB,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC;QACxC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC;QAChC,OAAO,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IACpD,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;GAGG;AACH,SAAS,uBAAuB,CAC5B,cAAsC,EACtC,gBAA6B,EAC7B,kBAA4B;IAE5B,MAAM,QAAQ,GAAyB,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChE,uFAAuF;QACvF,wFAAwF;QACxF,8EAA8E;QAC9E,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,SAAS;QACxD,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;YAAE,SAAS;QACjF,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,QAAQ,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QACpF,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED,SAAS,aAAa,CAAC,QAAyB,EAAE,YAAsB;IACpE,OAAO,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACrF,OAAO,CAAC,KAAK,CAAC,uEAAuE,CAAC,CAAC;IACvF,OAAO,CAAC,KAAK,CAAC,gEAAgE,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChF,OAAO,CAAC,KAAK,CAAC,MAAM,OAAO,CAAC,YAAY,MAAM,CAAC,CAAC;QAChD,OAAO,CAAC,KAAK,CAAC,uBAAuB,QAAQ,aAAa,CAAC,CAAC;QAC5D,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,KAAK,CAAC,gBAAgB,WAAW,IAAI,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACrF,OAAO,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,wBAAwB,CAAC,QAA8B;IAC5D,OAAO,CAAC,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC1F,OAAO,CAAC,KAAK,CAAC,+EAA+E,CAAC,CAAC;IAC/F,OAAO,CAAC,KAAK,CAAC,6EAA6E,CAAC,CAAC;IAC7F,OAAO,CAAC,KAAK,CAAC,8EAA8E,CAAC,CAAC;IAC9F,OAAO,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IACxC,OAAO,CAAC,KAAK,CAAC,6EAA6E,CAAC,CAAC;IAC7F,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;IACtE,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACvB,MAAM,WAAW,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC;QACrD,OAAO,CAAC,KAAK,CAAC,iCAAiC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1E,OAAO,CAAC,KAAK,CAAC,gBAAgB,WAAW,IAAI,CAAC,CAAC;IACnD,CAAC;AACL,CAAC;AAEc,KAAK,UAAU,WAAW,CACrC,OAAgC,EAChC,OAAwB;IAExB,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;QAC/D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACnE,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,yBAAyB,CAAC;IAE/E,OAAO,CAAC,GAAG,CAAC,iEAAiE,CAAC,CAAC;IAE/E,MAAM,YAAY,GAAG,MAAM,IAAA,gCAAuB,GAAE,CAAC;IACrD,MAAM,cAAc,GAAG,IAAA,kDAAyC,EAAC,YAAY,CAAC,CAAC;IAE/E,MAAM,KAAK,GAAG,yBAAyB,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAC;IAC1E,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,YAAoB,EAAE,EAAE,CACvE,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAC1B,CAAC;IAEF,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;QACvE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,cAAc,GAAG,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,MAAM,cAAc,GAAG,YAAY,CAAC,iBAAiB,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;IACrF,MAAM,YAAY,GAAG,uBAAuB,CAAC,cAAc,EAAE,KAAK,EAAE;QAChE,GAAG,YAAY;QACf,eAAe;KAClB,CAAC,CAAC;IAEH,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,OAAO,CAAC,GAAG,CAAC,0EAA0E,CAAC,CAAC;QACxF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC;QAAE,aAAa,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAC3E,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;QAAE,wBAAwB,CAAC,YAAY,CAAC,CAAC;IACpE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC","sourcesContent":["/**\n * Validate Nx Wiring Executor\n *\n * Enforces that the webpieces validators are actually wired into the build. Two checks:\n *\n * 1. nx.json targetDefaults — the compile executors (@nx/js:tsc, @angular/build:application)\n * must carry the load-bearing dependsOn:\n * [\"architecture:validate-complete\", \"validate-no-file-import-cycles\", \"^build\"]\n *\n * 2. Per-project override guard — a project that declares its OWN build.dependsOn OVERRIDES\n * the targetDefaults entirely (nx does not merge dependsOn). That silently drops `^build`\n * (→ upstream libs not built first, builds against an empty dist) AND the validator gates\n * (→ the project builds green while validating nothing). So we check each compile project's\n * RESOLVED build.dependsOn and fail if it is missing any required entry.\n *\n * If either is stripped, validators exist but never run and the build stays green while\n * validating nothing. This executor fails when the wiring is missing.\n *\n * Conservative by design: only REQUIRES wiring on compile executors actually in use\n * (@nx/js:tsc, @angular/build:application). A repo that uses neither passes.\n *\n * Disable via webpieces.config.json rules[\"nx-wiring\"].mode=\"OFF\".\n *\n * Usage: nx run architecture:validate-nx-wiring\n */\n\nimport type {\n ExecutorContext,\n ProjectsConfigurations,\n TargetDependencyConfig,\n} from '@nx/devkit';\nimport { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph } from '@nx/devkit';\nimport { loadAndValidate } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nexport interface ValidateNxWiringOptions {\n requiredDeps?: string[];\n compileExecutors?: string[];\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst DEFAULT_REQUIRED_DEPS: string[] = [\n 'architecture:validate-complete',\n 'validate-no-file-import-cycles',\n];\n\n// Per-project build targets must ALSO keep `^build` so build order follows nx's full graph.\nconst BUILD_ORDER_DEP = '^build';\n\nconst DEFAULT_COMPILE_EXECUTORS: string[] = [\n '@nx/js:tsc',\n '@angular/build:application',\n];\n\nclass WiringProblem {\n executorName: string;\n missing: string[];\n found: string[];\n\n constructor(executorName: string, missing: string[], found: string[]) {\n this.executorName = executorName;\n this.missing = missing;\n this.found = found;\n }\n}\n\nclass ProjectGateProblem {\n project: string;\n executorName: string;\n missing: string[];\n found: string[];\n\n constructor(project: string, executorName: string, missing: string[], found: string[]) {\n this.project = project;\n this.executorName = executorName;\n this.missing = missing;\n this.found = found;\n }\n}\n\ninterface TargetDefaultEntry {\n dependsOn?: string[];\n}\n\ninterface RawNxJson {\n targetDefaults?: Record<string, TargetDefaultEntry>;\n}\n\nfunction readTargetDefaults(workspaceRoot: string): Record<string, TargetDefaultEntry> {\n const nxJsonPath = path.join(workspaceRoot, 'nx.json');\n if (!fs.existsSync(nxJsonPath)) return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = fs.readFileSync(nxJsonPath, 'utf8');\n const parsed = JSON.parse(raw) as RawNxJson;\n return parsed.targetDefaults ?? {};\n // webpieces-disable catch-error-pattern -- malformed nx.json fails open so the check does not crash the build\n } catch (err: unknown) {\n //const error = toError(err); -- malformed nx.json fails open\n void err;\n return {};\n }\n}\n\nfunction findCompileExecutorsInUse(\n projectsConfig: ProjectsConfigurations,\n compileExecutors: string[],\n): Set<string> {\n const known = new Set(compileExecutors);\n const inUse = new Set<string>();\n for (const cfg of Object.values(projectsConfig.projects)) {\n const targets = cfg.targets ?? {};\n for (const target of Object.values(targets)) {\n const executorName = target.executor;\n if (executorName && known.has(executorName)) {\n inUse.add(executorName);\n }\n }\n }\n return inUse;\n}\n\nfunction findProblems(\n relevantExecutors: string[],\n targetDefaults: Record<string, TargetDefaultEntry>,\n requiredDeps: string[],\n): WiringProblem[] {\n const problems: WiringProblem[] = [];\n for (const executorName of relevantExecutors) {\n const entry = targetDefaults[executorName];\n const dependsOn = entry?.dependsOn ?? [];\n const missing = requiredDeps.filter((dep: string) => !dependsOn.includes(dep));\n if (missing.length > 0) {\n problems.push(new WiringProblem(executorName, missing, dependsOn));\n }\n }\n return problems;\n}\n\n/**\n * Normalize an nx dependsOn entry to a comparable string. The string form (\"^build\",\n * \"validate-complete\") passes through; the object form is rendered the same way\n * ({target:\"build\", dependencies:true} → \"^build\", {target:\"build\"} → \"build\").\n */\nfunction normalizeDeps(deps: (string | TargetDependencyConfig)[]): string[] {\n return deps.map((dep) => {\n if (typeof dep === 'string') return dep;\n const target = dep.target ?? '';\n return dep.dependencies ? `^${target}` : target;\n });\n}\n\n/**\n * Check each compile project's RESOLVED build.dependsOn (targetDefaults already merged in)\n * for the required gates + `^build`. A project that overrode them away is flagged.\n */\nfunction findProjectGateProblems(\n projectsConfig: ProjectsConfigurations,\n compileExecutors: Set<string>,\n requiredPerProject: string[],\n): ProjectGateProblem[] {\n const problems: ProjectGateProblem[] = [];\n for (const [name, cfg] of Object.entries(projectsConfig.projects)) {\n // Tooling/foundation packages (type:tooling) BUILD the validators, so gating their own\n // build on validate-complete would create a bootstrap cycle. They are exempt: they keep\n // build-order (\"^build\") only. Application/library code is still fully gated.\n if ((cfg.tags ?? []).includes('type:tooling')) continue;\n const build = cfg.targets?.['build'];\n if (!build || !build.executor || !compileExecutors.has(build.executor)) continue;\n const dependsOn = normalizeDeps(build.dependsOn ?? []);\n const missing = requiredPerProject.filter((dep) => !dependsOn.includes(dep));\n if (missing.length > 0) {\n problems.push(new ProjectGateProblem(name, build.executor, missing, dependsOn));\n }\n }\n return problems;\n}\n\nfunction reportFailure(problems: WiringProblem[], requiredDeps: string[]): void {\n console.error('\\n❌ webpieces validators are not wired into your build (nx.json).\\n');\n console.error('The validators exist but no build depends on them, so they never run.');\n console.error('Add the missing dependsOn entries to nx.json targetDefaults:\\n');\n const depsList = requiredDeps.map((dep: string) => `\"${dep}\"`).join(', ');\n for (const problem of problems) {\n const missingList = problem.missing.map((dep: string) => `\"${dep}\"`).join(', ');\n console.error(` \"${problem.executorName}\": {`);\n console.error(` \"dependsOn\": [${depsList}, \"^build\"]`);\n console.error(' }');\n console.error(` missing: ${missingList}\\n`);\n }\n console.error('To turn this wiring check off, set rules[\"nx-wiring\"].mode=\"OFF\" in');\n console.error('webpieces.config.json.\\n');\n}\n\nfunction reportProjectGateFailure(problems: ProjectGateProblem[]): void {\n console.error('\\n❌ Some projects override away the build gates in their project.json.\\n');\n console.error('A project-level build.dependsOn OVERRIDES nx.json targetDefaults (nx does not');\n console.error('merge dependsOn). That silently drops \"^build\" (upstreams not built first →');\n console.error('builds against an empty dist) and/or the validators (build stays green while');\n console.error('validating nothing).\\n');\n console.error('Fix: remove build.dependsOn from these project.json files so targetDefaults');\n console.error('apply, OR include all required entries explicitly:\\n');\n for (const p of problems) {\n const missingList = p.missing.map((dep) => `\"${dep}\"`).join(', ');\n console.error(` ${p.project} (${p.executorName}):`);\n console.error(` resolved build.dependsOn: ${JSON.stringify(p.found)}`);\n console.error(` missing: ${missingList}\\n`);\n }\n}\n\nexport default async function runExecutor(\n options: ValidateNxWiringOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get('nx-wiring');\n if (rule && rule.isOff) {\n console.log('\\n⏭️ Skipping validate-nx-wiring (mode: OFF)\\n');\n return { success: true };\n }\n\n const requiredDeps = options.requiredDeps ?? DEFAULT_REQUIRED_DEPS;\n const compileExecutors = options.compileExecutors ?? DEFAULT_COMPILE_EXECUTORS;\n\n console.log('\\n🔌 Validating webpieces validators are wired into the build\\n');\n\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n const inUse = findCompileExecutorsInUse(projectsConfig, compileExecutors);\n const relevantExecutors = compileExecutors.filter((executorName: string) =>\n inUse.has(executorName),\n );\n\n if (relevantExecutors.length === 0) {\n console.log('✅ No known compile executors in use — nothing to gate\\n');\n return { success: true };\n }\n\n const targetDefaults = readTargetDefaults(context.root);\n const wiringProblems = findProblems(relevantExecutors, targetDefaults, requiredDeps);\n const gateProblems = findProjectGateProblems(projectsConfig, inUse, [\n ...requiredDeps,\n BUILD_ORDER_DEP,\n ]);\n\n if (wiringProblems.length === 0 && gateProblems.length === 0) {\n console.log('✅ Validators are wired into the build (targetDefaults + every project)\\n');\n return { success: true };\n }\n\n if (wiringProblems.length > 0) reportFailure(wiringProblems, requiredDeps);\n if (gateProblems.length > 0) reportProjectGateFailure(gateProblems);\n return { success: false };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-nx-wiring/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Nx Wiring Executor\n *\n * Enforces that the webpieces validators are actually wired into the build. Two checks:\n *\n * 1. nx.json targetDefaults — the compile executors (@nx/js:tsc, @angular/build:application)\n * must carry the load-bearing dependsOn:\n * [\"architecture:validate-complete\", \"validate-no-file-import-cycles\", \"^build\"]\n *\n * 2. Per-project override guard — a project that declares its OWN build.dependsOn OVERRIDES\n * the targetDefaults entirely (nx does not merge dependsOn). That silently drops `^build`\n * (→ upstream libs not built first, builds against an empty dist) AND the validator gates\n * (→ the project builds green while validating nothing). So we check each compile project's\n * RESOLVED build.dependsOn and fail if it is missing any required entry.\n *\n * If either is stripped, validators exist but never run and the build stays green while\n * validating nothing. This executor fails when the wiring is missing.\n *\n * Conservative by design: only REQUIRES wiring on compile executors actually in use\n * (@nx/js:tsc, @angular/build:application). A repo that uses neither passes.\n *\n * Disable via webpieces.config.json rules[\"nx-wiring\"].mode=\"OFF\".\n *\n * Usage: nx run architecture:validate-nx-wiring\n */\n\nimport type {\n ExecutorContext,\n ProjectsConfigurations,\n TargetDependencyConfig,\n} from '@nx/devkit';\nimport { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph } from '@nx/devkit';\nimport { loadAndValidate } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nexport interface ValidateNxWiringOptions {\n requiredDeps?: string[];\n compileExecutors?: string[];\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst DEFAULT_REQUIRED_DEPS: string[] = [\n 'architecture:validate-complete',\n 'validate-no-file-import-cycles',\n 'validate-di-graph-unchanged',\n];\n\n// Per-project build targets must ALSO keep `^build` so build order follows nx's full graph.\nconst BUILD_ORDER_DEP = '^build';\n\nconst DEFAULT_COMPILE_EXECUTORS: string[] = [\n '@nx/js:tsc',\n '@angular/build:application',\n];\n\nclass WiringProblem {\n executorName: string;\n missing: string[];\n found: string[];\n\n constructor(executorName: string, missing: string[], found: string[]) {\n this.executorName = executorName;\n this.missing = missing;\n this.found = found;\n }\n}\n\nclass ProjectGateProblem {\n project: string;\n executorName: string;\n missing: string[];\n found: string[];\n\n constructor(project: string, executorName: string, missing: string[], found: string[]) {\n this.project = project;\n this.executorName = executorName;\n this.missing = missing;\n this.found = found;\n }\n}\n\ninterface TargetDefaultEntry {\n dependsOn?: string[];\n}\n\ninterface RawNxJson {\n targetDefaults?: Record<string, TargetDefaultEntry>;\n}\n\nfunction readTargetDefaults(workspaceRoot: string): Record<string, TargetDefaultEntry> {\n const nxJsonPath = path.join(workspaceRoot, 'nx.json');\n if (!fs.existsSync(nxJsonPath)) return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = fs.readFileSync(nxJsonPath, 'utf8');\n const parsed = JSON.parse(raw) as RawNxJson;\n return parsed.targetDefaults ?? {};\n // webpieces-disable catch-error-pattern -- malformed nx.json fails open so the check does not crash the build\n } catch (err: unknown) {\n //const error = toError(err); -- malformed nx.json fails open\n void err;\n return {};\n }\n}\n\nfunction findCompileExecutorsInUse(\n projectsConfig: ProjectsConfigurations,\n compileExecutors: string[],\n): Set<string> {\n const known = new Set(compileExecutors);\n const inUse = new Set<string>();\n for (const cfg of Object.values(projectsConfig.projects)) {\n const targets = cfg.targets ?? {};\n for (const target of Object.values(targets)) {\n const executorName = target.executor;\n if (executorName && known.has(executorName)) {\n inUse.add(executorName);\n }\n }\n }\n return inUse;\n}\n\nfunction findProblems(\n relevantExecutors: string[],\n targetDefaults: Record<string, TargetDefaultEntry>,\n requiredDeps: string[],\n): WiringProblem[] {\n const problems: WiringProblem[] = [];\n for (const executorName of relevantExecutors) {\n const entry = targetDefaults[executorName];\n const dependsOn = entry?.dependsOn ?? [];\n const missing = requiredDeps.filter((dep: string) => !dependsOn.includes(dep));\n if (missing.length > 0) {\n problems.push(new WiringProblem(executorName, missing, dependsOn));\n }\n }\n return problems;\n}\n\n/**\n * Normalize an nx dependsOn entry to a comparable string. The string form (\"^build\",\n * \"validate-complete\") passes through; the object form is rendered the same way\n * ({target:\"build\", dependencies:true} → \"^build\", {target:\"build\"} → \"build\").\n */\nfunction normalizeDeps(deps: (string | TargetDependencyConfig)[]): string[] {\n return deps.map((dep) => {\n if (typeof dep === 'string') return dep;\n const target = dep.target ?? '';\n return dep.dependencies ? `^${target}` : target;\n });\n}\n\n/**\n * Check each compile project's RESOLVED build.dependsOn (targetDefaults already merged in)\n * for the required gates + `^build`. A project that overrode them away is flagged.\n */\nfunction findProjectGateProblems(\n projectsConfig: ProjectsConfigurations,\n compileExecutors: Set<string>,\n requiredPerProject: string[],\n): ProjectGateProblem[] {\n const problems: ProjectGateProblem[] = [];\n for (const [name, cfg] of Object.entries(projectsConfig.projects)) {\n // Tooling/foundation packages (type:tooling) BUILD the validators, so gating their own\n // build on validate-complete would create a bootstrap cycle. They are exempt: they keep\n // build-order (\"^build\") only. Application/library code is still fully gated.\n if ((cfg.tags ?? []).includes('type:tooling')) continue;\n const build = cfg.targets?.['build'];\n if (!build || !build.executor || !compileExecutors.has(build.executor)) continue;\n const dependsOn = normalizeDeps(build.dependsOn ?? []);\n const missing = requiredPerProject.filter((dep) => !dependsOn.includes(dep));\n if (missing.length > 0) {\n problems.push(new ProjectGateProblem(name, build.executor, missing, dependsOn));\n }\n }\n return problems;\n}\n\nfunction reportFailure(problems: WiringProblem[], requiredDeps: string[]): void {\n console.error('\\n❌ webpieces validators are not wired into your build (nx.json).\\n');\n console.error('The validators exist but no build depends on them, so they never run.');\n console.error('Add the missing dependsOn entries to nx.json targetDefaults:\\n');\n const depsList = requiredDeps.map((dep: string) => `\"${dep}\"`).join(', ');\n for (const problem of problems) {\n const missingList = problem.missing.map((dep: string) => `\"${dep}\"`).join(', ');\n console.error(` \"${problem.executorName}\": {`);\n console.error(` \"dependsOn\": [${depsList}, \"^build\"]`);\n console.error(' }');\n console.error(` missing: ${missingList}\\n`);\n }\n console.error('To turn this wiring check off, set rules[\"nx-wiring\"].mode=\"OFF\" in');\n console.error('webpieces.config.json.\\n');\n}\n\nfunction reportProjectGateFailure(problems: ProjectGateProblem[]): void {\n console.error('\\n❌ Some projects override away the build gates in their project.json.\\n');\n console.error('A project-level build.dependsOn OVERRIDES nx.json targetDefaults (nx does not');\n console.error('merge dependsOn). That silently drops \"^build\" (upstreams not built first →');\n console.error('builds against an empty dist) and/or the validators (build stays green while');\n console.error('validating nothing).\\n');\n console.error('Fix: remove build.dependsOn from these project.json files so targetDefaults');\n console.error('apply, OR include all required entries explicitly:\\n');\n for (const p of problems) {\n const missingList = p.missing.map((dep) => `\"${dep}\"`).join(', ');\n console.error(` ${p.project} (${p.executorName}):`);\n console.error(` resolved build.dependsOn: ${JSON.stringify(p.found)}`);\n console.error(` missing: ${missingList}\\n`);\n }\n}\n\nexport default async function runExecutor(\n options: ValidateNxWiringOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get('nx-wiring');\n if (rule && rule.isOff) {\n console.log('\\n⏭️ Skipping validate-nx-wiring (mode: OFF)\\n');\n return { success: true };\n }\n\n const requiredDeps = options.requiredDeps ?? DEFAULT_REQUIRED_DEPS;\n const compileExecutors = options.compileExecutors ?? DEFAULT_COMPILE_EXECUTORS;\n\n console.log('\\n🔌 Validating webpieces validators are wired into the build\\n');\n\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n const inUse = findCompileExecutorsInUse(projectsConfig, compileExecutors);\n const relevantExecutors = compileExecutors.filter((executorName: string) =>\n inUse.has(executorName),\n );\n\n if (relevantExecutors.length === 0) {\n console.log('✅ No known compile executors in use — nothing to gate\\n');\n return { success: true };\n }\n\n const targetDefaults = readTargetDefaults(context.root);\n const wiringProblems = findProblems(relevantExecutors, targetDefaults, requiredDeps);\n const gateProblems = findProjectGateProblems(projectsConfig, inUse, [\n ...requiredDeps,\n BUILD_ORDER_DEP,\n ]);\n\n if (wiringProblems.length === 0 && gateProblems.length === 0) {\n console.log('✅ Validators are wired into the build (targetDefaults + every project)\\n');\n return { success: true };\n }\n\n if (wiringProblems.length > 0) reportFailure(wiringProblems, requiredDeps);\n if (gateProblems.length > 0) reportProjectGateFailure(gateProblems);\n return { success: false };\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI Graph Analyzer (pass 2)
|
|
3
|
+
*
|
|
4
|
+
* Walks constructor injection from a project's root classes down to leaves:
|
|
5
|
+
*
|
|
6
|
+
* - @inject(TOKEN) params → token lookup in the binding table → bound impl
|
|
7
|
+
* - @multiInject(TOKEN) → fan-out edge to EVERY binding of that token
|
|
8
|
+
* - bare typed params → checker resolves the type to a class (inject-by-type)
|
|
9
|
+
* - toConstantValue/toDynamicValue bindings → leaf nodes, no recursion
|
|
10
|
+
* - unresolvable tokens/types → kind "unresolved" nodes (generation never fails)
|
|
11
|
+
*
|
|
12
|
+
* Roots: @Controller() classes when the project has any; otherwise every
|
|
13
|
+
* DI-registered class in the project that no other project class injects
|
|
14
|
+
* (the tops of the local DAG).
|
|
15
|
+
*/
|
|
16
|
+
import * as ts from 'typescript';
|
|
17
|
+
import { DiGraph } from './model';
|
|
18
|
+
export declare function isControllerClass(cls: ts.ClassDeclaration): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Build the full DI graph for one project. `projectRoot` is workspace-relative.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildDiGraph(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Graph Analyzer (pass 2)\n *\n * Walks constructor injection from a project's root classes down to leaves:\n *\n * - @inject(TOKEN) params → token lookup in the binding table → bound impl\n * - @multiInject(TOKEN) → fan-out edge to EVERY binding of that token\n * - bare typed params → checker resolves the type to a class (inject-by-type)\n * - toConstantValue/toDynamicValue bindings → leaf nodes, no recursion\n * - unresolvable tokens/types → kind \"unresolved\" nodes (generation never fails)\n *\n * Roots: @Controller() classes when the project has any; otherwise every\n * DI-registered class in the project that no other project class injects\n * (the tops of the local DAG).\n */\n\nimport * as ts from 'typescript';\nimport * as path from 'path';\nimport { Binding, DiEdge, DiGraph, DiNode, DiNodeKind, DiScope } from './model';\nimport {\n BindingTable,\n classDecorators,\n collectBindings,\n decoratorCall,\n decoratorName,\n resolveClassDeclaration,\n} from './bindings';\nimport { classTokenKey, relativeFile, resolveTokenKey } from './token-resolver';\n\nconst DI_DECORATORS = new Set(['provideSingleton', 'provideTransient', 'provideSingletonAs', 'injectable']);\n\nclass ParamInjection {\n expr: ts.Expression | null;\n multi: boolean;\n optional: boolean;\n unmanaged: boolean;\n\n constructor(expr: ts.Expression | null, multi: boolean, optional: boolean, unmanaged: boolean) {\n this.expr = expr;\n this.multi = multi;\n this.optional = optional;\n this.unmanaged = unmanaged;\n }\n}\n\nfunction readParamDecorators(param: ts.ParameterDeclaration): ParamInjection {\n let expr: ts.Expression | null = null;\n let multi = false;\n let optional = false;\n let unmanaged = false;\n for (const decorator of ts.getDecorators(param) ?? []) {\n const name = decoratorName(decorator);\n const call = decoratorCall(decorator);\n if (name === 'inject' && call?.arguments[0]) {\n expr = call.arguments[0];\n } else if (name === 'multiInject' && call?.arguments[0]) {\n expr = call.arguments[0];\n multi = true;\n } else if (name === 'optional') {\n optional = true;\n } else if (name === 'unmanaged') {\n unmanaged = true;\n }\n }\n return new ParamInjection(expr, multi, optional, unmanaged);\n}\n\nfunction hasDecoratorNamed(cls: ts.ClassDeclaration, names: Set<string>): boolean {\n for (const decorator of classDecorators(cls)) {\n const name = decoratorName(decorator);\n if (name && names.has(name)) return true;\n }\n return false;\n}\n\nexport function isControllerClass(cls: ts.ClassDeclaration): boolean {\n return hasDecoratorNamed(cls, new Set(['Controller']));\n}\n\nfunction isDiRegisteredClass(cls: ts.ClassDeclaration): boolean {\n return hasDecoratorNamed(cls, DI_DECORATORS);\n}\n\nfunction findConstructor(cls: ts.ClassDeclaration): ts.ConstructorDeclaration | null {\n for (const member of cls.members) {\n if (ts.isConstructorDeclaration(member) && member.body) return member;\n if (ts.isConstructorDeclaration(member)) return member;\n }\n return null;\n}\n\n/** All class declarations in files under the project root. */\nfunction projectClasses(\n program: ts.Program,\n workspaceRoot: string,\n projectRoot: string,\n): ts.ClassDeclaration[] {\n const prefix = projectRoot.endsWith('/') ? projectRoot : projectRoot + '/';\n const classes: ts.ClassDeclaration[] = [];\n for (const sourceFile of program.getSourceFiles()) {\n if (sourceFile.isDeclarationFile) continue;\n const rel = relativeFile(workspaceRoot, sourceFile);\n if (!rel.startsWith(prefix)) continue;\n const visit = (node: ts.Node): void => {\n if (ts.isClassDeclaration(node) && node.name) classes.push(node);\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n }\n return classes;\n}\n\nclass DiGraphBuilder {\n private readonly checker: ts.TypeChecker;\n private readonly table: BindingTable;\n private readonly workspaceRoot: string;\n private readonly graph: DiGraph;\n private readonly classIds = new Map<ts.ClassDeclaration, string>();\n private readonly leafIds = new Map<Binding, string>();\n private readonly unresolvedIds = new Map<string, string>();\n private readonly usedIds = new Set<string>();\n private readonly visited = new Set<ts.ClassDeclaration>();\n\n constructor(checker: ts.TypeChecker, table: BindingTable, workspaceRoot: string, graph: DiGraph) {\n this.checker = checker;\n this.table = table;\n this.workspaceRoot = workspaceRoot;\n this.graph = graph;\n }\n\n addRoot(cls: ts.ClassDeclaration): void {\n const id = this.classNode(cls);\n this.graph.roots.push(id);\n this.walkClass(cls);\n }\n\n /**\n * Register (or fetch) the node for a class, returning its stable id.\n * `scopeHint` carries the scope of the module binding the class was reached\n * through (e.g. bind(TOKEN).to(X).inSingletonScope() where X is not self-bound).\n */\n private classNode(cls: ts.ClassDeclaration, scopeHint: DiScope = 'unknown'): string {\n const existing = this.classIds.get(cls);\n if (existing) return existing;\n\n const className = cls.name ? cls.name.text : '<anonymous>';\n const file = relativeFile(this.workspaceRoot, cls.getSourceFile());\n const id = this.claimId(className, file);\n this.classIds.set(cls, id);\n\n const declaredScope = this.classScope(cls);\n const scope = declaredScope !== 'unknown' ? declaredScope : scopeHint;\n const kind: DiNodeKind = isControllerClass(cls) ? 'controller' : 'class';\n this.graph.nodes.push(new DiNode(id, className, kind, scope, file));\n return id;\n }\n\n private claimId(preferred: string, file: string): string {\n if (!this.usedIds.has(preferred)) {\n this.usedIds.add(preferred);\n return preferred;\n }\n const disambiguated = `${preferred}@${path.posix.dirname(file)}`;\n let id = disambiguated;\n let n = 2;\n while (this.usedIds.has(id)) {\n id = `${disambiguated}~${n}`;\n n++;\n }\n this.usedIds.add(id);\n return id;\n }\n\n /** Scope from the class's own decorator/module binding, if any. */\n private classScope(cls: ts.ClassDeclaration): DiScope {\n const token = classTokenKey(cls, this.workspaceRoot);\n for (const binding of this.table.lookup(token.key)) {\n if (binding.scope !== 'unknown') return binding.scope;\n }\n return 'unknown';\n }\n\n private leafNode(binding: Binding, kind: DiNodeKind): string {\n const existing = this.leafIds.get(binding);\n if (existing) return existing;\n\n // Dynamic values are factory functions — their source text is noise, so label\n // them by the token they satisfy. Constants keep their (short) expression text.\n const className = kind === 'dynamic'\n ? `${binding.tokenDisplay} (dynamic)`\n : binding.valueText !== '' ? binding.valueText : binding.tokenDisplay;\n const id = this.claimId(className, binding.file);\n this.leafIds.set(binding, id);\n this.graph.nodes.push(new DiNode(id, className, kind, binding.scope, binding.file));\n return id;\n }\n\n private unresolvedNode(display: string, file: string): string {\n const key = `${display}\u0000${file}`;\n const existing = this.unresolvedIds.get(key);\n if (existing) return existing;\n\n const id = this.claimId(display, file);\n this.unresolvedIds.set(key, id);\n this.graph.nodes.push(new DiNode(id, display, 'unresolved', 'unknown', file));\n this.graph.unresolved.push(display);\n return id;\n }\n\n walkClass(cls: ts.ClassDeclaration): void {\n if (this.visited.has(cls)) return;\n this.visited.add(cls);\n\n const ctor = findConstructor(cls);\n if (!ctor) return;\n const fromId = this.classNode(cls);\n\n for (const param of ctor.parameters) {\n this.walkParam(fromId, cls, param);\n }\n }\n\n private walkParam(fromId: string, cls: ts.ClassDeclaration, param: ts.ParameterDeclaration): void {\n const injection = readParamDecorators(param);\n if (injection.unmanaged) return;\n\n const paramName = ts.isIdentifier(param.name) ? param.name.text : param.name.getText();\n const paramType = param.type ? param.type.getText() : '';\n\n if (injection.expr) {\n this.walkTokenParam(fromId, cls, injection, paramName, paramType);\n } else {\n this.walkTypeParam(fromId, cls, param, paramName, paramType);\n }\n }\n\n private walkTokenParam(\n fromId: string,\n cls: ts.ClassDeclaration,\n injection: ParamInjection,\n paramName: string,\n paramType: string,\n ): void {\n const expr = injection.expr;\n if (!expr) return;\n const token = resolveTokenKey(expr, this.checker, this.workspaceRoot);\n const kind = injection.multi ? 'multiInject' : 'token';\n const bindings = this.table.lookup(token.key);\n\n if (bindings.length === 0) {\n // @multiInject @optional with zero bindings is legal — no edge, no error.\n if (injection.multi && injection.optional) return;\n const file = relativeFile(this.workspaceRoot, cls.getSourceFile());\n const toId = this.unresolvedNode(token.display, file);\n this.graph.edges.push(new DiEdge(fromId, toId, kind, token.display, token.key, paramName, paramType));\n return;\n }\n\n for (const binding of bindings) {\n const toId = this.bindingTarget(binding);\n this.graph.edges.push(new DiEdge(fromId, toId, kind, token.display, token.key, paramName, paramType));\n }\n }\n\n private bindingTarget(binding: Binding): string {\n if (binding.implClass) {\n const toId = this.classNode(binding.implClass, binding.scope);\n this.walkClass(binding.implClass);\n return toId;\n }\n if (binding.kind === 'toConstantValue') return this.leafNode(binding, 'constant');\n if (binding.kind === 'toDynamicValue') return this.leafNode(binding, 'dynamic');\n // .to(X) where X did not resolve to a class declaration.\n return this.unresolvedNode(binding.valueText !== '' ? binding.valueText : binding.tokenDisplay, binding.file);\n }\n\n private walkTypeParam(\n fromId: string,\n cls: ts.ClassDeclaration,\n param: ts.ParameterDeclaration,\n paramName: string,\n paramType: string,\n ): void {\n const typeName = param.type && ts.isTypeReferenceNode(param.type) ? param.type.typeName : null;\n const target = typeName && ts.isIdentifier(typeName)\n ? resolveClassDeclaration(typeName, this.checker)\n : null;\n\n if (target) {\n const token = classTokenKey(target, this.workspaceRoot);\n const toId = this.classNode(target);\n this.walkClass(target);\n this.graph.edges.push(new DiEdge(fromId, toId, 'type', token.display, token.key, paramName, paramType));\n return;\n }\n\n // Bare param whose type is not a resolvable class (interface, primitive, etc.).\n const file = relativeFile(this.workspaceRoot, cls.getSourceFile());\n const toId = this.unresolvedNode(paramType !== '' ? paramType : paramName, file);\n this.graph.edges.push(new DiEdge(fromId, toId, 'type', paramType, `type:${paramType}`, paramName, paramType));\n }\n}\n\nfunction byClassName(a: ts.ClassDeclaration, b: ts.ClassDeclaration): number {\n const nameA = a.name ? a.name.text : '';\n const nameB = b.name ? b.name.text : '';\n if (nameA !== nameB) return nameA < nameB ? -1 : 1;\n return a.getSourceFile().fileName < b.getSourceFile().fileName ? -1 : 1;\n}\n\n/**\n * Library-project roots: DI-registered classes in the project that no other\n * project class injects (tops of the local DAG).\n */\nfunction findLibraryRoots(\n classes: ts.ClassDeclaration[],\n checker: ts.TypeChecker,\n table: BindingTable,\n workspaceRoot: string,\n): ts.ClassDeclaration[] {\n const diClasses = classes.filter((cls: ts.ClassDeclaration) => isDiRegisteredClass(cls));\n const injected = new Set<ts.ClassDeclaration>();\n\n for (const cls of diClasses) {\n const ctor = findConstructor(cls);\n for (const param of ctor?.parameters ?? []) {\n const injection = readParamDecorators(param);\n if (injection.unmanaged) continue;\n if (injection.expr) {\n const token = resolveTokenKey(injection.expr, checker, workspaceRoot);\n for (const binding of table.lookup(token.key)) {\n if (binding.implClass) injected.add(binding.implClass);\n }\n } else if (param.type && ts.isTypeReferenceNode(param.type) && ts.isIdentifier(param.type.typeName)) {\n const target = resolveClassDeclaration(param.type.typeName, checker);\n if (target) injected.add(target);\n }\n }\n }\n\n return diClasses.filter((cls: ts.ClassDeclaration) => !injected.has(cls));\n}\n\n/**\n * Build the full DI graph for one project. `projectRoot` is workspace-relative.\n */\nexport function buildDiGraph(\n program: ts.Program,\n workspaceRoot: string,\n projectRoot: string,\n projectName: string,\n): DiGraph {\n const checker = program.getTypeChecker();\n const table = collectBindings(program, checker, workspaceRoot);\n const graph = new DiGraph(projectName);\n const builder = new DiGraphBuilder(checker, table, workspaceRoot, graph);\n\n const classes = projectClasses(program, workspaceRoot, projectRoot);\n const controllers = classes.filter((cls: ts.ClassDeclaration) => isControllerClass(cls));\n const roots = controllers.length > 0\n ? controllers\n : findLibraryRoots(classes, checker, table, workspaceRoot);\n\n for (const root of [...roots].sort(byClassName)) {\n builder.addRoot(root);\n }\n\n return graph;\n}\n"]}
|