@webpieces/nx-webpieces-rules 0.3.245 → 0.3.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/executors/di-graph-generate/executor.js +42 -12
- package/src/executors/di-graph-generate/executor.js.map +1 -1
- package/src/lib/di-graph/analyzer-strategy.d.ts +47 -0
- package/src/lib/di-graph/analyzer-strategy.js +86 -0
- package/src/lib/di-graph/analyzer-strategy.js.map +1 -0
- package/src/lib/di-graph/analyzer.d.ts +128 -5
- package/src/lib/di-graph/analyzer.js +190 -65
- package/src/lib/di-graph/analyzer.js.map +1 -1
- package/src/lib/di-graph/angular-analyzer.d.ts +28 -0
- package/src/lib/di-graph/angular-analyzer.js +139 -0
- package/src/lib/di-graph/angular-analyzer.js.map +1 -0
- package/src/lib/di-graph/angular-providers.d.ts +32 -0
- package/src/lib/di-graph/angular-providers.js +177 -0
- package/src/lib/di-graph/angular-providers.js.map +1 -0
- package/src/lib/di-graph/angular-roots.d.ts +21 -0
- package/src/lib/di-graph/angular-roots.js +150 -0
- package/src/lib/di-graph/angular-roots.js.map +1 -0
- package/src/lib/di-graph/dot.js +8 -5
- package/src/lib/di-graph/dot.js.map +1 -1
- package/src/lib/di-graph/mermaid.js +12 -7
- package/src/lib/di-graph/mermaid.js.map +1 -1
- package/src/lib/di-graph/model.d.ts +17 -3
- package/src/lib/di-graph/model.js +19 -2
- package/src/lib/di-graph/model.js.map +1 -1
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.246",
|
|
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.3.
|
|
25
|
-
"@webpieces/code-rules": "0.3.
|
|
26
|
-
"@webpieces/eslint-rules": "0.3.
|
|
27
|
-
"@webpieces/pr-gate": "0.3.
|
|
28
|
-
"@webpieces/rules-config": "0.3.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.3.246",
|
|
25
|
+
"@webpieces/code-rules": "0.3.246",
|
|
26
|
+
"@webpieces/eslint-rules": "0.3.246",
|
|
27
|
+
"@webpieces/pr-gate": "0.3.246",
|
|
28
|
+
"@webpieces/rules-config": "0.3.246",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -23,7 +23,7 @@ const tslib_1 = require("tslib");
|
|
|
23
23
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
24
24
|
const fs = tslib_1.__importStar(require("fs"));
|
|
25
25
|
const path = tslib_1.__importStar(require("path"));
|
|
26
|
-
const
|
|
26
|
+
const analyzer_strategy_1 = require("../../lib/di-graph/analyzer-strategy");
|
|
27
27
|
const program_1 = require("../../lib/di-graph/program");
|
|
28
28
|
const serializer_1 = require("../../lib/di-graph/serializer");
|
|
29
29
|
const mermaid_1 = require("../../lib/di-graph/mermaid");
|
|
@@ -31,7 +31,9 @@ const model_1 = require("../../lib/di-graph/model");
|
|
|
31
31
|
const toError_1 = require("../../toError");
|
|
32
32
|
const RULE_NAME = 'di-graph';
|
|
33
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.
|
|
34
|
+
// gets an empty graph without paying for a ts.Program. Angular markers
|
|
35
|
+
// (@Component/bootstrapApplication) are included so an Angular app that uses no
|
|
36
|
+
// Inversify decorator isn't short-circuited to empty.
|
|
35
37
|
const DI_MARKERS = [
|
|
36
38
|
'@Controller(',
|
|
37
39
|
'@provideSingleton',
|
|
@@ -39,25 +41,46 @@ const DI_MARKERS = [
|
|
|
39
41
|
'@injectable',
|
|
40
42
|
'new ContainerModule',
|
|
41
43
|
'@inject(',
|
|
44
|
+
'@Component(',
|
|
45
|
+
'bootstrapApplication',
|
|
42
46
|
];
|
|
43
|
-
|
|
47
|
+
const ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];
|
|
48
|
+
const CONTROLLER_MARKER = '@Controller(';
|
|
49
|
+
/** Recursively read every project .ts file, folding each into `visit`. */
|
|
50
|
+
function forEachSourceFile(dir, visit) {
|
|
44
51
|
if (!fs.existsSync(dir))
|
|
45
|
-
return
|
|
52
|
+
return;
|
|
46
53
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
47
54
|
const full = path.join(dir, entry.name);
|
|
48
55
|
if (entry.isDirectory()) {
|
|
49
56
|
if (entry.name === 'node_modules' || entry.name === 'dist')
|
|
50
57
|
continue;
|
|
51
|
-
|
|
52
|
-
return true;
|
|
58
|
+
forEachSourceFile(full, visit);
|
|
53
59
|
}
|
|
54
60
|
else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
55
|
-
|
|
56
|
-
if (DI_MARKERS.some((marker) => content.includes(marker)))
|
|
57
|
-
return true;
|
|
61
|
+
visit(fs.readFileSync(full, 'utf-8'));
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
|
-
|
|
64
|
+
}
|
|
65
|
+
function sourceHasDiMarkers(dir) {
|
|
66
|
+
let found = false;
|
|
67
|
+
forEachSourceFile(dir, (content) => {
|
|
68
|
+
if (!found && DI_MARKERS.some((marker) => content.includes(marker)))
|
|
69
|
+
found = true;
|
|
70
|
+
});
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
/** Pre-scan a project's source for the framework markers used when no tag is set. */
|
|
74
|
+
function detectFrameworkMarkers(dir) {
|
|
75
|
+
let angular = false;
|
|
76
|
+
let controller = false;
|
|
77
|
+
forEachSourceFile(dir, (content) => {
|
|
78
|
+
if (!angular && ANGULAR_MARKERS.some((marker) => content.includes(marker)))
|
|
79
|
+
angular = true;
|
|
80
|
+
if (!controller && content.includes(CONTROLLER_MARKER))
|
|
81
|
+
controller = true;
|
|
82
|
+
});
|
|
83
|
+
return new analyzer_strategy_1.FrameworkMarkers(angular, controller);
|
|
61
84
|
}
|
|
62
85
|
function writeDesignFiles(projectRootAbs, graph) {
|
|
63
86
|
fs.writeFileSync(path.join(projectRootAbs, 'design.json'), (0, serializer_1.toDesignJson)(graph));
|
|
@@ -74,10 +97,11 @@ async function runExecutor(_options, context) {
|
|
|
74
97
|
const projectConfig = context.projectsConfigurations?.projects[projectName];
|
|
75
98
|
const projectRoot = projectConfig?.root ?? '.';
|
|
76
99
|
const projectRootAbs = path.join(context.root, projectRoot);
|
|
100
|
+
const srcDir = path.join(projectRootAbs, 'src');
|
|
77
101
|
console.log(`\n🧬 Generating DI design graph for ${projectName}\n`);
|
|
78
102
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- chokepoint: a generator crash must produce an actionable failure, not a stack trace mid-build
|
|
79
103
|
try {
|
|
80
|
-
if (!sourceHasDiMarkers(
|
|
104
|
+
if (!sourceHasDiMarkers(srcDir)) {
|
|
81
105
|
console.log(' No DI markers found — writing empty design graph');
|
|
82
106
|
writeDesignFiles(projectRootAbs, new model_1.DiGraph(projectName));
|
|
83
107
|
return { success: true };
|
|
@@ -88,7 +112,13 @@ async function runExecutor(_options, context) {
|
|
|
88
112
|
writeDesignFiles(projectRootAbs, new model_1.DiGraph(projectName));
|
|
89
113
|
return { success: true };
|
|
90
114
|
}
|
|
91
|
-
|
|
115
|
+
// Select the analyzer by framework: express → Inversify, angular →
|
|
116
|
+
// Angular, else skip. The explicit `framework:` nx tag wins; a marker
|
|
117
|
+
// pre-scan corroborates only when the tag is absent.
|
|
118
|
+
const framework = (0, analyzer_strategy_1.explicitFrameworkTag)(projectConfig?.tags ?? []);
|
|
119
|
+
const analyzer = (0, analyzer_strategy_1.selectAnalyzer)(framework, detectFrameworkMarkers(srcDir));
|
|
120
|
+
console.log(` Analyzer: ${analyzer.constructor.name} (framework tag: ${framework ?? 'none'})`);
|
|
121
|
+
const graph = analyzer.analyzeProject(program, context.root, projectRoot, projectName);
|
|
92
122
|
writeDesignFiles(projectRootAbs, graph);
|
|
93
123
|
const nodeCount = graph.designs.reduce((sum, d) => sum + d.nodes.length, 0);
|
|
94
124
|
const edgeCount = graph.designs.reduce((sum, d) => sum + d.edges.length, 0);
|
|
@@ -1 +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 { DiDesign, 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 const nodeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.nodes.length, 0);\n const edgeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.edges.length, 0);\n console.log(\n `✅ Wrote ${projectRoot}/design.json + design.md ` +\n `(${graph.designs.length} design(s), ${nodeCount} node(s), ${edgeCount} edge(s))`,\n );\n const unresolved = [...new Set(graph.designs.flatMap((d: DiDesign) => d.unresolved))];\n if (unresolved.length > 0) {\n console.warn(`⚠️ ${unresolved.length} unresolved token(s)/type(s): ${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"]}
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/di-graph-generate/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;AAmFH,8BA6DC;;AA7ID,0DAA0D;AAC1D,+CAAyB;AACzB,mDAA6B;AAC7B,4EAI8C;AAC9C,wDAAkE;AAClE,8DAA6D;AAC7D,wDAA8D;AAC9D,oDAA6D;AAC7D,2CAAwC;AAUxC,MAAM,SAAS,GAAG,UAAU,CAAC;AAE7B,gFAAgF;AAChF,uEAAuE;AACvE,gFAAgF;AAChF,sDAAsD;AACtD,MAAM,UAAU,GAAG;IACf,cAAc;IACd,mBAAmB;IACnB,mBAAmB;IACnB,aAAa;IACb,qBAAqB;IACrB,UAAU;IACV,aAAa;IACb,sBAAsB;CACzB,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,aAAa,EAAE,sBAAsB,CAAC,CAAC;AAChE,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAEzC,0EAA0E;AAC1E,SAAS,iBAAiB,CAAC,GAAW,EAAE,KAAgC;IACpE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO;IAChC,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACtB,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;gBAAE,SAAS;YACrE,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACnC,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACrE,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QAC1C,CAAC;IACL,CAAC;AACL,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAW;IACnC,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,iBAAiB,CAAC,GAAG,EAAE,CAAC,OAAe,EAAE,EAAE;QACvC,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAAE,KAAK,GAAG,IAAI,CAAC;IAC9F,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,qFAAqF;AACrF,SAAS,sBAAsB,CAAC,GAAW;IACvC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,iBAAiB,CAAC,GAAG,EAAE,CAAC,OAAe,EAAE,EAAE;QACvC,IAAI,CAAC,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAAE,OAAO,GAAG,IAAI,CAAC;QACnG,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;YAAE,UAAU,GAAG,IAAI,CAAC;IAC9E,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,oCAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,gBAAgB,CAAC,cAAsB,EAAE,KAAc;IAC5D,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,aAAa,CAAC,EAAE,IAAA,yBAAY,EAAC,KAAK,CAAC,CAAC,CAAC;IAChF,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,EAAE,IAAA,0BAAgB,EAAC,KAAK,CAAC,CAAC,CAAC;AACtF,CAAC;AAEc,KAAK,UAAU,WAAW,CACrC,QAAgC,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,SAAS,CAAC,CAAC;IACzC,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,kBAAkB,SAAS,2BAA2B,CAAC,CAAC;QACpE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,SAAS,CAAC;IACrD,MAAM,aAAa,GAAG,OAAO,CAAC,sBAAsB,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC5E,MAAM,WAAW,GAAG,aAAa,EAAE,IAAI,IAAI,GAAG,CAAC;IAC/C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAEhD,OAAO,CAAC,GAAG,CAAC,uCAAuC,WAAW,IAAI,CAAC,CAAC;IAEpE,+JAA+J;IAC/J,IAAI,CAAC;QACD,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;YACnE,gBAAgB,CAAC,cAAc,EAAE,IAAI,eAAO,CAAC,WAAW,CAAC,CAAC,CAAC;YAC3D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAED,MAAM,OAAO,GAAG,IAAA,8BAAoB,EAAC,cAAc,CAAC,CAAC;QACrD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;YACzE,gBAAgB,CAAC,cAAc,EAAE,IAAI,eAAO,CAAC,WAAW,CAAC,CAAC,CAAC;YAC3D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAED,mEAAmE;QACnE,sEAAsE;QACtE,qDAAqD;QACrD,MAAM,SAAS,GAAG,IAAA,wCAAoB,EAAC,aAAa,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QAClE,MAAM,QAAQ,GAAG,IAAA,kCAAc,EAAC,SAAS,EAAE,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3E,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,WAAW,CAAC,IAAI,oBAAoB,SAAS,IAAI,MAAM,GAAG,CAAC,CAAC;QAEjG,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;QACvF,gBAAgB,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QAExC,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAW,EAAE,CAAW,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC9F,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAW,EAAE,CAAW,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC9F,OAAO,CAAC,GAAG,CACP,WAAW,WAAW,2BAA2B;YAC7C,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,eAAe,SAAS,aAAa,SAAS,WAAW,CACxF,CAAC;QACF,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAW,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACtF,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,IAAI,CAAC,OAAO,UAAU,CAAC,MAAM,iCAAiC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC;QACD,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,oCAAoC,WAAW,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACnF,OAAO,CAAC,KAAK,CAAC,oCAAoC,SAAS,yCAAyC,CAAC,CAAC;QACtG,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","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 {\n explicitFrameworkTag,\n FrameworkMarkers,\n selectAnalyzer,\n} from '../../lib/di-graph/analyzer-strategy';\nimport { createProjectProgram } from '../../lib/di-graph/program';\nimport { toDesignJson } from '../../lib/di-graph/serializer';\nimport { toDesignMarkdown } from '../../lib/di-graph/mermaid';\nimport { DiDesign, 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. Angular markers\n// (@Component/bootstrapApplication) are included so an Angular app that uses no\n// Inversify decorator isn't short-circuited to empty.\nconst DI_MARKERS = [\n '@Controller(',\n '@provideSingleton',\n '@provideTransient',\n '@injectable',\n 'new ContainerModule',\n '@inject(',\n '@Component(',\n 'bootstrapApplication',\n];\n\nconst ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];\nconst CONTROLLER_MARKER = '@Controller(';\n\n/** Recursively read every project .ts file, folding each into `visit`. */\nfunction forEachSourceFile(dir: string, visit: (content: string) => void): void {\n if (!fs.existsSync(dir)) return;\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 forEachSourceFile(full, visit);\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n visit(fs.readFileSync(full, 'utf-8'));\n }\n }\n}\n\nfunction sourceHasDiMarkers(dir: string): boolean {\n let found = false;\n forEachSourceFile(dir, (content: string) => {\n if (!found && DI_MARKERS.some((marker: string) => content.includes(marker))) found = true;\n });\n return found;\n}\n\n/** Pre-scan a project's source for the framework markers used when no tag is set. */\nfunction detectFrameworkMarkers(dir: string): FrameworkMarkers {\n let angular = false;\n let controller = false;\n forEachSourceFile(dir, (content: string) => {\n if (!angular && ANGULAR_MARKERS.some((marker: string) => content.includes(marker))) angular = true;\n if (!controller && content.includes(CONTROLLER_MARKER)) controller = true;\n });\n return new FrameworkMarkers(angular, controller);\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 const srcDir = path.join(projectRootAbs, 'src');\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(srcDir)) {\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 // Select the analyzer by framework: express → Inversify, angular →\n // Angular, else skip. The explicit `framework:` nx tag wins; a marker\n // pre-scan corroborates only when the tag is absent.\n const framework = explicitFrameworkTag(projectConfig?.tags ?? []);\n const analyzer = selectAnalyzer(framework, detectFrameworkMarkers(srcDir));\n console.log(` Analyzer: ${analyzer.constructor.name} (framework tag: ${framework ?? 'none'})`);\n\n const graph = analyzer.analyzeProject(program, context.root, projectRoot, projectName);\n writeDesignFiles(projectRootAbs, graph);\n\n const nodeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.nodes.length, 0);\n const edgeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.edges.length, 0);\n console.log(\n `✅ Wrote ${projectRoot}/design.json + design.md ` +\n `(${graph.designs.length} design(s), ${nodeCount} node(s), ${edgeCount} edge(s))`,\n );\n const unresolved = [...new Set(graph.designs.flatMap((d: DiDesign) => d.unresolved))];\n if (unresolved.length > 0) {\n console.warn(`⚠️ ${unresolved.length} unresolved token(s)/type(s): ${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,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI Analyzer Strategy
|
|
3
|
+
*
|
|
4
|
+
* Picks which DI analyzer generates a project's design graph, driven by the
|
|
5
|
+
* project's `framework` nx tag (see the framework/libType tagging PR):
|
|
6
|
+
*
|
|
7
|
+
* - `express` → {@link InversifyAnalyzer} (roots on `@Controller`)
|
|
8
|
+
* - `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
|
|
9
|
+
* - anything else (`react`, `all`, ...) → {@link EmptyAnalyzer} (skip; the
|
|
10
|
+
* controller-less library top-of-DAG behavior is deferred for v1)
|
|
11
|
+
*
|
|
12
|
+
* The explicit tag is the source of truth. When it is ABSENT (e.g. before the
|
|
13
|
+
* tagging PR lands), a cheap marker pre-scan corroborates: `@Component(` /
|
|
14
|
+
* `bootstrapApplication` → angular; `@Controller(` → express. This makes the
|
|
15
|
+
* committed design.* identical whether selection is tag- or marker-driven.
|
|
16
|
+
*/
|
|
17
|
+
import type * as ts from 'typescript';
|
|
18
|
+
import { DiGraph } from './model';
|
|
19
|
+
/** Which framework a source tree's decorators point at (marker pre-scan fallback). */
|
|
20
|
+
export declare class FrameworkMarkers {
|
|
21
|
+
angular: boolean;
|
|
22
|
+
controller: boolean;
|
|
23
|
+
constructor(angular: boolean, controller: boolean);
|
|
24
|
+
}
|
|
25
|
+
/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */
|
|
26
|
+
export interface DiAnalyzer {
|
|
27
|
+
analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
|
|
28
|
+
}
|
|
29
|
+
/** Inversify (express): one design per `@Controller` root. Library roots deferred (v1). */
|
|
30
|
+
export declare class InversifyAnalyzer implements DiAnalyzer {
|
|
31
|
+
analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
|
|
32
|
+
}
|
|
33
|
+
/** Angular: one design per entry component (bootstrap + routed). */
|
|
34
|
+
export declare class AngularAnalyzer implements DiAnalyzer {
|
|
35
|
+
analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
|
|
36
|
+
}
|
|
37
|
+
/** Non-angular/non-express projects: an empty graph (skip). */
|
|
38
|
+
export declare class EmptyAnalyzer implements DiAnalyzer {
|
|
39
|
+
analyzeProject(_program: ts.Program, _workspaceRoot: string, _projectRoot: string, projectName: string): DiGraph;
|
|
40
|
+
}
|
|
41
|
+
/** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */
|
|
42
|
+
export declare function explicitFrameworkTag(tags: readonly string[]): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Choose the analyzer for a project. `framework` is the EXPLICIT tag value (or
|
|
45
|
+
* null); `markers` is only consulted when the tag is absent.
|
|
46
|
+
*/
|
|
47
|
+
export declare function selectAnalyzer(framework: string | null, markers: FrameworkMarkers): DiAnalyzer;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DI Analyzer Strategy
|
|
4
|
+
*
|
|
5
|
+
* Picks which DI analyzer generates a project's design graph, driven by the
|
|
6
|
+
* project's `framework` nx tag (see the framework/libType tagging PR):
|
|
7
|
+
*
|
|
8
|
+
* - `express` → {@link InversifyAnalyzer} (roots on `@Controller`)
|
|
9
|
+
* - `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
|
|
10
|
+
* - anything else (`react`, `all`, ...) → {@link EmptyAnalyzer} (skip; the
|
|
11
|
+
* controller-less library top-of-DAG behavior is deferred for v1)
|
|
12
|
+
*
|
|
13
|
+
* The explicit tag is the source of truth. When it is ABSENT (e.g. before the
|
|
14
|
+
* tagging PR lands), a cheap marker pre-scan corroborates: `@Component(` /
|
|
15
|
+
* `bootstrapApplication` → angular; `@Controller(` → express. This makes the
|
|
16
|
+
* committed design.* identical whether selection is tag- or marker-driven.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.EmptyAnalyzer = exports.AngularAnalyzer = exports.InversifyAnalyzer = exports.FrameworkMarkers = void 0;
|
|
20
|
+
exports.explicitFrameworkTag = explicitFrameworkTag;
|
|
21
|
+
exports.selectAnalyzer = selectAnalyzer;
|
|
22
|
+
const model_1 = require("./model");
|
|
23
|
+
const analyzer_1 = require("./analyzer");
|
|
24
|
+
const angular_analyzer_1 = require("./angular-analyzer");
|
|
25
|
+
const FRAMEWORK_TAG_PREFIX = 'framework:';
|
|
26
|
+
/** Which framework a source tree's decorators point at (marker pre-scan fallback). */
|
|
27
|
+
class FrameworkMarkers {
|
|
28
|
+
angular;
|
|
29
|
+
controller;
|
|
30
|
+
constructor(angular, controller) {
|
|
31
|
+
this.angular = angular;
|
|
32
|
+
this.controller = controller;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.FrameworkMarkers = FrameworkMarkers;
|
|
36
|
+
/** Inversify (express): one design per `@Controller` root. Library roots deferred (v1). */
|
|
37
|
+
class InversifyAnalyzer {
|
|
38
|
+
analyzeProject(program, workspaceRoot, projectRoot, projectName) {
|
|
39
|
+
return (0, analyzer_1.buildDiGraph)(program, workspaceRoot, projectRoot, projectName);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.InversifyAnalyzer = InversifyAnalyzer;
|
|
43
|
+
/** Angular: one design per entry component (bootstrap + routed). */
|
|
44
|
+
class AngularAnalyzer {
|
|
45
|
+
analyzeProject(program, workspaceRoot, projectRoot, projectName) {
|
|
46
|
+
return (0, angular_analyzer_1.buildAngularDiGraph)(program, workspaceRoot, projectRoot, projectName);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.AngularAnalyzer = AngularAnalyzer;
|
|
50
|
+
/** Non-angular/non-express projects: an empty graph (skip). */
|
|
51
|
+
class EmptyAnalyzer {
|
|
52
|
+
analyzeProject(_program, _workspaceRoot, _projectRoot, projectName) {
|
|
53
|
+
return new model_1.DiGraph(projectName);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.EmptyAnalyzer = EmptyAnalyzer;
|
|
57
|
+
/** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */
|
|
58
|
+
function explicitFrameworkTag(tags) {
|
|
59
|
+
for (const tag of tags) {
|
|
60
|
+
if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {
|
|
61
|
+
const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();
|
|
62
|
+
if (value.length > 0)
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Choose the analyzer for a project. `framework` is the EXPLICIT tag value (or
|
|
70
|
+
* null); `markers` is only consulted when the tag is absent.
|
|
71
|
+
*/
|
|
72
|
+
function selectAnalyzer(framework, markers) {
|
|
73
|
+
if (framework === 'express')
|
|
74
|
+
return new InversifyAnalyzer();
|
|
75
|
+
if (framework === 'angular')
|
|
76
|
+
return new AngularAnalyzer();
|
|
77
|
+
if (framework !== null)
|
|
78
|
+
return new EmptyAnalyzer();
|
|
79
|
+
// Tag absent — fall back to marker corroboration.
|
|
80
|
+
if (markers.angular)
|
|
81
|
+
return new AngularAnalyzer();
|
|
82
|
+
if (markers.controller)
|
|
83
|
+
return new InversifyAnalyzer();
|
|
84
|
+
return new EmptyAnalyzer();
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=analyzer-strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAoDH,oDAQC;AAMD,wCASC;AAxED,mCAAkC;AAClC,yCAA0C;AAC1C,yDAAyD;AAEzD,MAAM,oBAAoB,GAAG,YAAY,CAAC;AAE1C,sFAAsF;AACtF,MAAa,gBAAgB;IACzB,OAAO,CAAU;IACjB,UAAU,CAAU;IAEpB,YAAY,OAAgB,EAAE,UAAmB;QAC7C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,CAAC;CACJ;AARD,4CAQC;AAOD,2FAA2F;AAC3F,MAAa,iBAAiB;IAC1B,cAAc,CAAC,OAAmB,EAAE,aAAqB,EAAE,WAAmB,EAAE,WAAmB;QAC/F,OAAO,IAAA,uBAAY,EAAC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IAC1E,CAAC;CACJ;AAJD,8CAIC;AAED,oEAAoE;AACpE,MAAa,eAAe;IACxB,cAAc,CAAC,OAAmB,EAAE,aAAqB,EAAE,WAAmB,EAAE,WAAmB;QAC/F,OAAO,IAAA,sCAAmB,EAAC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACjF,CAAC;CACJ;AAJD,0CAIC;AAED,+DAA+D;AAC/D,MAAa,aAAa;IACtB,cAAc,CACV,QAAoB,EACpB,cAAsB,EACtB,YAAoB,EACpB,WAAmB;QAEnB,OAAO,IAAI,eAAO,CAAC,WAAW,CAAC,CAAC;IACpC,CAAC;CACJ;AATD,sCASC;AAED,0FAA0F;AAC1F,SAAgB,oBAAoB,CAAC,IAAuB;IACxD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,GAAG,CAAC,UAAU,CAAC,oBAAoB,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;QACvC,CAAC;IACL,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,SAAgB,cAAc,CAAC,SAAwB,EAAE,OAAyB;IAC9E,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,IAAI,iBAAiB,EAAE,CAAC;IAC5D,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,IAAI,eAAe,EAAE,CAAC;IAC1D,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,IAAI,aAAa,EAAE,CAAC;IAEnD,kDAAkD;IAClD,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,IAAI,eAAe,EAAE,CAAC;IAClD,IAAI,OAAO,CAAC,UAAU;QAAE,OAAO,IAAI,iBAAiB,EAAE,CAAC;IACvD,OAAO,IAAI,aAAa,EAAE,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph, driven by the\n * project's `framework` nx tag (see the framework/libType tagging PR):\n *\n * - `express` → {@link InversifyAnalyzer} (roots on `@Controller`)\n * - `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `all`, ...) → {@link EmptyAnalyzer} (skip; the\n * controller-less library top-of-DAG behavior is deferred for v1)\n *\n * The explicit tag is the source of truth. When it is ABSENT (e.g. before the\n * tagging PR lands), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@Controller(` → express. This makes the\n * committed design.* identical whether selection is tag- or marker-driven.\n */\n\nimport type * as ts from 'typescript';\nimport { DiGraph } from './model';\nimport { buildDiGraph } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\n\n/** Which framework a source tree's decorators point at (marker pre-scan fallback). */\nexport class FrameworkMarkers {\n angular: boolean;\n controller: boolean;\n\n constructor(angular: boolean, controller: boolean) {\n this.angular = angular;\n this.controller = controller;\n }\n}\n\n/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */\nexport interface DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;\n}\n\n/** Inversify (express): one design per `@Controller` root. Library roots deferred (v1). */\nexport class InversifyAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Angular: one design per entry component (bootstrap + routed). */\nexport class AngularAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildAngularDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Non-angular/non-express projects: an empty graph (skip). */\nexport class EmptyAnalyzer implements DiAnalyzer {\n analyzeProject(\n _program: ts.Program,\n _workspaceRoot: string,\n _projectRoot: string,\n projectName: string,\n ): DiGraph {\n return new DiGraph(projectName);\n }\n}\n\n/** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n for (const tag of tags) {\n if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {\n const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/**\n * Choose the analyzer for a project. `framework` is the EXPLICIT tag value (or\n * null); `markers` is only consulted when the tag is absent.\n */\nexport function selectAnalyzer(framework: string | null, markers: FrameworkMarkers): DiAnalyzer {\n if (framework === 'express') return new InversifyAnalyzer();\n if (framework === 'angular') return new AngularAnalyzer();\n if (framework !== null) return new EmptyAnalyzer();\n\n // Tag absent — fall back to marker corroboration.\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer();\n return new EmptyAnalyzer();\n}\n"]}
|
|
@@ -14,11 +14,134 @@
|
|
|
14
14
|
* (the tops of the local DAG).
|
|
15
15
|
*/
|
|
16
16
|
import * as ts from 'typescript';
|
|
17
|
-
import { DiGraph } from './model';
|
|
17
|
+
import { DiDesign, DiGraph, DiNodeKind, DiScope } from './model';
|
|
18
|
+
import { BindingTable } from './bindings';
|
|
19
|
+
export declare class ParamInjection {
|
|
20
|
+
expr: ts.Expression | null;
|
|
21
|
+
multi: boolean;
|
|
22
|
+
optional: boolean;
|
|
23
|
+
unmanaged: boolean;
|
|
24
|
+
constructor(expr: ts.Expression | null, multi: boolean, optional: boolean, unmanaged: boolean);
|
|
25
|
+
}
|
|
26
|
+
export declare function readParamDecorators(param: ts.ParameterDeclaration): ParamInjection;
|
|
18
27
|
export declare function isControllerClass(cls: ts.ClassDeclaration): boolean;
|
|
28
|
+
export declare function findConstructor(cls: ts.ClassDeclaration): ts.ConstructorDeclaration | null;
|
|
29
|
+
/** All class declarations in files under the project root. */
|
|
30
|
+
export declare function projectClasses(program: ts.Program, workspaceRoot: string, projectRoot: string): ts.ClassDeclaration[];
|
|
19
31
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
32
|
+
* One normalized injection site, framework-agnostic — produced by a builder's
|
|
33
|
+
* `collectInjections(cls)` hook and processed by the shared base. Inversify
|
|
34
|
+
* emits `token`/`type` from constructor params; Angular emits `angularToken`
|
|
35
|
+
* from constructor params AND `inject()` field initializers.
|
|
36
|
+
*
|
|
37
|
+
* Modes:
|
|
38
|
+
* - `token` Inversify `@inject`/`@multiInject`: table lookup; an unbound
|
|
39
|
+
* token becomes an `unresolved` node (no class fallback).
|
|
40
|
+
* - `type` Inversify bare typed param: resolve the declared TYPE to a
|
|
41
|
+
* class (inject-by-type); no table lookup.
|
|
42
|
+
* - `angularToken` Angular `inject(T)` / `@Inject(T)` / bare typed ctor param:
|
|
43
|
+
* table lookup FIRST (provider table), then fall back to
|
|
44
|
+
* resolving the token expression as a class, else unresolved.
|
|
45
|
+
*/
|
|
46
|
+
export declare class Injection {
|
|
47
|
+
mode: 'token' | 'type' | 'angularToken';
|
|
48
|
+
/** Token/type expression (modes `token`/`angularToken`, or the type identifier for `type`). */
|
|
49
|
+
expr: ts.Expression | ts.Identifier | null;
|
|
50
|
+
multi: boolean;
|
|
51
|
+
optional: boolean;
|
|
52
|
+
paramName: string;
|
|
53
|
+
paramType: string;
|
|
54
|
+
constructor(mode: 'token' | 'type' | 'angularToken', expr: ts.Expression | ts.Identifier | null, paramName: string, paramType: string, multi?: boolean, optional?: boolean);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Builds ONE self-contained `DiDesign` for a single root. A fresh instance is
|
|
58
|
+
* created per root so its maps (visited/classIds/usedIds/...) are scoped to
|
|
59
|
+
* that root's tree — a dependency shared by two roots is therefore walked
|
|
60
|
+
* (and duplicated) into each root's design, not hidden under whichever root
|
|
61
|
+
* reached it first.
|
|
62
|
+
*
|
|
63
|
+
* Framework-agnostic base: subclasses implement {@link collectInjections} (and
|
|
64
|
+
* override {@link rootKindOf} for Angular components); everything else — node
|
|
65
|
+
* ids, leaf/unresolved labeling, token→binding resolution, factory-dep edges,
|
|
66
|
+
* level assignment — is shared so Inversify and Angular render identically.
|
|
67
|
+
*/
|
|
68
|
+
export declare abstract class DiDesignBuilder {
|
|
69
|
+
protected readonly checker: ts.TypeChecker;
|
|
70
|
+
protected readonly table: BindingTable;
|
|
71
|
+
protected readonly workspaceRoot: string;
|
|
72
|
+
protected readonly design: DiDesign;
|
|
73
|
+
private readonly classIds;
|
|
74
|
+
private readonly leafIds;
|
|
75
|
+
private readonly unresolvedIds;
|
|
76
|
+
private readonly usedIds;
|
|
77
|
+
private readonly visited;
|
|
78
|
+
constructor(checker: ts.TypeChecker, table: BindingTable, workspaceRoot: string, design: DiDesign);
|
|
79
|
+
/** Collect the injection sites for one class (constructor params, field inject(), ...). */
|
|
80
|
+
protected abstract collectInjections(cls: ts.ClassDeclaration): Injection[];
|
|
81
|
+
/** Node kind for a root/reached class — `controller` (Inversify) or `component` (Angular). */
|
|
82
|
+
protected rootKindOf(cls: ts.ClassDeclaration): DiNodeKind;
|
|
83
|
+
addRoot(cls: ts.ClassDeclaration): void;
|
|
84
|
+
/**
|
|
85
|
+
* Register (or fetch) the node for a class, returning its stable id.
|
|
86
|
+
* `scopeHint` carries the scope of the module binding the class was reached
|
|
87
|
+
* through (e.g. bind(TOKEN).to(X).inSingletonScope() where X is not self-bound).
|
|
88
|
+
*/
|
|
89
|
+
protected classNode(cls: ts.ClassDeclaration, scopeHint?: DiScope): string;
|
|
90
|
+
private claimId;
|
|
91
|
+
/** Scope from the class's own decorator/module binding, if any. */
|
|
92
|
+
private classScope;
|
|
93
|
+
/**
|
|
94
|
+
* Leaf box for a constant/dynamic binding. B0: the box is labeled by the
|
|
95
|
+
* DECLARED param TYPE (e.g. `FirestoreConfig`, `ClientConfig`) — the DI
|
|
96
|
+
* contract — while the bound expression (`buildConfigFromEnv(...)` /
|
|
97
|
+
* `TOKEN (dynamic)`) is kept as `detail`. A dynamic leaf also fans out to
|
|
98
|
+
* each of its `useFactory` `deps` (Angular; empty for Inversify).
|
|
99
|
+
*/
|
|
100
|
+
private leafNode;
|
|
101
|
+
/** Edges from a `useFactory` leaf to each declared `deps: [...]` token (Angular). */
|
|
102
|
+
private expandFactoryDeps;
|
|
103
|
+
/**
|
|
104
|
+
* `unresolved` placeholder box. B0: labeled by the declared param TYPE
|
|
105
|
+
* (`className`) with the resolving token expression kept as `detail` (and
|
|
106
|
+
* surfaced in `design.unresolved` for diagnostics).
|
|
107
|
+
*/
|
|
108
|
+
private unresolvedNode;
|
|
109
|
+
protected walkClass(cls: ts.ClassDeclaration): void;
|
|
110
|
+
/** Resolve one injection to a node and record the edge(s). Never throws. */
|
|
111
|
+
private processInjection;
|
|
112
|
+
/** Inversify `@inject`/`@multiInject` and Angular `inject()`/`@Inject`/bare token. */
|
|
113
|
+
private processTokenInjection;
|
|
114
|
+
private bindingTarget;
|
|
115
|
+
/** Inversify bare typed param — resolve the declared type directly to a class. */
|
|
116
|
+
private processTypeInjection;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Inversify builder: injection sites are constructor params — `@inject`/
|
|
120
|
+
* `@multiInject` tokens or bare typed (inject-by-type) params.
|
|
121
|
+
*/
|
|
122
|
+
export declare class InversifyDesignBuilder extends DiDesignBuilder {
|
|
123
|
+
protected collectInjections(cls: ts.ClassDeclaration): Injection[];
|
|
124
|
+
}
|
|
125
|
+
export declare function byClassName(a: ts.ClassDeclaration, b: ts.ClassDeclaration): number;
|
|
126
|
+
/**
|
|
127
|
+
* Assign each node its BFS shortest-path depth from the design's root (root =
|
|
128
|
+
* level 0, its direct injections = level 1, ...) and record the deepest level.
|
|
129
|
+
* A node reached from multiple parents keeps its SHALLOWEST depth. Cycles are
|
|
130
|
+
* safe: a node is levelled once, the first (shallowest) time BFS reaches it.
|
|
131
|
+
*/
|
|
132
|
+
export declare function assignLevels(design: DiDesign): void;
|
|
133
|
+
/**
|
|
134
|
+
* Build one self-contained downward design tree for a single root class, using
|
|
135
|
+
* the builder produced by `makeBuilder` (Inversify or Angular). `rootKind` is
|
|
136
|
+
* the node kind for the root box (`controller`/`component`/`class`).
|
|
137
|
+
*/
|
|
138
|
+
export declare function buildDesign(root: ts.ClassDeclaration, rootKind: DiNodeKind, workspaceRoot: string, makeBuilder: (design: DiDesign) => DiDesignBuilder): DiDesign;
|
|
139
|
+
/**
|
|
140
|
+
* Build the full Inversify DI graph for one project: one self-contained
|
|
141
|
+
* `DiDesign` per @Controller root. `projectRoot` is workspace-relative.
|
|
142
|
+
*
|
|
143
|
+
* `includeLibraryRoots` (default false, the v1 executor path) roots ONLY on
|
|
144
|
+
* @Controller classes; when true, a controller-less project falls back to its
|
|
145
|
+
* top-of-DAG DI classes (the deferred library behavior, still unit-tested).
|
|
23
146
|
*/
|
|
24
|
-
export declare function buildDiGraph(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
|
|
147
|
+
export declare function buildDiGraph(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string, includeLibraryRoots?: boolean): DiGraph;
|