@webpieces/nx-webpieces-rules 0.3.263 → 0.3.271
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 +40 -13
- package/src/executors/di-graph-generate/executor.js.map +1 -1
- package/src/lib/di-graph/analyzer-strategy.d.ts +8 -7
- package/src/lib/di-graph/analyzer-strategy.js +8 -7
- package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
- package/src/lib/di-graph/analyzer.d.ts +21 -11
- package/src/lib/di-graph/analyzer.js +32 -32
- package/src/lib/di-graph/analyzer.js.map +1 -1
- package/src/lib/di-graph/angular-roots.d.ts +1 -1
- package/src/lib/di-graph/angular-roots.js +1 -1
- package/src/lib/di-graph/angular-roots.js.map +1 -1
- package/src/lib/di-graph/design-visualizer.js +1 -1
- package/src/lib/di-graph/design-visualizer.js.map +1 -1
- package/src/lib/di-graph/dot.js +2 -2
- package/src/lib/di-graph/dot.js.map +1 -1
- package/src/lib/di-graph/mermaid.js +1 -1
- package/src/lib/di-graph/mermaid.js.map +1 -1
- package/src/lib/di-graph/model.d.ts +2 -2
- package/src/lib/di-graph/model.js +2 -2
- package/src/lib/di-graph/model.js.map +1 -1
- package/src/lib/graph-metadata.js +29 -2
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-visualizer.js +2 -2
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/role-resolver.d.ts +2 -2
- package/src/lib/role-resolver.js +2 -2
- package/src/lib/role-resolver.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.271",
|
|
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.271",
|
|
25
|
+
"@webpieces/code-rules": "0.3.271",
|
|
26
|
+
"@webpieces/eslint-rules": "0.3.271",
|
|
27
|
+
"@webpieces/pr-gate": "0.3.271",
|
|
28
|
+
"@webpieces/rules-config": "0.3.271",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -32,13 +32,13 @@ const design_visualizer_1 = require("../../lib/di-graph/design-visualizer");
|
|
|
32
32
|
const model_1 = require("../../lib/di-graph/model");
|
|
33
33
|
const toError_1 = require("../../toError");
|
|
34
34
|
const RULE_NAME = 'di-graph';
|
|
35
|
+
const MISSING_DESIGN_RULE_NAME = 'missing-design-annotation';
|
|
35
36
|
// Cheap substring pre-scan: a project whose source never mentions any DI marker
|
|
36
37
|
// gets an empty graph without paying for a ts.Program. Angular markers
|
|
37
38
|
// (@Component/bootstrapApplication) are included so an Angular app that uses no
|
|
38
39
|
// Inversify decorator isn't short-circuited to empty.
|
|
39
40
|
const DI_MARKERS = [
|
|
40
|
-
'@
|
|
41
|
-
'@ApiImplementation(',
|
|
41
|
+
'@DocumentDesign(',
|
|
42
42
|
'@provideSingleton',
|
|
43
43
|
'@provideTransient',
|
|
44
44
|
'@injectable',
|
|
@@ -48,7 +48,9 @@ const DI_MARKERS = [
|
|
|
48
48
|
'bootstrapApplication',
|
|
49
49
|
];
|
|
50
50
|
const ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];
|
|
51
|
-
|
|
51
|
+
// A non-Angular DI-design root. When no role tag is set, its presence steers the
|
|
52
|
+
// marker-fallback toward the Inversify analyzer (server/controller mode).
|
|
53
|
+
const DESIGN_ROOT_MARKER = '@DocumentDesign(';
|
|
52
54
|
/** Recursively read every project .ts file, folding each into `visit`. */
|
|
53
55
|
function forEachSourceFile(dir, visit) {
|
|
54
56
|
if (!fs.existsSync(dir))
|
|
@@ -80,7 +82,7 @@ function detectFrameworkMarkers(dir) {
|
|
|
80
82
|
forEachSourceFile(dir, (content) => {
|
|
81
83
|
if (!angular && ANGULAR_MARKERS.some((marker) => content.includes(marker)))
|
|
82
84
|
angular = true;
|
|
83
|
-
if (!controller && content.includes(
|
|
85
|
+
if (!controller && content.includes(DESIGN_ROOT_MARKER))
|
|
84
86
|
controller = true;
|
|
85
87
|
});
|
|
86
88
|
return new analyzer_strategy_1.FrameworkMarkers(angular, controller);
|
|
@@ -111,7 +113,7 @@ class AnalyzerChoice {
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
/**
|
|
114
|
-
* Select the analyzer by role (server
|
|
116
|
+
* Select the analyzer by role (server & designed-lib → @DocumentDesign,
|
|
115
117
|
* client→angular design, lib→skip). The explicit `role:` nx tag is the source of
|
|
116
118
|
* truth; when absent we fall back to the legacy `framework:` selection + marker
|
|
117
119
|
* pre-scan so designs stay identical until a project is retagged.
|
|
@@ -124,10 +126,38 @@ function chooseAnalyzer(tags, srcDir) {
|
|
|
124
126
|
`(role tag: ${role ?? 'none'}, framework tags: ${frameworks.length > 0 ? frameworks.join(', ') : 'none'})`);
|
|
125
127
|
return new AnalyzerChoice(analyzer, role);
|
|
126
128
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
/**
|
|
130
|
+
* A server/designed-lib project that produced no design (zero @DocumentDesign
|
|
131
|
+
* roots) fails the build with role-specific guidance. Enforced under the
|
|
132
|
+
* `missing-design-annotation` rule.
|
|
133
|
+
*/
|
|
134
|
+
function reportMissingDesignAnnotation(projectName, role) {
|
|
135
|
+
if (role === 'server') {
|
|
136
|
+
console.error(`❌ ${projectName} is tagged role:server but has no @DocumentDesign class.\n` +
|
|
137
|
+
` All controllers should be annotated with @DocumentDesign() ` +
|
|
138
|
+
`(from @webpieces/http-routing) so their design.json / design.html get generated\n` +
|
|
139
|
+
` and linked from architecture/dependencies.html.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.error(`❌ ${projectName} is tagged role:designed-lib but has no @DocumentDesign class.\n` +
|
|
143
|
+
` One or more classes you want a design printed for need @DocumentDesign() ` +
|
|
144
|
+
`(from @webpieces/http-routing) added —\n` +
|
|
145
|
+
` or retag the project role:lib if it has no design.`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A server/designed-lib project MUST expose at least one @DocumentDesign root,
|
|
149
|
+
* else its design is empty and the role is meaningless. Returns true (and reports)
|
|
150
|
+
* when the build should fail. The `missing-design-annotation` rule gates it:
|
|
151
|
+
* absent (an older published config) → enforce; OFF → skip.
|
|
152
|
+
*/
|
|
153
|
+
function failsMissingDesignAnnotation(shared, role, graph, projectName) {
|
|
154
|
+
const missingRule = shared.rules.get(MISSING_DESIGN_RULE_NAME);
|
|
155
|
+
const enforce = !missingRule || !missingRule.isOff;
|
|
156
|
+
if (enforce && graph.designs.length === 0 && (role === 'server' || role === 'designed-lib')) {
|
|
157
|
+
reportMissingDesignAnnotation(projectName, role);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
131
161
|
}
|
|
132
162
|
async function runExecutor(_options, context) {
|
|
133
163
|
const shared = (0, rules_config_1.loadAndValidate)(context.root).resolved;
|
|
@@ -157,10 +187,7 @@ async function runExecutor(_options, context) {
|
|
|
157
187
|
}
|
|
158
188
|
const choice = chooseAnalyzer(projectConfig?.tags ?? [], srcDir);
|
|
159
189
|
const graph = choice.analyzer.analyzeProject(program, context.root, projectRoot, projectName);
|
|
160
|
-
|
|
161
|
-
// design would be empty and the tag is meaningless — fail loudly.
|
|
162
|
-
if (choice.role === 'designed-lib' && graph.designs.length === 0) {
|
|
163
|
-
reportDesignedLibMissingRoot(projectName);
|
|
190
|
+
if (failsMissingDesignAnnotation(shared, choice.role, graph, projectName)) {
|
|
164
191
|
return { success: false };
|
|
165
192
|
}
|
|
166
193
|
writeDesignFiles(projectRootAbs, projectRoot, graph);
|
|
@@ -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 three 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 * design.html — clickable viz.js page (linked from architecture/dependencies.html)\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 DiAnalyzer,\n frameworkTags,\n explicitRoleTag,\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 { generateDesignHTML } from '../../lib/di-graph/design-visualizer';\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 '@ApiImplementation(',\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\n/**\n * Repo-relative back link from a project's committed design.html up to\n * architecture/dependencies.html, so a reader who clicked a box in the\n * architecture graph can click back out. E.g. 'packages/http/http-api' →\n * '../../../architecture/dependencies.html'.\n */\nfunction architectureBackHref(projectRoot: string): string {\n return path.posix.relative(projectRoot.replace(/\\\\/g, '/'), 'architecture/dependencies.html');\n}\n\nfunction writeDesignFiles(projectRootAbs: string, projectRoot: string, graph: DiGraph): void {\n // toDesignJson sorts the graph in place, so design.md/design.html below all\n // see the same deterministic ordering (no git churn on re-run).\n fs.writeFileSync(path.join(projectRootAbs, 'design.json'), toDesignJson(graph));\n fs.writeFileSync(path.join(projectRootAbs, 'design.md'), toDesignMarkdown(graph));\n fs.writeFileSync(\n path.join(projectRootAbs, 'design.html'),\n generateDesignHTML(graph, architectureBackHref(projectRoot)),\n );\n}\n\n/** The analyzer chosen for a project plus the role tag that drove the choice. */\nclass AnalyzerChoice {\n constructor(\n public readonly analyzer: DiAnalyzer,\n public readonly role: string | null,\n ) {}\n}\n\n/**\n * Select the analyzer by role (server→@Controller, designed-lib→@ApiImplementation,\n * client→angular design, lib→skip). The explicit `role:` nx tag is the source of\n * truth; when absent we fall back to the legacy `framework:` selection + marker\n * pre-scan so designs stay identical until a project is retagged.\n */\nfunction chooseAnalyzer(tags: string[], srcDir: string): AnalyzerChoice {\n const role = explicitRoleTag(tags);\n const frameworks = frameworkTags(tags);\n const analyzer = selectAnalyzer(role, frameworks, detectFrameworkMarkers(srcDir));\n console.log(\n ` Analyzer: ${analyzer.constructor.name} ` +\n `(role tag: ${role ?? 'none'}, framework tags: ${frameworks.length > 0 ? frameworks.join(', ') : 'none'})`,\n );\n return new AnalyzerChoice(analyzer, role);\n}\n\nfunction reportDesignedLibMissingRoot(projectName: string): void {\n console.error(\n `❌ ${projectName} is tagged role:designed-lib but has no @ApiImplementation class.\\n` +\n ` Annotate the library's top implementation class with @ApiImplementation ` +\n `(from @webpieces/http-routing), or retag the project role:lib if it has no design.`,\n );\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, projectRoot, 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, projectRoot, new DiGraph(projectName));\n return { success: true };\n }\n\n const choice = chooseAnalyzer(projectConfig?.tags ?? [], srcDir);\n const graph = choice.analyzer.analyzeProject(program, context.root, projectRoot, projectName);\n\n // A designed-lib MUST expose at least one @ApiImplementation root, else its\n // design would be empty and the tag is meaningless — fail loudly.\n if (choice.role === 'designed-lib' && graph.designs.length === 0) {\n reportDesignedLibMissingRoot(projectName);\n return { success: false };\n }\n\n writeDesignFiles(projectRootAbs, projectRoot, 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 + design.html ` +\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":"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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 three 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 * design.html — clickable viz.js page (linked from architecture/dependencies.html)\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, ResolvedConfig } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n DiAnalyzer,\n frameworkTags,\n explicitRoleTag,\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 { generateDesignHTML } from '../../lib/di-graph/design-visualizer';\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';\nconst MISSING_DESIGN_RULE_NAME = 'missing-design-annotation';\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 '@DocumentDesign(',\n '@provideSingleton',\n '@provideTransient',\n '@injectable',\n 'new ContainerModule',\n '@inject(',\n '@Component(',\n 'bootstrapApplication',\n];\n\nconst ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];\n// A non-Angular DI-design root. When no role tag is set, its presence steers the\n// marker-fallback toward the Inversify analyzer (server/controller mode).\nconst DESIGN_ROOT_MARKER = '@DocumentDesign(';\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(DESIGN_ROOT_MARKER)) controller = true;\n });\n return new FrameworkMarkers(angular, controller);\n}\n\n/**\n * Repo-relative back link from a project's committed design.html up to\n * architecture/dependencies.html, so a reader who clicked a box in the\n * architecture graph can click back out. E.g. 'packages/http/http-api' →\n * '../../../architecture/dependencies.html'.\n */\nfunction architectureBackHref(projectRoot: string): string {\n return path.posix.relative(projectRoot.replace(/\\\\/g, '/'), 'architecture/dependencies.html');\n}\n\nfunction writeDesignFiles(projectRootAbs: string, projectRoot: string, graph: DiGraph): void {\n // toDesignJson sorts the graph in place, so design.md/design.html below all\n // see the same deterministic ordering (no git churn on re-run).\n fs.writeFileSync(path.join(projectRootAbs, 'design.json'), toDesignJson(graph));\n fs.writeFileSync(path.join(projectRootAbs, 'design.md'), toDesignMarkdown(graph));\n fs.writeFileSync(\n path.join(projectRootAbs, 'design.html'),\n generateDesignHTML(graph, architectureBackHref(projectRoot)),\n );\n}\n\n/** The analyzer chosen for a project plus the role tag that drove the choice. */\nclass AnalyzerChoice {\n constructor(\n public readonly analyzer: DiAnalyzer,\n public readonly role: string | null,\n ) {}\n}\n\n/**\n * Select the analyzer by role (server & designed-lib → @DocumentDesign,\n * client→angular design, lib→skip). The explicit `role:` nx tag is the source of\n * truth; when absent we fall back to the legacy `framework:` selection + marker\n * pre-scan so designs stay identical until a project is retagged.\n */\nfunction chooseAnalyzer(tags: string[], srcDir: string): AnalyzerChoice {\n const role = explicitRoleTag(tags);\n const frameworks = frameworkTags(tags);\n const analyzer = selectAnalyzer(role, frameworks, detectFrameworkMarkers(srcDir));\n console.log(\n ` Analyzer: ${analyzer.constructor.name} ` +\n `(role tag: ${role ?? 'none'}, framework tags: ${frameworks.length > 0 ? frameworks.join(', ') : 'none'})`,\n );\n return new AnalyzerChoice(analyzer, role);\n}\n\n/**\n * A server/designed-lib project that produced no design (zero @DocumentDesign\n * roots) fails the build with role-specific guidance. Enforced under the\n * `missing-design-annotation` rule.\n */\nfunction reportMissingDesignAnnotation(projectName: string, role: string): void {\n if (role === 'server') {\n console.error(\n `❌ ${projectName} is tagged role:server but has no @DocumentDesign class.\\n` +\n ` All controllers should be annotated with @DocumentDesign() ` +\n `(from @webpieces/http-routing) so their design.json / design.html get generated\\n` +\n ` and linked from architecture/dependencies.html.`,\n );\n return;\n }\n console.error(\n `❌ ${projectName} is tagged role:designed-lib but has no @DocumentDesign class.\\n` +\n ` One or more classes you want a design printed for need @DocumentDesign() ` +\n `(from @webpieces/http-routing) added —\\n` +\n ` or retag the project role:lib if it has no design.`,\n );\n}\n\n/**\n * A server/designed-lib project MUST expose at least one @DocumentDesign root,\n * else its design is empty and the role is meaningless. Returns true (and reports)\n * when the build should fail. The `missing-design-annotation` rule gates it:\n * absent (an older published config) → enforce; OFF → skip.\n */\nfunction failsMissingDesignAnnotation(\n shared: ResolvedConfig,\n role: string | null,\n graph: DiGraph,\n projectName: string,\n): boolean {\n const missingRule = shared.rules.get(MISSING_DESIGN_RULE_NAME);\n const enforce = !missingRule || !missingRule.isOff;\n if (enforce && graph.designs.length === 0 && (role === 'server' || role === 'designed-lib')) {\n reportMissingDesignAnnotation(projectName, role);\n return true;\n }\n return false;\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, projectRoot, 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, projectRoot, new DiGraph(projectName));\n return { success: true };\n }\n\n const choice = chooseAnalyzer(projectConfig?.tags ?? [], srcDir);\n const graph = choice.analyzer.analyzeProject(program, context.root, projectRoot, projectName);\n\n if (failsMissingDesignAnnotation(shared, choice.role, graph, projectName)) {\n return { success: false };\n }\n\n writeDesignFiles(projectRootAbs, projectRoot, 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 + design.html ` +\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"]}
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* driver is the project's `role` nx tag; the `framework` env set (its libType)
|
|
6
6
|
* only distinguishes the client runtime by set membership:
|
|
7
7
|
*
|
|
8
|
-
* - env set includes `express` → {@link InversifyAnalyzer} (roots on `@
|
|
8
|
+
* - env set includes `express` → {@link InversifyAnalyzer} (roots on `@DocumentDesign`)
|
|
9
9
|
* - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
|
|
10
10
|
* - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)
|
|
11
11
|
*
|
|
12
12
|
* The explicit tags are the source of truth. When the role tag is ABSENT (e.g.
|
|
13
13
|
* before retag), a cheap marker pre-scan corroborates: `@Component(` /
|
|
14
|
-
* `bootstrapApplication` → angular; `@
|
|
14
|
+
* `bootstrapApplication` → angular; `@DocumentDesign(` → Inversify. This makes the
|
|
15
15
|
* committed design.* identical whether selection is tag- or marker-driven.
|
|
16
16
|
*/
|
|
17
17
|
import type * as ts from 'typescript';
|
|
@@ -28,9 +28,10 @@ export interface DiAnalyzer {
|
|
|
28
28
|
analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Inversify analyzer: one design per root
|
|
32
|
-
*
|
|
33
|
-
* - `'
|
|
31
|
+
* Inversify analyzer: one design per `@DocumentDesign` root. The `rootMode` only
|
|
32
|
+
* sets the root box kind:
|
|
33
|
+
* - `'controller'` (server projects) → `controller`.
|
|
34
|
+
* - `'apiImplementation'` (role:designed-lib) → `apiImplementation`.
|
|
34
35
|
*/
|
|
35
36
|
export declare class InversifyAnalyzer implements DiAnalyzer {
|
|
36
37
|
private readonly rootMode;
|
|
@@ -58,8 +59,8 @@ export declare function explicitRoleTag(tags: readonly string[]): string | null;
|
|
|
58
59
|
* membership; `markers` corroborate only when the role tag is ABSENT (rollout
|
|
59
60
|
* fallback keeps pre-retag designs identical).
|
|
60
61
|
*
|
|
61
|
-
* - `server` → Inversify, roots on `@
|
|
62
|
-
* - `designed-lib` → Inversify, roots on `@
|
|
62
|
+
* - `server` → Inversify, roots on `@DocumentDesign` (controller kind)
|
|
63
|
+
* - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)
|
|
63
64
|
* - `client` → Angular design for angular apps; otherwise skip
|
|
64
65
|
* - `lib` → skip (plain libraries get no design)
|
|
65
66
|
* - role absent → legacy framework/marker selection
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* driver is the project's `role` nx tag; the `framework` env set (its libType)
|
|
7
7
|
* only distinguishes the client runtime by set membership:
|
|
8
8
|
*
|
|
9
|
-
* - env set includes `express` → {@link InversifyAnalyzer} (roots on `@
|
|
9
|
+
* - env set includes `express` → {@link InversifyAnalyzer} (roots on `@DocumentDesign`)
|
|
10
10
|
* - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
|
|
11
11
|
* - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)
|
|
12
12
|
*
|
|
13
13
|
* The explicit tags are the source of truth. When the role tag is ABSENT (e.g.
|
|
14
14
|
* before retag), a cheap marker pre-scan corroborates: `@Component(` /
|
|
15
|
-
* `bootstrapApplication` → angular; `@
|
|
15
|
+
* `bootstrapApplication` → angular; `@DocumentDesign(` → Inversify. This makes the
|
|
16
16
|
* committed design.* identical whether selection is tag- or marker-driven.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -37,9 +37,10 @@ class FrameworkMarkers {
|
|
|
37
37
|
}
|
|
38
38
|
exports.FrameworkMarkers = FrameworkMarkers;
|
|
39
39
|
/**
|
|
40
|
-
* Inversify analyzer: one design per root
|
|
41
|
-
*
|
|
42
|
-
* - `'
|
|
40
|
+
* Inversify analyzer: one design per `@DocumentDesign` root. The `rootMode` only
|
|
41
|
+
* sets the root box kind:
|
|
42
|
+
* - `'controller'` (server projects) → `controller`.
|
|
43
|
+
* - `'apiImplementation'` (role:designed-lib) → `apiImplementation`.
|
|
43
44
|
*/
|
|
44
45
|
class InversifyAnalyzer {
|
|
45
46
|
rootMode;
|
|
@@ -103,8 +104,8 @@ function explicitRoleTag(tags) {
|
|
|
103
104
|
* membership; `markers` corroborate only when the role tag is ABSENT (rollout
|
|
104
105
|
* fallback keeps pre-retag designs identical).
|
|
105
106
|
*
|
|
106
|
-
* - `server` → Inversify, roots on `@
|
|
107
|
-
* - `designed-lib` → Inversify, roots on `@
|
|
107
|
+
* - `server` → Inversify, roots on `@DocumentDesign` (controller kind)
|
|
108
|
+
* - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)
|
|
108
109
|
* - `client` → Angular design for angular apps; otherwise skip
|
|
109
110
|
* - `lib` → skip (plain libraries get no design)
|
|
110
111
|
* - role absent → legacy framework/marker selection
|
|
@@ -1 +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;;;
|
|
1
|
+
{"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph. The primary\n * driver is the project's `role` nx tag; the `framework` env set (its libType)\n * only distinguishes the client runtime by set membership:\n *\n * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@DocumentDesign`)\n * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)\n *\n * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.\n * before retag), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@DocumentDesign(` → Inversify. 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, DiRootMode } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\nconst ROLE_TAG_PREFIX = 'role:';\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/**\n * Inversify analyzer: one design per `@DocumentDesign` root. The `rootMode` only\n * sets the root box kind:\n * - `'controller'` (server projects) → `controller`.\n * - `'apiImplementation'` (role:designed-lib) → `apiImplementation`.\n */\nexport class InversifyAnalyzer implements DiAnalyzer {\n private readonly rootMode: DiRootMode;\n\n constructor(rootMode: DiRootMode = 'controller') {\n this.rootMode = rootMode;\n }\n\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);\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 `<prefix><value>` nx tag, or null when the project has none. */\nfunction explicitTag(tags: readonly string[], prefix: string): string | null {\n for (const tag of tags) {\n if (tag.startsWith(prefix)) {\n const value = tag.slice(prefix.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n return explicitTag(tags, FRAMEWORK_TAG_PREFIX);\n}\n\n/** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */\nexport function frameworkTags(tags: readonly string[]): string[] {\n const values: string[] = [];\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) values.push(value);\n }\n }\n return values;\n}\n\n/** Extract the explicit `role:<value>` nx tag, or null when the project has none. */\nexport function explicitRoleTag(tags: readonly string[]): string | null {\n return explicitTag(tags, ROLE_TAG_PREFIX);\n}\n\n/**\n * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is\n * the source of truth for WHAT to root on; `frameworks` is the project's env set\n * (its libType) and distinguishes the client runtime (angular vs other) by set\n * membership; `markers` corroborate only when the role tag is ABSENT (rollout\n * fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@DocumentDesign` (controller kind)\n * - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)\n * - `client` → Angular design for angular apps; otherwise skip\n * - `lib` → skip (plain libraries get no design)\n * - role absent → legacy framework/marker selection\n */\nexport function selectAnalyzer(\n role: string | null,\n frameworks: string[],\n markers: FrameworkMarkers,\n): DiAnalyzer {\n if (role === 'server') return new InversifyAnalyzer('controller');\n if (role === 'designed-lib') return new InversifyAnalyzer('apiImplementation');\n if (role === 'client') return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();\n if (role === 'lib') return new EmptyAnalyzer();\n\n // Role tag absent — fall back to the legacy framework/marker selection so\n // designs stay identical until a project is retagged.\n if (frameworks.includes('express')) return new InversifyAnalyzer('controller');\n if (frameworks.includes('angular')) return new AngularAnalyzer();\n if (frameworks.length > 0) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - toConstantValue/toDynamicValue bindings → leaf nodes, no recursion
|
|
10
10
|
* - unresolvable tokens/types → kind "unresolved" nodes (generation never fails)
|
|
11
11
|
*
|
|
12
|
-
* Roots: @
|
|
12
|
+
* Roots: @DocumentDesign() classes when the project has any; otherwise every
|
|
13
13
|
* DI-registered class in the project that no other project class injects
|
|
14
14
|
* (the tops of the local DAG).
|
|
15
15
|
*/
|
|
@@ -24,12 +24,15 @@ export declare class ParamInjection {
|
|
|
24
24
|
constructor(expr: ts.Expression | null, multi: boolean, optional: boolean, unmanaged: boolean);
|
|
25
25
|
}
|
|
26
26
|
export declare function readParamDecorators(param: ts.ParameterDeclaration): ParamInjection;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
/** A `@DocumentDesign` class — the explicit DI-design root (server controller or designed-lib impl). */
|
|
28
|
+
export declare function isDocumentDesignClass(cls: ts.ClassDeclaration): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The node kind for a `@DocumentDesign` root, chosen by the analyzer's root mode
|
|
31
|
+
* rather than the decorator (a single `@DocumentDesign` marks both). `server` →
|
|
32
|
+
* `controller`, `designed-lib` → `apiImplementation`.
|
|
33
|
+
*/
|
|
32
34
|
export type DiRootMode = 'controller' | 'apiImplementation';
|
|
35
|
+
export declare function rootKindForMode(mode: DiRootMode): DiNodeKind;
|
|
33
36
|
export declare function findConstructor(cls: ts.ClassDeclaration): ts.ConstructorDeclaration | null;
|
|
34
37
|
/** All class declarations in files under the project root. */
|
|
35
38
|
export declare function projectClasses(program: ts.Program, workspaceRoot: string, projectRoot: string): ts.ClassDeclaration[];
|
|
@@ -80,10 +83,16 @@ export declare abstract class DiDesignBuilder {
|
|
|
80
83
|
private readonly unresolvedIds;
|
|
81
84
|
private readonly usedIds;
|
|
82
85
|
private readonly visited;
|
|
86
|
+
protected rootClass: ts.ClassDeclaration | null;
|
|
83
87
|
constructor(checker: ts.TypeChecker, table: BindingTable, workspaceRoot: string, design: DiDesign);
|
|
84
88
|
/** Collect the injection sites for one class (constructor params, field inject(), ...). */
|
|
85
89
|
protected abstract collectInjections(cls: ts.ClassDeclaration): Injection[];
|
|
86
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Node kind for a root/reached class. Default: the ROOT box takes the design's
|
|
92
|
+
* `rootKind` (`controller`/`apiImplementation`, chosen by root mode); every
|
|
93
|
+
* reached dependency is a plain `class`. Angular overrides this to render
|
|
94
|
+
* component classes as `component`.
|
|
95
|
+
*/
|
|
87
96
|
protected rootKindOf(cls: ts.ClassDeclaration): DiNodeKind;
|
|
88
97
|
addRoot(cls: ts.ClassDeclaration): void;
|
|
89
98
|
/**
|
|
@@ -150,10 +159,11 @@ export declare function assignLevels(design: DiDesign): void;
|
|
|
150
159
|
export declare function buildDesign(root: ts.ClassDeclaration, rootKind: DiNodeKind, workspaceRoot: string, makeBuilder: (design: DiDesign) => DiDesignBuilder): DiDesign;
|
|
151
160
|
/**
|
|
152
161
|
* Build the full Inversify DI graph for one project: one self-contained
|
|
153
|
-
* `DiDesign` per @
|
|
162
|
+
* `DiDesign` per @DocumentDesign root. `projectRoot` is workspace-relative.
|
|
154
163
|
*
|
|
155
|
-
* `
|
|
156
|
-
*
|
|
157
|
-
* `
|
|
164
|
+
* Both `rootMode`s root on @DocumentDesign classes; the mode only sets the root
|
|
165
|
+
* box kind (`'controller'` for server, `'apiImplementation'` for designed-lib).
|
|
166
|
+
* `includeLibraryRoots` (default false) lets a project with NO @DocumentDesign
|
|
167
|
+
* class fall back to top-of-DAG DI classes (rendered as plain `class` roots).
|
|
158
168
|
*/
|
|
159
169
|
export declare function buildDiGraph(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string, includeLibraryRoots?: boolean, rootMode?: DiRootMode): DiGraph;
|
|
@@ -10,16 +10,15 @@
|
|
|
10
10
|
* - toConstantValue/toDynamicValue bindings → leaf nodes, no recursion
|
|
11
11
|
* - unresolvable tokens/types → kind "unresolved" nodes (generation never fails)
|
|
12
12
|
*
|
|
13
|
-
* Roots: @
|
|
13
|
+
* Roots: @DocumentDesign() classes when the project has any; otherwise every
|
|
14
14
|
* DI-registered class in the project that no other project class injects
|
|
15
15
|
* (the tops of the local DAG).
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.InversifyDesignBuilder = exports.DiDesignBuilder = exports.Injection = exports.ParamInjection = void 0;
|
|
19
19
|
exports.readParamDecorators = readParamDecorators;
|
|
20
|
-
exports.
|
|
21
|
-
exports.
|
|
22
|
-
exports.rootKindOfClass = rootKindOfClass;
|
|
20
|
+
exports.isDocumentDesignClass = isDocumentDesignClass;
|
|
21
|
+
exports.rootKindForMode = rootKindForMode;
|
|
23
22
|
exports.findConstructor = findConstructor;
|
|
24
23
|
exports.projectClasses = projectClasses;
|
|
25
24
|
exports.byClassName = byClassName;
|
|
@@ -78,20 +77,12 @@ function hasDecoratorNamed(cls, names) {
|
|
|
78
77
|
}
|
|
79
78
|
return false;
|
|
80
79
|
}
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
/** A `@DocumentDesign` class — the explicit DI-design root (server controller or designed-lib impl). */
|
|
81
|
+
function isDocumentDesignClass(cls) {
|
|
82
|
+
return hasDecoratorNamed(cls, new Set(['DocumentDesign']));
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return hasDecoratorNamed(cls, new Set(['ApiImplementation']));
|
|
87
|
-
}
|
|
88
|
-
/** Node kind for a class: `@Controller` → controller, `@ApiImplementation` → apiImplementation, else class. */
|
|
89
|
-
function rootKindOfClass(cls) {
|
|
90
|
-
if (isControllerClass(cls))
|
|
91
|
-
return 'controller';
|
|
92
|
-
if (isApiImplementationClass(cls))
|
|
93
|
-
return 'apiImplementation';
|
|
94
|
-
return 'class';
|
|
84
|
+
function rootKindForMode(mode) {
|
|
85
|
+
return mode === 'apiImplementation' ? 'apiImplementation' : 'controller';
|
|
95
86
|
}
|
|
96
87
|
function isDiRegisteredClass(cls) {
|
|
97
88
|
return hasDecoratorNamed(cls, DI_DECORATORS);
|
|
@@ -179,17 +170,24 @@ class DiDesignBuilder {
|
|
|
179
170
|
unresolvedIds = new Map();
|
|
180
171
|
usedIds = new Set();
|
|
181
172
|
visited = new Set();
|
|
173
|
+
rootClass = null;
|
|
182
174
|
constructor(checker, table, workspaceRoot, design) {
|
|
183
175
|
this.checker = checker;
|
|
184
176
|
this.table = table;
|
|
185
177
|
this.workspaceRoot = workspaceRoot;
|
|
186
178
|
this.design = design;
|
|
187
179
|
}
|
|
188
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Node kind for a root/reached class. Default: the ROOT box takes the design's
|
|
182
|
+
* `rootKind` (`controller`/`apiImplementation`, chosen by root mode); every
|
|
183
|
+
* reached dependency is a plain `class`. Angular overrides this to render
|
|
184
|
+
* component classes as `component`.
|
|
185
|
+
*/
|
|
189
186
|
rootKindOf(cls) {
|
|
190
|
-
return
|
|
187
|
+
return cls === this.rootClass ? this.design.rootKind : 'class';
|
|
191
188
|
}
|
|
192
189
|
addRoot(cls) {
|
|
190
|
+
this.rootClass = cls;
|
|
193
191
|
const id = this.classNode(cls);
|
|
194
192
|
this.design.root = id;
|
|
195
193
|
this.walkClass(cls);
|
|
@@ -524,27 +522,29 @@ function buildDesign(root, rootKind, workspaceRoot, makeBuilder) {
|
|
|
524
522
|
}
|
|
525
523
|
/**
|
|
526
524
|
* Build the full Inversify DI graph for one project: one self-contained
|
|
527
|
-
* `DiDesign` per @
|
|
525
|
+
* `DiDesign` per @DocumentDesign root. `projectRoot` is workspace-relative.
|
|
528
526
|
*
|
|
529
|
-
* `
|
|
530
|
-
*
|
|
531
|
-
* `
|
|
527
|
+
* Both `rootMode`s root on @DocumentDesign classes; the mode only sets the root
|
|
528
|
+
* box kind (`'controller'` for server, `'apiImplementation'` for designed-lib).
|
|
529
|
+
* `includeLibraryRoots` (default false) lets a project with NO @DocumentDesign
|
|
530
|
+
* class fall back to top-of-DAG DI classes (rendered as plain `class` roots).
|
|
532
531
|
*/
|
|
533
532
|
function buildDiGraph(program, workspaceRoot, projectRoot, projectName, includeLibraryRoots = false, rootMode = 'controller') {
|
|
534
533
|
const checker = program.getTypeChecker();
|
|
535
534
|
const table = (0, bindings_1.collectBindings)(program, checker, workspaceRoot);
|
|
536
535
|
const graph = new model_1.DiGraph(projectName);
|
|
537
536
|
const classes = projectClasses(program, workspaceRoot, projectRoot);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
537
|
+
const designRoots = classes.filter((cls) => isDocumentDesignClass(cls));
|
|
538
|
+
// Both modes root on @DocumentDesign classes. In controller mode a project with
|
|
539
|
+
// no explicit design root may fall back to top-of-DAG DI classes (those render
|
|
540
|
+
// as plain `class` roots, preserving prior behaviour).
|
|
541
|
+
const roots = designRoots.length > 0
|
|
542
|
+
? designRoots
|
|
543
|
+
: rootMode === 'controller' && includeLibraryRoots
|
|
544
|
+
? findLibraryRoots(classes, checker, table, workspaceRoot)
|
|
545
|
+
: [];
|
|
546
546
|
for (const root of [...roots].sort(byClassName)) {
|
|
547
|
-
const rootKind =
|
|
547
|
+
const rootKind = isDocumentDesignClass(root) ? rootKindForMode(rootMode) : 'class';
|
|
548
548
|
graph.designs.push(buildDesign(root, rootKind, workspaceRoot, (design) => new InversifyDesignBuilder(checker, table, workspaceRoot, design)));
|
|
549
549
|
}
|
|
550
550
|
return graph;
|