@webpieces/nx-webpieces-rules 0.3.252 → 0.3.253

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.3.252",
3
+ "version": "0.3.253",
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.252",
25
- "@webpieces/code-rules": "0.3.252",
26
- "@webpieces/eslint-rules": "0.3.252",
27
- "@webpieces/pr-gate": "0.3.252",
28
- "@webpieces/rules-config": "0.3.252",
24
+ "@webpieces/ai-hook-rules": "0.3.253",
25
+ "@webpieces/code-rules": "0.3.253",
26
+ "@webpieces/eslint-rules": "0.3.253",
27
+ "@webpieces/pr-gate": "0.3.253",
28
+ "@webpieces/rules-config": "0.3.253",
29
29
  "madge": "8.0.0"
30
30
  },
31
31
  "peerDependencies": {
@@ -36,6 +36,7 @@ const RULE_NAME = 'di-graph';
36
36
  // Inversify decorator isn't short-circuited to empty.
37
37
  const DI_MARKERS = [
38
38
  '@Controller(',
39
+ '@ApiImplementation(',
39
40
  '@provideSingleton',
40
41
  '@provideTransient',
41
42
  '@injectable',
@@ -86,6 +87,34 @@ function writeDesignFiles(projectRootAbs, graph) {
86
87
  fs.writeFileSync(path.join(projectRootAbs, 'design.json'), (0, serializer_1.toDesignJson)(graph));
87
88
  fs.writeFileSync(path.join(projectRootAbs, 'design.md'), (0, mermaid_1.toDesignMarkdown)(graph));
88
89
  }
90
+ /** The analyzer chosen for a project plus the role tag that drove the choice. */
91
+ class AnalyzerChoice {
92
+ analyzer;
93
+ role;
94
+ constructor(analyzer, role) {
95
+ this.analyzer = analyzer;
96
+ this.role = role;
97
+ }
98
+ }
99
+ /**
100
+ * Select the analyzer by role (server→@Controller, designed-lib→@ApiImplementation,
101
+ * client→angular design, lib→skip). The explicit `role:` nx tag is the source of
102
+ * truth; when absent we fall back to the legacy `framework:` selection + marker
103
+ * pre-scan so designs stay identical until a project is retagged.
104
+ */
105
+ function chooseAnalyzer(tags, srcDir) {
106
+ const role = (0, analyzer_strategy_1.explicitRoleTag)(tags);
107
+ const framework = (0, analyzer_strategy_1.explicitFrameworkTag)(tags);
108
+ const analyzer = (0, analyzer_strategy_1.selectAnalyzer)(role, framework, detectFrameworkMarkers(srcDir));
109
+ console.log(` Analyzer: ${analyzer.constructor.name} ` +
110
+ `(role tag: ${role ?? 'none'}, framework tag: ${framework ?? 'none'})`);
111
+ return new AnalyzerChoice(analyzer, role);
112
+ }
113
+ function reportDesignedLibMissingRoot(projectName) {
114
+ console.error(`❌ ${projectName} is tagged role:designed-lib but has no @ApiImplementation class.\n` +
115
+ ` Annotate the library's top implementation class with @ApiImplementation ` +
116
+ `(from @webpieces/http-routing), or retag the project role:lib if it has no design.`);
117
+ }
89
118
  async function runExecutor(_options, context) {
90
119
  const shared = (0, rules_config_1.loadAndValidate)(context.root).resolved;
91
120
  const rule = shared.rules.get(RULE_NAME);
@@ -112,13 +141,14 @@ async function runExecutor(_options, context) {
112
141
  writeDesignFiles(projectRootAbs, new model_1.DiGraph(projectName));
113
142
  return { success: true };
114
143
  }
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);
144
+ const choice = chooseAnalyzer(projectConfig?.tags ?? [], srcDir);
145
+ const graph = choice.analyzer.analyzeProject(program, context.root, projectRoot, projectName);
146
+ // A designed-lib MUST expose at least one @ApiImplementation root, else its
147
+ // design would be empty and the tag is meaningless — fail loudly.
148
+ if (choice.role === 'designed-lib' && graph.designs.length === 0) {
149
+ reportDesignedLibMissingRoot(projectName);
150
+ return { success: false };
151
+ }
122
152
  writeDesignFiles(projectRootAbs, graph);
123
153
  const nodeCount = graph.designs.reduce((sum, d) => sum + d.nodes.length, 0);
124
154
  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 {\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"]}
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 {\n DiAnalyzer,\n explicitFrameworkTag,\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 { 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\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\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 framework = explicitFrameworkTag(tags);\n const analyzer = selectAnalyzer(role, framework, detectFrameworkMarkers(srcDir));\n console.log(\n ` Analyzer: ${analyzer.constructor.name} ` +\n `(role tag: ${role ?? 'none'}, framework tag: ${framework ?? '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, 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 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, 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"]}
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import type * as ts from 'typescript';
18
18
  import { DiGraph } from './model';
19
+ import { DiRootMode } from './analyzer';
19
20
  /** Which framework a source tree's decorators point at (marker pre-scan fallback). */
20
21
  export declare class FrameworkMarkers {
21
22
  angular: boolean;
@@ -26,8 +27,14 @@ export declare class FrameworkMarkers {
26
27
  export interface DiAnalyzer {
27
28
  analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
28
29
  }
29
- /** Inversify (express): one design per `@Controller` root. Library roots deferred (v1). */
30
+ /**
31
+ * Inversify analyzer: one design per root decorator.
32
+ * - `'controller'` (server projects) roots on `@Controller`.
33
+ * - `'apiImplementation'` (role:designed-lib) roots on `@ApiImplementation`.
34
+ */
30
35
  export declare class InversifyAnalyzer implements DiAnalyzer {
36
+ private readonly rootMode;
37
+ constructor(rootMode?: DiRootMode);
31
38
  analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;
32
39
  }
33
40
  /** Angular: one design per entry component (bootstrap + routed). */
@@ -40,8 +47,18 @@ export declare class EmptyAnalyzer implements DiAnalyzer {
40
47
  }
41
48
  /** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */
42
49
  export declare function explicitFrameworkTag(tags: readonly string[]): string | null;
50
+ /** Extract the explicit `role:<value>` nx tag, or null when the project has none. */
51
+ export declare function explicitRoleTag(tags: readonly string[]): string | null;
43
52
  /**
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.
53
+ * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is
54
+ * the source of truth for WHAT to root on; `framework` distinguishes the client
55
+ * runtime (angular vs other); `markers` corroborate only when the role tag is
56
+ * ABSENT (rollout fallback keeps pre-retag designs identical).
57
+ *
58
+ * - `server` → Inversify, roots on `@Controller`
59
+ * - `designed-lib` → Inversify, roots on `@ApiImplementation`
60
+ * - `client` → Angular design for angular apps; otherwise skip
61
+ * - `lib` → skip (plain libraries get no design)
62
+ * - role absent → legacy framework/marker selection
46
63
  */
47
- export declare function selectAnalyzer(framework: string | null, markers: FrameworkMarkers): DiAnalyzer;
64
+ export declare function selectAnalyzer(role: string | null, framework: string | null, markers: FrameworkMarkers): DiAnalyzer;
@@ -18,11 +18,13 @@
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.EmptyAnalyzer = exports.AngularAnalyzer = exports.InversifyAnalyzer = exports.FrameworkMarkers = void 0;
20
20
  exports.explicitFrameworkTag = explicitFrameworkTag;
21
+ exports.explicitRoleTag = explicitRoleTag;
21
22
  exports.selectAnalyzer = selectAnalyzer;
22
23
  const model_1 = require("./model");
23
24
  const analyzer_1 = require("./analyzer");
24
25
  const angular_analyzer_1 = require("./angular-analyzer");
25
26
  const FRAMEWORK_TAG_PREFIX = 'framework:';
27
+ const ROLE_TAG_PREFIX = 'role:';
26
28
  /** Which framework a source tree's decorators point at (marker pre-scan fallback). */
27
29
  class FrameworkMarkers {
28
30
  angular;
@@ -33,10 +35,18 @@ class FrameworkMarkers {
33
35
  }
34
36
  }
35
37
  exports.FrameworkMarkers = FrameworkMarkers;
36
- /** Inversify (express): one design per `@Controller` root. Library roots deferred (v1). */
38
+ /**
39
+ * Inversify analyzer: one design per root decorator.
40
+ * - `'controller'` (server projects) roots on `@Controller`.
41
+ * - `'apiImplementation'` (role:designed-lib) roots on `@ApiImplementation`.
42
+ */
37
43
  class InversifyAnalyzer {
44
+ rootMode;
45
+ constructor(rootMode = 'controller') {
46
+ this.rootMode = rootMode;
47
+ }
38
48
  analyzeProject(program, workspaceRoot, projectRoot, projectName) {
39
- return (0, analyzer_1.buildDiGraph)(program, workspaceRoot, projectRoot, projectName);
49
+ return (0, analyzer_1.buildDiGraph)(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);
40
50
  }
41
51
  }
42
52
  exports.InversifyAnalyzer = InversifyAnalyzer;
@@ -54,33 +64,58 @@ class EmptyAnalyzer {
54
64
  }
55
65
  }
56
66
  exports.EmptyAnalyzer = EmptyAnalyzer;
57
- /** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */
58
- function explicitFrameworkTag(tags) {
67
+ /** Extract the explicit `<prefix><value>` nx tag, or null when the project has none. */
68
+ function explicitTag(tags, prefix) {
59
69
  for (const tag of tags) {
60
- if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {
61
- const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();
70
+ if (tag.startsWith(prefix)) {
71
+ const value = tag.slice(prefix.length).trim();
62
72
  if (value.length > 0)
63
73
  return value;
64
74
  }
65
75
  }
66
76
  return null;
67
77
  }
78
+ /** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */
79
+ function explicitFrameworkTag(tags) {
80
+ return explicitTag(tags, FRAMEWORK_TAG_PREFIX);
81
+ }
82
+ /** Extract the explicit `role:<value>` nx tag, or null when the project has none. */
83
+ function explicitRoleTag(tags) {
84
+ return explicitTag(tags, ROLE_TAG_PREFIX);
85
+ }
68
86
  /**
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.
87
+ * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is
88
+ * the source of truth for WHAT to root on; `framework` distinguishes the client
89
+ * runtime (angular vs other); `markers` corroborate only when the role tag is
90
+ * ABSENT (rollout fallback keeps pre-retag designs identical).
91
+ *
92
+ * - `server` → Inversify, roots on `@Controller`
93
+ * - `designed-lib` → Inversify, roots on `@ApiImplementation`
94
+ * - `client` → Angular design for angular apps; otherwise skip
95
+ * - `lib` → skip (plain libraries get no design)
96
+ * - role absent → legacy framework/marker selection
71
97
  */
72
- function selectAnalyzer(framework, markers) {
98
+ function selectAnalyzer(role, framework, markers) {
99
+ if (role === 'server')
100
+ return new InversifyAnalyzer('controller');
101
+ if (role === 'designed-lib')
102
+ return new InversifyAnalyzer('apiImplementation');
103
+ if (role === 'client')
104
+ return framework === 'angular' ? new AngularAnalyzer() : new EmptyAnalyzer();
105
+ if (role === 'lib')
106
+ return new EmptyAnalyzer();
107
+ // Role tag absent — fall back to the legacy framework/marker selection so
108
+ // designs stay identical until a project is retagged.
73
109
  if (framework === 'express')
74
- return new InversifyAnalyzer();
110
+ return new InversifyAnalyzer('controller');
75
111
  if (framework === 'angular')
76
112
  return new AngularAnalyzer();
77
113
  if (framework !== null)
78
114
  return new EmptyAnalyzer();
79
- // Tag absent — fall back to marker corroboration.
80
115
  if (markers.angular)
81
116
  return new AngularAnalyzer();
82
117
  if (markers.controller)
83
- return new InversifyAnalyzer();
118
+ return new InversifyAnalyzer('controller');
84
119
  return new EmptyAnalyzer();
85
120
  }
86
121
  //# sourceMappingURL=analyzer-strategy.js.map
@@ -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;;;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"]}
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, 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, 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 root decorator.\n * - `'controller'` (server projects) roots on `@Controller`.\n * - `'apiImplementation'` (role:designed-lib) roots on `@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 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 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; `framework` distinguishes the client\n * runtime (angular vs other); `markers` corroborate only when the role tag is\n * ABSENT (rollout fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@Controller`\n * - `designed-lib` → Inversify, roots on `@ApiImplementation`\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 framework: string | null,\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 framework === '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 (framework === 'express') return new InversifyAnalyzer('controller');\n if (framework === 'angular') return new AngularAnalyzer();\n if (framework !== null) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
@@ -25,6 +25,11 @@ export declare class ParamInjection {
25
25
  }
26
26
  export declare function readParamDecorators(param: ts.ParameterDeclaration): ParamInjection;
27
27
  export declare function isControllerClass(cls: ts.ClassDeclaration): boolean;
28
+ /** A `@ApiImplementation` class — the explicit DI-design root for a `role:designed-lib` project. */
29
+ export declare function isApiImplementationClass(cls: ts.ClassDeclaration): boolean;
30
+ /** Node kind for a class: `@Controller` → controller, `@ApiImplementation` → apiImplementation, else class. */
31
+ export declare function rootKindOfClass(cls: ts.ClassDeclaration): DiNodeKind;
32
+ export type DiRootMode = 'controller' | 'apiImplementation';
28
33
  export declare function findConstructor(cls: ts.ClassDeclaration): ts.ConstructorDeclaration | null;
29
34
  /** All class declarations in files under the project root. */
30
35
  export declare function projectClasses(program: ts.Program, workspaceRoot: string, projectRoot: string): ts.ClassDeclaration[];
@@ -78,7 +83,7 @@ export declare abstract class DiDesignBuilder {
78
83
  constructor(checker: ts.TypeChecker, table: BindingTable, workspaceRoot: string, design: DiDesign);
79
84
  /** Collect the injection sites for one class (constructor params, field inject(), ...). */
80
85
  protected abstract collectInjections(cls: ts.ClassDeclaration): Injection[];
81
- /** Node kind for a root/reached class — `controller` (Inversify) or `component` (Angular). */
86
+ /** Node kind for a root/reached class — `controller`/`apiImplementation` (Inversify) or `component` (Angular). */
82
87
  protected rootKindOf(cls: ts.ClassDeclaration): DiNodeKind;
83
88
  addRoot(cls: ts.ClassDeclaration): void;
84
89
  /**
@@ -147,8 +152,8 @@ export declare function buildDesign(root: ts.ClassDeclaration, rootKind: DiNodeK
147
152
  * Build the full Inversify DI graph for one project: one self-contained
148
153
  * `DiDesign` per @Controller root. `projectRoot` is workspace-relative.
149
154
  *
150
- * `includeLibraryRoots` (default false, the v1 executor path) roots ONLY on
151
- * @Controller classes; when true, a controller-less project falls back to its
152
- * top-of-DAG DI classes (the deferred library behavior, still unit-tested).
155
+ * `includeLibraryRoots` (default false) roots ONLY on @Controller classes; when
156
+ * true, a controller-less project falls back to top-of-DAG DI classes. `rootMode`
157
+ * `'apiImplementation'` (role:designed-lib) instead roots on `@ApiImplementation`.
153
158
  */
154
- export declare function buildDiGraph(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string, includeLibraryRoots?: boolean): DiGraph;
159
+ export declare function buildDiGraph(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string, includeLibraryRoots?: boolean, rootMode?: DiRootMode): DiGraph;
@@ -18,6 +18,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.InversifyDesignBuilder = exports.DiDesignBuilder = exports.Injection = exports.ParamInjection = void 0;
19
19
  exports.readParamDecorators = readParamDecorators;
20
20
  exports.isControllerClass = isControllerClass;
21
+ exports.isApiImplementationClass = isApiImplementationClass;
22
+ exports.rootKindOfClass = rootKindOfClass;
21
23
  exports.findConstructor = findConstructor;
22
24
  exports.projectClasses = projectClasses;
23
25
  exports.byClassName = byClassName;
@@ -79,6 +81,18 @@ function hasDecoratorNamed(cls, names) {
79
81
  function isControllerClass(cls) {
80
82
  return hasDecoratorNamed(cls, new Set(['Controller']));
81
83
  }
84
+ /** A `@ApiImplementation` class — the explicit DI-design root for a `role:designed-lib` project. */
85
+ function isApiImplementationClass(cls) {
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';
95
+ }
82
96
  function isDiRegisteredClass(cls) {
83
97
  return hasDecoratorNamed(cls, DI_DECORATORS);
84
98
  }
@@ -171,9 +185,9 @@ class DiDesignBuilder {
171
185
  this.workspaceRoot = workspaceRoot;
172
186
  this.design = design;
173
187
  }
174
- /** Node kind for a root/reached class — `controller` (Inversify) or `component` (Angular). */
188
+ /** Node kind for a root/reached class — `controller`/`apiImplementation` (Inversify) or `component` (Angular). */
175
189
  rootKindOf(cls) {
176
- return isControllerClass(cls) ? 'controller' : 'class';
190
+ return rootKindOfClass(cls);
177
191
  }
178
192
  addRoot(cls) {
179
193
  const id = this.classNode(cls);
@@ -512,23 +526,25 @@ function buildDesign(root, rootKind, workspaceRoot, makeBuilder) {
512
526
  * Build the full Inversify DI graph for one project: one self-contained
513
527
  * `DiDesign` per @Controller root. `projectRoot` is workspace-relative.
514
528
  *
515
- * `includeLibraryRoots` (default false, the v1 executor path) roots ONLY on
516
- * @Controller classes; when true, a controller-less project falls back to its
517
- * top-of-DAG DI classes (the deferred library behavior, still unit-tested).
529
+ * `includeLibraryRoots` (default false) roots ONLY on @Controller classes; when
530
+ * true, a controller-less project falls back to top-of-DAG DI classes. `rootMode`
531
+ * `'apiImplementation'` (role:designed-lib) instead roots on `@ApiImplementation`.
518
532
  */
519
- function buildDiGraph(program, workspaceRoot, projectRoot, projectName, includeLibraryRoots = false) {
533
+ function buildDiGraph(program, workspaceRoot, projectRoot, projectName, includeLibraryRoots = false, rootMode = 'controller') {
520
534
  const checker = program.getTypeChecker();
521
535
  const table = (0, bindings_1.collectBindings)(program, checker, workspaceRoot);
522
536
  const graph = new model_1.DiGraph(projectName);
523
537
  const classes = projectClasses(program, workspaceRoot, projectRoot);
524
- const controllers = classes.filter((cls) => isControllerClass(cls));
525
- const roots = controllers.length > 0
526
- ? controllers
527
- : includeLibraryRoots
528
- ? findLibraryRoots(classes, checker, table, workspaceRoot)
529
- : [];
538
+ const roots = rootMode === 'apiImplementation'
539
+ ? classes.filter((cls) => isApiImplementationClass(cls))
540
+ : (() => {
541
+ const controllers = classes.filter((cls) => isControllerClass(cls));
542
+ if (controllers.length > 0)
543
+ return controllers;
544
+ return includeLibraryRoots ? findLibraryRoots(classes, checker, table, workspaceRoot) : [];
545
+ })();
530
546
  for (const root of [...roots].sort(byClassName)) {
531
- const rootKind = isControllerClass(root) ? 'controller' : 'class';
547
+ const rootKind = rootKindOfClass(root);
532
548
  graph.designs.push(buildDesign(root, rootKind, workspaceRoot, (design) => new InversifyDesignBuilder(checker, table, workspaceRoot, design)));
533
549
  }
534
550
  return graph;