@webpieces/nx-webpieces-rules 0.4.398 → 0.4.400
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/generate/executor.js +13 -2
- package/src/executors/generate/executor.js.map +1 -1
- package/src/lib/api-usage/api-relations-validator.d.ts +7 -1
- package/src/lib/api-usage/api-relations-validator.js +27 -1
- package/src/lib/api-usage/api-relations-validator.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +58 -6
- package/src/lib/api-usage/api-scanner.js +184 -32
- package/src/lib/api-usage/api-scanner.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.400",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
25
|
-
"@webpieces/code-rules": "0.4.
|
|
26
|
-
"@webpieces/eslint-rules": "0.4.
|
|
27
|
-
"@webpieces/pr-gate": "0.4.
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.4.400",
|
|
25
|
+
"@webpieces/code-rules": "0.4.400",
|
|
26
|
+
"@webpieces/eslint-rules": "0.4.400",
|
|
27
|
+
"@webpieces/pr-gate": "0.4.400",
|
|
28
|
+
"@webpieces/rules-config": "0.4.400",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -32,6 +32,18 @@ function generateRuntimeGraph(workspaceRoot, graph, hiddenProjects) {
|
|
|
32
32
|
const serviceCount = Object.keys(runtimeGraph.services).length;
|
|
33
33
|
console.log(`✅ Runtime graph saved (${serviceCount} services, ${runtimeGraph.runtimeEdges.length} runtime edges)`);
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Scan for api relations and attach them, surfacing any contract we could NOT map back to source.
|
|
37
|
+
* Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —
|
|
38
|
+
* a green run that quietly omits a service gets committed, rendered, and trusted.
|
|
39
|
+
*/
|
|
40
|
+
// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file
|
|
41
|
+
function scanApiRelations(workspaceRoot, graph, projectInfos) {
|
|
42
|
+
console.log('🔎 Scanning source for implements/uses API relations...');
|
|
43
|
+
const scan = (0, api_scanner_1.scanAndAttachApiRelations)(workspaceRoot, graph, projectInfos);
|
|
44
|
+
if (scan.unresolvedApiCalls.length > 0)
|
|
45
|
+
console.warn((0, api_scanner_1.describeUnresolvedApiCalls)(scan.unresolvedApiCalls));
|
|
46
|
+
}
|
|
35
47
|
async function runExecutor(options, context) {
|
|
36
48
|
const graphPath = options.graphPath;
|
|
37
49
|
const workspaceRoot = context.root;
|
|
@@ -53,8 +65,7 @@ async function runExecutor(options, context) {
|
|
|
53
65
|
// Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by
|
|
54
66
|
// scanning source, so dependencies.json + the viz + the runtime graph all
|
|
55
67
|
// read the same derived truth.
|
|
56
|
-
|
|
57
|
-
(0, api_scanner_1.scanAndAttachApiRelations)(workspaceRoot, enhancedGraph, projectInfos);
|
|
68
|
+
scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);
|
|
58
69
|
// Step 4: Save the graph
|
|
59
70
|
console.log('💾 Saving graph to architecture/dependencies.json...');
|
|
60
71
|
(0, graph_loader_1.saveGraph)(enhancedGraph, workspaceRoot, graphPath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json.\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { saveGraph } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { ProjectInfo } from '../../lib/project-info';\nimport { scanAndAttachApiRelations, describeUnresolvedApiCalls } from '../../lib/api-usage/api-scanner';\nimport type { EnhancedGraph } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraph, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(workspaceRoot: string, graph: EnhancedGraph, hiddenProjects: Set<string>): void {\n console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');\n const runtimeGraph = deriveRuntimeGraph(graph, hiddenProjects);\n saveRuntimeGraph(runtimeGraph, workspaceRoot);\n const serviceCount = Object.keys(runtimeGraph.services).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${runtimeGraph.runtimeEdges.length} runtime edges)`,\n );\n}\n\n/**\n * Scan for api relations and attach them, surfacing any contract we could NOT map back to source.\n * Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —\n * a green run that quietly omits a service gets committed, rendered, and trusted.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction scanApiRelations(workspaceRoot: string, graph: EnhancedGraph, projectInfos: Map<string, ProjectInfo>): void {\n console.log('🔎 Scanning source for implements/uses API relations...');\n const scan = scanAndAttachApiRelations(workspaceRoot, graph, projectInfos);\n if (scan.unresolvedApiCalls.length > 0) console.warn(describeUnresolvedApiCalls(scan.unresolvedApiCalls));\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n\n // Step 4: Save the graph\n console.log('💾 Saving graph to architecture/dependencies.json...');\n saveGraph(enhancedGraph, workspaceRoot, graphPath);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n const hiddenProjects = new Set<string>();\n for (const name of Object.keys(enhancedGraph)) {\n if (enhancedGraph[name].drawOnGraph === false) hiddenProjects.add(name);\n }\n generateRuntimeGraph(workspaceRoot, enhancedGraph, hiddenProjects);\n\n // Print summary\n const projectCount = Object.keys(enhancedGraph).length;\n const levels = new Set(Object.values(enhancedGraph).map((e) => e.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${projectCount}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('❌ Graph generation failed:', error.message);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { EnhancedGraph } from '../graph-sorter';
|
|
14
14
|
import { ProjectInfo } from '../project-info';
|
|
15
|
-
import { ApiScanResult } from './api-scanner';
|
|
15
|
+
import { ApiScanResult, UnresolvedApiCall } from './api-scanner';
|
|
16
16
|
/** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */
|
|
17
17
|
export interface UnclassifiedApiDep {
|
|
18
18
|
project: string;
|
|
@@ -20,6 +20,12 @@ export interface UnclassifiedApiDep {
|
|
|
20
20
|
apiLib: string;
|
|
21
21
|
/** The API contract class names that api-lib exports (for the fix hint). */
|
|
22
22
|
apis: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Contracts this project DOES name at a call site but which never resolved to decorated
|
|
25
|
+
* source. When non-empty the wiring almost certainly exists and the scan is blind — advice
|
|
26
|
+
* to "add a controller" or "remove the dependency" would be actively wrong.
|
|
27
|
+
*/
|
|
28
|
+
unresolved: UnresolvedApiCall[];
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Every server/client → api-lib edge for which the scan found NO implements and
|
|
@@ -52,14 +52,40 @@ function findUnclassifiedApiDeps(graph, projectInfos, scan) {
|
|
|
52
52
|
continue;
|
|
53
53
|
if (entry.apiRelations && entry.apiRelations[dep])
|
|
54
54
|
continue;
|
|
55
|
-
violations.push({
|
|
55
|
+
violations.push({
|
|
56
|
+
project: projectName,
|
|
57
|
+
role,
|
|
58
|
+
apiLib: dep,
|
|
59
|
+
apis: apisOwnedBy(scan, dep),
|
|
60
|
+
unresolved: scan.unresolvedApiCalls.filter((c) => c.project === projectName),
|
|
61
|
+
});
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
return violations;
|
|
59
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The project DOES wire this contract up — we just couldn't see it, because the import resolved
|
|
68
|
+
* to a decorator-erased declaration file. Report THAT, not a list of dead ends: telling a dev to
|
|
69
|
+
* register a controller they already registered, or to delete a load-bearing dependency, sends
|
|
70
|
+
* them chasing ghosts and gets the whole validator turned off.
|
|
71
|
+
*/
|
|
72
|
+
// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style
|
|
73
|
+
function describeBlindScan(violation) {
|
|
74
|
+
const lines = [
|
|
75
|
+
` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' and its ` +
|
|
76
|
+
`wiring IS present in source, but the contract could not be read:`,
|
|
77
|
+
];
|
|
78
|
+
for (const call of violation.unresolved) {
|
|
79
|
+
lines.push(` • ${call.api} at ${call.at} resolved to ${call.declaredIn} (decorators erased in .d.ts).`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(` This is a CONFIG gap, not a wiring gap. Do NOT add a controller and do NOT remove the dependency.`, ` Fix: add a tsconfig.base.json 'paths' entry for '${violation.apiLib}' → its src/index.ts,`, ` so the import resolves to source instead of dist/.`);
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
60
84
|
/** Human-readable, fix-oriented report for one unclassified dependency. */
|
|
61
85
|
// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style
|
|
62
86
|
function describeUnclassifiedApiDep(violation) {
|
|
87
|
+
if (violation.unresolved.length > 0)
|
|
88
|
+
return describeBlindScan(violation);
|
|
63
89
|
const apiHint = violation.apis.length > 0 ? violation.apis.join(', ') : 'the API';
|
|
64
90
|
const theApi = violation.apis[0] ?? 'TheApi';
|
|
65
91
|
const lines = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-relations-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations-validator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;
|
|
1
|
+
{"version":3,"file":"api-relations-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Relations Validator\n *\n * Enforces that the dependency graph is TRUTHFUL: a runnable project (role server\n * or client) that compiles against an api-lib must actually IMPLEMENT (serve) or\n * USE (call) at least one of its APIs. A dependency that is neither is almost\n * always a mistake — a forgotten client wiring, a dead import, or a controller\n * that was never registered — and it would draw an unexplained edge in the graph.\n *\n * The check reuses the same source scan that produces `apiRelations`, so \"does P\n * relate to api-lib D\" is answered by real code, never a declaration.\n */\n\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult, UnresolvedApiCall } from './api-scanner';\n\n/** Roles that must justify every api-lib dependency (top-level runnables). */\nconst CHECKED_ROLES: ReadonlyArray<string> = ['server', 'client'];\n\n/** One `role:server`/`role:client` project that depends on an api-lib it neither implements nor uses. */\nexport interface UnclassifiedApiDep {\n project: string;\n role: string;\n apiLib: string;\n /** The API contract class names that api-lib exports (for the fix hint). */\n apis: string[];\n /**\n * Contracts this project DOES name at a call site but which never resolved to decorated\n * source. When non-empty the wiring almost certainly exists and the scan is blind — advice\n * to \"add a controller\" or \"remove the dependency\" would be actively wrong.\n */\n unresolved: UnresolvedApiCall[];\n}\n\n/** The API class names owned by `apiLib`, sorted (for a stable fix hint). */\n// webpieces-disable no-function-outside-class -- pure lookup helper, matches the validator-lib style\nfunction apisOwnedBy(scan: ApiScanResult, apiLib: string): string[] {\n const names: string[] = [];\n for (const info of scan.apiIndex.values()) {\n if (info.owner === apiLib) names.push(info.api);\n }\n return names.sort();\n}\n\n/**\n * Every server/client → api-lib edge for which the scan found NO implements and\n * NO uses. `graph` must already carry `apiRelations` (call scanAndAttachApiRelations first).\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors findUnclassified-style validators\nexport function findUnclassifiedApiDeps(\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n): UnclassifiedApiDep[] {\n const violations: UnclassifiedApiDep[] = [];\n for (const projectName of Object.keys(graph)) {\n const info = projectInfos.get(projectName);\n if (!info) continue;\n const role = resolveRole(info).role;\n if (role === null || !CHECKED_ROLES.includes(role)) continue;\n // Only flag projects whose production source was actually scanned. An all-test project\n // (e.g. an e2e harness whose only files are *.spec.ts) is never observed, so we can't\n // conclude its api-lib dependency is unused.\n if (!scan.scannedProjects.has(projectName)) continue;\n\n const entry = graph[projectName];\n for (const dep of entry.dependsOn) {\n if (!scan.apiLibProjects.has(dep)) continue;\n if (entry.apiRelations && entry.apiRelations[dep]) continue;\n violations.push({\n project: projectName,\n role,\n apiLib: dep,\n apis: apisOwnedBy(scan, dep),\n unresolved: scan.unresolvedApiCalls.filter((c: UnresolvedApiCall) => c.project === projectName),\n });\n }\n }\n return violations;\n}\n\n/**\n * The project DOES wire this contract up — we just couldn't see it, because the import resolved\n * to a decorator-erased declaration file. Report THAT, not a list of dead ends: telling a dev to\n * register a controller they already registered, or to delete a load-bearing dependency, sends\n * them chasing ghosts and gets the whole validator turned off.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nfunction describeBlindScan(violation: UnclassifiedApiDep): string {\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' and its ` +\n `wiring IS present in source, but the contract could not be read:`,\n ];\n for (const call of violation.unresolved) {\n lines.push(\n ` • ${call.api} at ${call.at} resolved to ${call.declaredIn} (decorators erased in .d.ts).`,\n );\n }\n lines.push(\n ` This is a CONFIG gap, not a wiring gap. Do NOT add a controller and do NOT remove the dependency.`,\n ` Fix: add a tsconfig.base.json 'paths' entry for '${violation.apiLib}' → its src/index.ts,`,\n ` so the import resolves to source instead of dist/.`,\n );\n return lines.join('\\n');\n}\n\n/** Human-readable, fix-oriented report for one unclassified dependency. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches the validator-lib style\nexport function describeUnclassifiedApiDep(violation: UnclassifiedApiDep): string {\n if (violation.unresolved.length > 0) return describeBlindScan(violation);\n const apiHint = violation.apis.length > 0 ? violation.apis.join(', ') : 'the API';\n const theApi = violation.apis[0] ?? 'TheApi';\n const lines = [\n ` ❌ '${violation.project}' (role:${violation.role}) depends on api-lib '${violation.apiLib}' ` +\n `but neither IMPLEMENTS nor USES any of its APIs (${apiHint}).`,\n ` Do ONE of:`,\n ` 1. USE it as a client: inject ClientHttpFactory (@webpieces/http-client-node) or ` +\n `ClientHttpBrowserFactory (@webpieces/http-client-browser) and call ` +\n `factory.createRpcClient(${theApi}, config); for a @PubSub api use ` +\n `ClientCloudTasksFactory.createPubSubClient(${theApi}, config).`,\n ` 2. IMPLEMENT it: add a controller and register it — ` +\n `apiFactory.addRoutes(${theApi}, TheController).`,\n ` 3. If the dependency is unused, remove '${violation.apiLib}' from '${violation.project}'.`,\n ];\n return lines.join('\\n');\n}\n"]}
|
|
@@ -16,10 +16,43 @@
|
|
|
16
16
|
* `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client
|
|
17
17
|
* An api-lib is DETECTED, not tagged: a project exporting an `abstract class`
|
|
18
18
|
* carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.
|
|
19
|
+
*
|
|
20
|
+
* Contracts are indexed from SOURCE in a pre-pass (ApiSourceIndexBuilder) rather than
|
|
21
|
+
* from wherever the checker resolves an import to. A consumer without a tsconfig.base
|
|
22
|
+
* `paths` entry resolves `import { XxxApi } from '@scope/xxx-api'` through node_modules
|
|
23
|
+
* to the package's BUILT `dist/**.d.ts` — and tsc ERASES decorators when emitting
|
|
24
|
+
* declarations, so `@ApiPath` can never be read there. Keying off the resolved
|
|
25
|
+
* declaration therefore dropped whole services from the graph, silently. See
|
|
26
|
+
* `recoverFromDeclaration`.
|
|
19
27
|
*/
|
|
20
28
|
import type { EnhancedGraph } from '../graph-sorter';
|
|
21
29
|
import { ProjectInfo } from '../project-info';
|
|
22
30
|
import { ApiClassInfo, ProjectApiRelations } from './api-relations';
|
|
31
|
+
/**
|
|
32
|
+
* An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an
|
|
33
|
+
* abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken
|
|
34
|
+
* scan (a real api-lib whose source we never indexed), never a "this isn't an API" argument —
|
|
35
|
+
* so it is reported loudly instead of collapsing into a silent `return null`.
|
|
36
|
+
*/
|
|
37
|
+
export declare class UnresolvedApiCall {
|
|
38
|
+
/** The project whose source makes the call. */
|
|
39
|
+
readonly project: string;
|
|
40
|
+
/** The contract class name as written at the call site. */
|
|
41
|
+
readonly api: string;
|
|
42
|
+
/** `path/to/file.ts:LINE` of the call site, workspace-relative. */
|
|
43
|
+
readonly at: string;
|
|
44
|
+
/** The declaration file the checker resolved to (where decorators are erased). */
|
|
45
|
+
readonly declaredIn: string;
|
|
46
|
+
constructor(
|
|
47
|
+
/** The project whose source makes the call. */
|
|
48
|
+
project: string,
|
|
49
|
+
/** The contract class name as written at the call site. */
|
|
50
|
+
api: string,
|
|
51
|
+
/** `path/to/file.ts:LINE` of the call site, workspace-relative. */
|
|
52
|
+
at: string,
|
|
53
|
+
/** The declaration file the checker resolved to (where decorators are erased). */
|
|
54
|
+
declaredIn: string);
|
|
55
|
+
}
|
|
23
56
|
/** The whole-workspace result of a scan. */
|
|
24
57
|
export interface ApiScanResult {
|
|
25
58
|
/** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */
|
|
@@ -34,27 +67,40 @@ export interface ApiScanResult {
|
|
|
34
67
|
* conclude "no implements/uses" for it, because its behavior was never observed.
|
|
35
68
|
*/
|
|
36
69
|
scannedProjects: Set<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Call sites naming a contract we could not map back to workspace source. Non-empty means the
|
|
72
|
+
* graph is INCOMPLETE — callers must surface these rather than emit a green, wrong graph.
|
|
73
|
+
*/
|
|
74
|
+
unresolvedApiCalls: UnresolvedApiCall[];
|
|
37
75
|
}
|
|
38
76
|
/** Statically scans every project for its api-lib implements/uses relationships. */
|
|
39
77
|
export declare class ApiUsageScanner {
|
|
40
78
|
private readonly workspaceRoot;
|
|
41
79
|
private readonly projectInfos;
|
|
42
80
|
private readonly locator;
|
|
43
|
-
private readonly apiLibProjects;
|
|
44
|
-
private readonly apiIndex;
|
|
45
81
|
private readonly relationsByProject;
|
|
46
82
|
private readonly scannedProjects;
|
|
83
|
+
private readonly unresolvedApiCalls;
|
|
84
|
+
private sourceIndex;
|
|
47
85
|
constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>);
|
|
48
86
|
scan(): ApiScanResult;
|
|
49
87
|
private scanProject;
|
|
50
88
|
private visit;
|
|
51
|
-
/** Register a project-owned API contract (abstract @ApiPath class) into the index. */
|
|
52
|
-
private recordApiClass;
|
|
53
89
|
private recordCall;
|
|
54
|
-
private addImplementsFromExpr;
|
|
55
90
|
/** Resolve an expression to the API contract it names, or null if it is not one. */
|
|
56
91
|
private apiInfoFromExpr;
|
|
57
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* The checker landed on a BUILT declaration instead of source — the consumer has no
|
|
94
|
+
* tsconfig.base `paths` entry for the api-lib, so the import went through node_modules to
|
|
95
|
+
* `dist/**.d.ts`. tsc erases decorators when emitting declarations, so `@ApiPath` is simply
|
|
96
|
+
* not there and never will be. Recover the contract by name from the source index; the graph
|
|
97
|
+
* is then correct no matter how the consumer's tsconfig is laid out.
|
|
98
|
+
*/
|
|
99
|
+
private recoverFromDeclaration;
|
|
100
|
+
/** `path/to/file.ts:LINE` for `node`, workspace-relative, for a human-readable report. */
|
|
101
|
+
private relativeLocation;
|
|
102
|
+
private relativePath;
|
|
103
|
+
/** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE, else null. */
|
|
58
104
|
private apiClassInfoFor;
|
|
59
105
|
}
|
|
60
106
|
/**
|
|
@@ -65,3 +111,9 @@ export declare class ApiUsageScanner {
|
|
|
65
111
|
* full scan so callers (validators, runtime graph) can reuse the api index.
|
|
66
112
|
*/
|
|
67
113
|
export declare function scanAndAttachApiRelations(workspaceRoot: string, graph: EnhancedGraph, projectInfos: Map<string, ProjectInfo>): ApiScanResult;
|
|
114
|
+
/**
|
|
115
|
+
* Loud, actionable report for contracts the scan could not map to source. Callers print this
|
|
116
|
+
* instead of emitting a green graph that is quietly missing relations. Not fatal: a contract
|
|
117
|
+
* from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.
|
|
118
|
+
*/
|
|
119
|
+
export declare function describeUnresolvedApiCalls(calls: UnresolvedApiCall[]): string;
|
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
* `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client
|
|
18
18
|
* An api-lib is DETECTED, not tagged: a project exporting an `abstract class`
|
|
19
19
|
* carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.
|
|
20
|
+
*
|
|
21
|
+
* Contracts are indexed from SOURCE in a pre-pass (ApiSourceIndexBuilder) rather than
|
|
22
|
+
* from wherever the checker resolves an import to. A consumer without a tsconfig.base
|
|
23
|
+
* `paths` entry resolves `import { XxxApi } from '@scope/xxx-api'` through node_modules
|
|
24
|
+
* to the package's BUILT `dist/**.d.ts` — and tsc ERASES decorators when emitting
|
|
25
|
+
* declarations, so `@ApiPath` can never be read there. Keying off the resolved
|
|
26
|
+
* declaration therefore dropped whole services from the graph, silently. See
|
|
27
|
+
* `recoverFromDeclaration`.
|
|
20
28
|
*/
|
|
21
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.ApiUsageScanner = void 0;
|
|
30
|
+
exports.ApiUsageScanner = exports.UnresolvedApiCall = void 0;
|
|
23
31
|
exports.scanAndAttachApiRelations = scanAndAttachApiRelations;
|
|
32
|
+
exports.describeUnresolvedApiCalls = describeUnresolvedApiCalls;
|
|
24
33
|
const tslib_1 = require("tslib");
|
|
25
34
|
const ts = tslib_1.__importStar(require("typescript"));
|
|
26
35
|
const fs = tslib_1.__importStar(require("fs"));
|
|
@@ -31,6 +40,33 @@ const api_relations_1 = require("./api-relations");
|
|
|
31
40
|
const RPC_CLIENT_METHOD = 'createRpcClient';
|
|
32
41
|
const PUBSUB_CLIENT_METHOD = 'createPubSubClient';
|
|
33
42
|
const ADD_ROUTES_METHOD = 'addRoutes';
|
|
43
|
+
/**
|
|
44
|
+
* An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an
|
|
45
|
+
* abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken
|
|
46
|
+
* scan (a real api-lib whose source we never indexed), never a "this isn't an API" argument —
|
|
47
|
+
* so it is reported loudly instead of collapsing into a silent `return null`.
|
|
48
|
+
*/
|
|
49
|
+
class UnresolvedApiCall {
|
|
50
|
+
project;
|
|
51
|
+
api;
|
|
52
|
+
at;
|
|
53
|
+
declaredIn;
|
|
54
|
+
constructor(
|
|
55
|
+
/** The project whose source makes the call. */
|
|
56
|
+
project,
|
|
57
|
+
/** The contract class name as written at the call site. */
|
|
58
|
+
api,
|
|
59
|
+
/** `path/to/file.ts:LINE` of the call site, workspace-relative. */
|
|
60
|
+
at,
|
|
61
|
+
/** The declaration file the checker resolved to (where decorators are erased). */
|
|
62
|
+
declaredIn) {
|
|
63
|
+
this.project = project;
|
|
64
|
+
this.api = api;
|
|
65
|
+
this.at = at;
|
|
66
|
+
this.declaredIn = declaredIn;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.UnresolvedApiCall = UnresolvedApiCall;
|
|
34
70
|
/** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */
|
|
35
71
|
class ProjectLocator {
|
|
36
72
|
roots;
|
|
@@ -61,6 +97,71 @@ class ProjectRoot {
|
|
|
61
97
|
this.abs = abs;
|
|
62
98
|
}
|
|
63
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Every API contract in the workspace, keyed by class name, read from SOURCE.
|
|
102
|
+
*
|
|
103
|
+
* Name-keyed because a call site only ever gives us a name once its import has resolved into a
|
|
104
|
+
* decorator-erased declaration. Two api-libs exporting the same class name collide (last wins) —
|
|
105
|
+
* the same collision the published `apiIndex` has always had.
|
|
106
|
+
*/
|
|
107
|
+
class ApiSourceIndex {
|
|
108
|
+
byName;
|
|
109
|
+
owners;
|
|
110
|
+
constructor(byName, owners) {
|
|
111
|
+
this.byName = byName;
|
|
112
|
+
this.owners = owners;
|
|
113
|
+
}
|
|
114
|
+
lookup(api) {
|
|
115
|
+
return this.byName.get(api) ?? null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Builds the ApiSourceIndex by parsing each project's own `src/**` directly.
|
|
120
|
+
*
|
|
121
|
+
* Deliberately parser-only (no ts.Program, no checker): we need the decorators exactly as
|
|
122
|
+
* written, and a plain parse cannot be diverted to a `.d.ts` by module resolution — which is
|
|
123
|
+
* the entire bug this guards against. It is also cheap enough to run over every project.
|
|
124
|
+
*/
|
|
125
|
+
class ApiSourceIndexBuilder {
|
|
126
|
+
workspaceRoot;
|
|
127
|
+
projectInfos;
|
|
128
|
+
byName = new Map();
|
|
129
|
+
owners = new Set();
|
|
130
|
+
constructor(workspaceRoot, projectInfos) {
|
|
131
|
+
this.workspaceRoot = workspaceRoot;
|
|
132
|
+
this.projectInfos = projectInfos;
|
|
133
|
+
}
|
|
134
|
+
build() {
|
|
135
|
+
for (const info of this.projectInfos.values()) {
|
|
136
|
+
if (info.root === '' || info.root === '.')
|
|
137
|
+
continue;
|
|
138
|
+
this.indexProject(info);
|
|
139
|
+
}
|
|
140
|
+
return new ApiSourceIndex(this.byName, this.owners);
|
|
141
|
+
}
|
|
142
|
+
indexProject(info) {
|
|
143
|
+
const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');
|
|
144
|
+
if (!fs.existsSync(srcDir))
|
|
145
|
+
return;
|
|
146
|
+
for (const file of collectTsFiles(srcDir)) {
|
|
147
|
+
if (isTestFile(file))
|
|
148
|
+
continue; // tests are not production topology
|
|
149
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
150
|
+
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
151
|
+
this.indexNode(sourceFile, info.name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
indexNode(node, project) {
|
|
155
|
+
if (ts.isClassDeclaration(node)) {
|
|
156
|
+
const info = apiClassInfoFrom(node, project);
|
|
157
|
+
if (info) {
|
|
158
|
+
this.owners.add(project);
|
|
159
|
+
this.byName.set(info.api, info);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
ts.forEachChild(node, (child) => this.indexNode(child, project));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
64
165
|
/** Per-owner accumulator that dedupes API refs while a single project is scanned. */
|
|
65
166
|
class RelationAccumulator {
|
|
66
167
|
implementsByOwner = new Map();
|
|
@@ -105,16 +206,19 @@ class ApiUsageScanner {
|
|
|
105
206
|
workspaceRoot;
|
|
106
207
|
projectInfos;
|
|
107
208
|
locator;
|
|
108
|
-
apiLibProjects = new Set();
|
|
109
|
-
apiIndex = new Map();
|
|
110
209
|
relationsByProject = new Map();
|
|
111
210
|
scannedProjects = new Set();
|
|
211
|
+
unresolvedApiCalls = [];
|
|
212
|
+
sourceIndex = new ApiSourceIndex(new Map(), new Set());
|
|
112
213
|
constructor(workspaceRoot, projectInfos) {
|
|
113
214
|
this.workspaceRoot = workspaceRoot;
|
|
114
215
|
this.projectInfos = projectInfos;
|
|
115
216
|
this.locator = new ProjectLocator(workspaceRoot, projectInfos);
|
|
116
217
|
}
|
|
117
218
|
scan() {
|
|
219
|
+
// Pre-pass: every contract, from source, BEFORE any call site is resolved — a call site in
|
|
220
|
+
// one project routinely names a contract owned by a project we have not walked yet.
|
|
221
|
+
this.sourceIndex = new ApiSourceIndexBuilder(this.workspaceRoot, this.projectInfos).build();
|
|
118
222
|
for (const info of this.projectInfos.values()) {
|
|
119
223
|
if (info.root === '' || info.root === '.')
|
|
120
224
|
continue;
|
|
@@ -122,9 +226,10 @@ class ApiUsageScanner {
|
|
|
122
226
|
}
|
|
123
227
|
return {
|
|
124
228
|
relationsByProject: this.relationsByProject,
|
|
125
|
-
apiLibProjects: this.
|
|
126
|
-
apiIndex: this.
|
|
229
|
+
apiLibProjects: this.sourceIndex.owners,
|
|
230
|
+
apiIndex: this.sourceIndex.byName,
|
|
127
231
|
scannedProjects: this.scannedProjects,
|
|
232
|
+
unresolvedApiCalls: this.unresolvedApiCalls,
|
|
128
233
|
};
|
|
129
234
|
}
|
|
130
235
|
scanProject(info) {
|
|
@@ -153,55 +258,70 @@ class ApiUsageScanner {
|
|
|
153
258
|
this.relationsByProject.set(info.name, accumulator.toRelations());
|
|
154
259
|
}
|
|
155
260
|
visit(node, checker, project, acc) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
else if (ts.isCallExpression(node)) {
|
|
160
|
-
this.recordCall(node, checker, acc);
|
|
161
|
-
}
|
|
261
|
+
// Contract classes are indexed by the source pre-pass, so only calls matter here.
|
|
262
|
+
if (ts.isCallExpression(node))
|
|
263
|
+
this.recordCall(node, checker, project, acc);
|
|
162
264
|
ts.forEachChild(node, (child) => this.visit(child, checker, project, acc));
|
|
163
265
|
}
|
|
164
|
-
|
|
165
|
-
recordApiClass(cls, project) {
|
|
166
|
-
const own = this.apiClassInfoFor(cls);
|
|
167
|
-
if (!own)
|
|
168
|
-
return;
|
|
169
|
-
this.apiLibProjects.add(project);
|
|
170
|
-
this.apiIndex.set(own.api, own);
|
|
171
|
-
}
|
|
172
|
-
recordCall(call, checker, acc) {
|
|
266
|
+
recordCall(call, checker, project, acc) {
|
|
173
267
|
const method = calleeMethodName(call);
|
|
174
268
|
if (method === null || call.arguments.length === 0)
|
|
175
269
|
return;
|
|
176
270
|
if (method === ADD_ROUTES_METHOD) {
|
|
177
|
-
this.
|
|
271
|
+
const info = this.apiInfoFromExpr(call.arguments[0], checker, project);
|
|
272
|
+
if (info)
|
|
273
|
+
acc.addImplements(info.owner, { api: info.api, type: info.type });
|
|
178
274
|
return;
|
|
179
275
|
}
|
|
180
276
|
if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {
|
|
181
|
-
const info = this.apiInfoFromExpr(call.arguments[0], checker);
|
|
277
|
+
const info = this.apiInfoFromExpr(call.arguments[0], checker, project);
|
|
182
278
|
if (info)
|
|
183
279
|
acc.addUses(info.owner, { api: info.api, type: info.type });
|
|
184
280
|
}
|
|
185
281
|
}
|
|
186
|
-
addImplementsFromExpr(expr, checker, acc) {
|
|
187
|
-
const info = this.apiInfoFromExpr(expr, checker);
|
|
188
|
-
if (info)
|
|
189
|
-
acc.addImplements(info.owner, { api: info.api, type: info.type });
|
|
190
|
-
}
|
|
191
282
|
/** Resolve an expression to the API contract it names, or null if it is not one. */
|
|
192
|
-
apiInfoFromExpr(expr, checker) {
|
|
283
|
+
apiInfoFromExpr(expr, checker, project) {
|
|
193
284
|
const decl = (0, bindings_1.resolveClassDeclaration)(expr, checker);
|
|
194
|
-
|
|
285
|
+
if (!decl)
|
|
286
|
+
return null;
|
|
287
|
+
const fromSource = this.apiClassInfoFor(decl);
|
|
288
|
+
return fromSource ?? this.recoverFromDeclaration(decl, expr, project);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The checker landed on a BUILT declaration instead of source — the consumer has no
|
|
292
|
+
* tsconfig.base `paths` entry for the api-lib, so the import went through node_modules to
|
|
293
|
+
* `dist/**.d.ts`. tsc erases decorators when emitting declarations, so `@ApiPath` is simply
|
|
294
|
+
* not there and never will be. Recover the contract by name from the source index; the graph
|
|
295
|
+
* is then correct no matter how the consumer's tsconfig is laid out.
|
|
296
|
+
*/
|
|
297
|
+
recoverFromDeclaration(decl, expr, project) {
|
|
298
|
+
// An abstract class is the shape of a contract; a non-abstract argument is genuinely not one.
|
|
299
|
+
if (!decl.getSourceFile().isDeclarationFile || !isAbstractClass(decl) || !decl.name)
|
|
300
|
+
return null;
|
|
301
|
+
const recovered = this.sourceIndex.lookup(decl.name.text);
|
|
302
|
+
if (recovered)
|
|
303
|
+
return recovered;
|
|
304
|
+
// Abstract, in a .d.ts, yet no workspace source owns it — the scan is blind here. Say so.
|
|
305
|
+
this.unresolvedApiCalls.push(new UnresolvedApiCall(project, decl.name.text, this.relativeLocation(expr), this.relativePath(decl.getSourceFile().fileName)));
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
/** `path/to/file.ts:LINE` for `node`, workspace-relative, for a human-readable report. */
|
|
309
|
+
relativeLocation(node) {
|
|
310
|
+
const sourceFile = node.getSourceFile();
|
|
311
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
312
|
+
return `${this.relativePath(sourceFile.fileName)}:${position.line + 1}`;
|
|
313
|
+
}
|
|
314
|
+
relativePath(absFile) {
|
|
315
|
+
return path.relative(this.workspaceRoot, absFile);
|
|
195
316
|
}
|
|
196
|
-
/** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath
|
|
317
|
+
/** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE, else null. */
|
|
197
318
|
apiClassInfoFor(cls) {
|
|
198
319
|
if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name)
|
|
199
320
|
return null;
|
|
200
321
|
const owner = this.locator.projectOf(cls.getSourceFile().fileName);
|
|
201
322
|
if (owner === null)
|
|
202
323
|
return null;
|
|
203
|
-
|
|
204
|
-
return { api: cls.name.text, owner, type };
|
|
324
|
+
return { api: cls.name.text, owner, type: apiTransport(cls) };
|
|
205
325
|
}
|
|
206
326
|
}
|
|
207
327
|
exports.ApiUsageScanner = ApiUsageScanner;
|
|
@@ -222,11 +342,32 @@ function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos) {
|
|
|
222
342
|
}
|
|
223
343
|
return result;
|
|
224
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Loud, actionable report for contracts the scan could not map to source. Callers print this
|
|
347
|
+
* instead of emitting a green graph that is quietly missing relations. Not fatal: a contract
|
|
348
|
+
* from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.
|
|
349
|
+
*/
|
|
350
|
+
// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnclassifiedApiDep
|
|
351
|
+
function describeUnresolvedApiCalls(calls) {
|
|
352
|
+
const lines = [
|
|
353
|
+
`⚠️ ${calls.length} API contract(s) resolved to a declaration file with no matching workspace source.`,
|
|
354
|
+
` Decorators (@ApiPath) are ERASED in .d.ts output, so these relations are MISSING from the graph:`,
|
|
355
|
+
];
|
|
356
|
+
for (const call of calls) {
|
|
357
|
+
lines.push(` • ${call.api} at ${call.at} (${call.project}) → resolved to ${call.declaredIn}`);
|
|
358
|
+
}
|
|
359
|
+
lines.push(` If the api-lib IS in this workspace, add a tsconfig.base.json 'paths' entry mapping it to its`, ` src/index.ts, or confirm its project root is registered. If it is a published external package,`, ` this relation cannot be derived and the graph edge will not appear.`);
|
|
360
|
+
return lines.join('\n');
|
|
361
|
+
}
|
|
225
362
|
/**
|
|
226
363
|
* Build a program for scanning ONE project. Prefers the project's compile tsconfig; but when that
|
|
227
364
|
* is a solution-style tsconfig (only `references`, no `files`/`include` — e.g. legacy-server), it
|
|
228
365
|
* yields zero files, so we fall back to globbing the project's own `src/**` and reuse the resolved
|
|
229
366
|
* compiler options (which carry tsconfig.base `paths` for cross-package @webpieces resolution).
|
|
367
|
+
*
|
|
368
|
+
* `paths` is a PREFERENCE, not a precondition: it lets imports resolve straight to source. Without
|
|
369
|
+
* it they land on a decorator-erased `dist/**.d.ts`, which the source index recovers from — see
|
|
370
|
+
* ApiUsageScanner.recoverFromDeclaration.
|
|
230
371
|
*/
|
|
231
372
|
// webpieces-disable no-function-outside-class -- ts Program factory, mirrors di-graph/program.ts
|
|
232
373
|
function createScanProgram(projectRootAbs) {
|
|
@@ -266,6 +407,17 @@ function collectTsFiles(dir) {
|
|
|
266
407
|
}
|
|
267
408
|
return out;
|
|
268
409
|
}
|
|
410
|
+
/** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */
|
|
411
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
412
|
+
function apiClassInfoFrom(cls, project) {
|
|
413
|
+
if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name)
|
|
414
|
+
return null;
|
|
415
|
+
return { api: cls.name.text, owner: project, type: apiTransport(cls) };
|
|
416
|
+
}
|
|
417
|
+
// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
|
|
418
|
+
function apiTransport(cls) {
|
|
419
|
+
return hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';
|
|
420
|
+
}
|
|
269
421
|
// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
|
|
270
422
|
function isAbstractClass(cls) {
|
|
271
423
|
return (ts.getModifiers(cls) ?? []).some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Usage Scanner\n *\n * Derives, by scanning real source (not a declaration file), how every project\n * relates to the api-lib projects it depends on. This is the single source of\n * truth for the `apiRelations` field in architecture/dependencies.json AND for\n * the runtime microservice graph.\n *\n * Signals (all resolved through the TypeScript checker, so re-exports resolve):\n * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration\n * that actually SERVES the contract over the wire. We deliberately\n * do NOT use `class Ctrl extends XxxApi`: a class can extend an API\n * as an in-process test double / simulator (e.g. Server2Simulator)\n * without ever serving it — only `addRoutes` proves a served route.\n * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client\n * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client\n * An api-lib is DETECTED, not tagged: a project exporting an `abstract class`\n * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { findProjectTsconfig } from '../di-graph/program';\nimport { resolveClassDeclaration, classDecorators, decoratorName } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiRef,\n ApiRelation,\n ProjectApiRelations,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\n\nconst RPC_CLIENT_METHOD = 'createRpcClient';\nconst PUBSUB_CLIENT_METHOD = 'createPubSubClient';\nconst ADD_ROUTES_METHOD = 'addRoutes';\n\n/** The whole-workspace result of a scan. */\nexport interface ApiScanResult {\n /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */\n relationsByProject: Map<string, ProjectApiRelations>;\n /** Every project that owns ≥1 API contract class. */\n apiLibProjects: Set<string>;\n /** apiClassName -> where it lives + its transport. */\n apiIndex: Map<string, ApiClassInfo>;\n /**\n * Projects whose production (non-test) source was actually scanned. A project with only test\n * files (e.g. an e2e harness), or one the compiler couldn't load, is ABSENT — callers must not\n * conclude \"no implements/uses\" for it, because its behavior was never observed.\n */\n scannedProjects: Set<string>;\n}\n\n/** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */\nclass ProjectLocator {\n private readonly roots: ProjectRoot[];\n\n constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>) {\n const roots: ProjectRoot[] = [];\n for (const info of projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n roots.push(new ProjectRoot(info.name, path.resolve(workspaceRoot, info.root)));\n }\n // Longest root first so a nested project wins over its parent.\n this.roots = roots.sort((a: ProjectRoot, b: ProjectRoot) => b.abs.length - a.abs.length);\n }\n\n projectOf(absFile: string): string | null {\n const normalized = path.resolve(absFile);\n for (const root of this.roots) {\n if (normalized === root.abs || normalized.startsWith(root.abs + path.sep)) return root.name;\n }\n return null;\n }\n}\n\nclass ProjectRoot {\n constructor(\n public readonly name: string,\n public readonly abs: string,\n ) {}\n}\n\n/** Per-owner accumulator that dedupes API refs while a single project is scanned. */\nclass RelationAccumulator {\n private readonly implementsByOwner = new Map<string, Map<string, ApiRef>>();\n private readonly usesByOwner = new Map<string, Map<string, ApiRef>>();\n\n addImplements(owner: string, ref: ApiRef): void {\n ensureRefMap(this.implementsByOwner, owner).set(ref.api, ref);\n }\n\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(ref.api, ref);\n }\n\n /** Build the deterministic { owner -> relation } record, owners in sorted order. */\n toRelations(): ProjectApiRelations {\n const owners = new Set<string>([...this.implementsByOwner.keys(), ...this.usesByOwner.keys()]);\n const relations: ProjectApiRelations = {};\n for (const owner of [...owners].sort()) {\n const implementsRefs = sortApiRefs([...(this.implementsByOwner.get(owner)?.values() ?? [])]);\n const usesRefs = sortApiRefs([...(this.usesByOwner.get(owner)?.values() ?? [])]);\n const relation: ApiRelation = {\n kind: deriveApiRelationKind(implementsRefs, usesRefs),\n implements: implementsRefs,\n uses: usesRefs,\n };\n relations[owner] = relation;\n }\n return relations;\n }\n\n isEmpty(): boolean {\n return this.implementsByOwner.size === 0 && this.usesByOwner.size === 0;\n }\n}\n\n// webpieces-disable no-function-outside-class -- tiny map helper, matching the AST-helper style of di-graph/bindings.ts\nfunction ensureRefMap(map: Map<string, Map<string, ApiRef>>, owner: string): Map<string, ApiRef> {\n let inner = map.get(owner);\n if (!inner) {\n inner = new Map<string, ApiRef>();\n map.set(owner, inner);\n }\n return inner;\n}\n\n/** Statically scans every project for its api-lib implements/uses relationships. */\nexport class ApiUsageScanner {\n private readonly locator: ProjectLocator;\n private readonly apiLibProjects = new Set<string>();\n private readonly apiIndex = new Map<string, ApiClassInfo>();\n private readonly relationsByProject = new Map<string, ProjectApiRelations>();\n private readonly scannedProjects = new Set<string>();\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n }\n\n scan(): ApiScanResult {\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.scanProject(info);\n }\n return {\n relationsByProject: this.relationsByProject,\n apiLibProjects: this.apiLibProjects,\n apiIndex: this.apiIndex,\n scannedProjects: this.scannedProjects,\n };\n }\n\n private scanProject(info: ProjectInfo): void {\n const program = createScanProgram(path.resolve(this.workspaceRoot, info.root));\n if (!program) return;\n const checker = program.getTypeChecker();\n const accumulator = new RelationAccumulator();\n let scannedProductionFile = false;\n\n for (const sourceFile of program.getSourceFiles()) {\n if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/')) continue;\n if (isTestFile(sourceFile.fileName)) continue; // tests are not production topology\n // Only this project's OWN files — imported api-lib source is in the program too.\n if (this.locator.projectOf(sourceFile.fileName) !== info.name) continue;\n scannedProductionFile = true;\n this.visit(sourceFile, checker, info.name, accumulator);\n }\n\n // Record coverage only when we actually saw production source — an all-test project (e2e)\n // stays absent so the validator won't wrongly flag its api-lib deps as unused.\n if (scannedProductionFile) this.scannedProjects.add(info.name);\n if (!accumulator.isEmpty()) this.relationsByProject.set(info.name, accumulator.toRelations());\n }\n\n private visit(node: ts.Node, checker: ts.TypeChecker, project: string, acc: RelationAccumulator): void {\n if (ts.isClassDeclaration(node)) {\n this.recordApiClass(node, project);\n } else if (ts.isCallExpression(node)) {\n this.recordCall(node, checker, acc);\n }\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n /** Register a project-owned API contract (abstract @ApiPath class) into the index. */\n private recordApiClass(cls: ts.ClassDeclaration, project: string): void {\n const own = this.apiClassInfoFor(cls);\n if (!own) return;\n this.apiLibProjects.add(project);\n this.apiIndex.set(own.api, own);\n }\n\n private recordCall(call: ts.CallExpression, checker: ts.TypeChecker, acc: RelationAccumulator): void {\n const method = calleeMethodName(call);\n if (method === null || call.arguments.length === 0) return;\n if (method === ADD_ROUTES_METHOD) {\n this.addImplementsFromExpr(call.arguments[0], checker, acc);\n return;\n }\n if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker);\n if (info) acc.addUses(info.owner, { api: info.api, type: info.type });\n }\n }\n\n private addImplementsFromExpr(expr: ts.Expression, checker: ts.TypeChecker, acc: RelationAccumulator): void {\n const info = this.apiInfoFromExpr(expr, checker);\n if (info) acc.addImplements(info.owner, { api: info.api, type: info.type });\n }\n\n /** Resolve an expression to the API contract it names, or null if it is not one. */\n private apiInfoFromExpr(expr: ts.Expression, checker: ts.TypeChecker): ApiClassInfo | null {\n const decl = resolveClassDeclaration(expr, checker);\n return decl ? this.apiClassInfoFor(decl) : null;\n }\n\n /** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n const type = hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';\n return { api: cls.name.text, owner, type };\n }\n}\n\n/**\n * Run the scan and attach the derived `apiRelations` onto each graph entry in\n * place. Shared by `architecture:generate` (which then saves) and\n * `architecture:validate-architecture-unchanged` (which regenerates in memory\n * and must attach the SAME field, or it would see a phantom diff). Returns the\n * full scan so callers (validators, runtime graph) can reuse the api index.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings\nexport function scanAndAttachApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ApiScanResult {\n const result = new ApiUsageScanner(workspaceRoot, projectInfos).scan();\n for (const projectName of result.relationsByProject.keys()) {\n const entry = graph[projectName];\n if (entry) entry.apiRelations = result.relationsByProject.get(projectName);\n }\n return result;\n}\n\n/**\n * Build a program for scanning ONE project. Prefers the project's compile tsconfig; but when that\n * is a solution-style tsconfig (only `references`, no `files`/`include` — e.g. legacy-server), it\n * yields zero files, so we fall back to globbing the project's own `src/**` and reuse the resolved\n * compiler options (which carry tsconfig.base `paths` for cross-package @webpieces resolution).\n */\n// webpieces-disable no-function-outside-class -- ts Program factory, mirrors di-graph/program.ts\nfunction createScanProgram(projectRootAbs: string): ts.Program | null {\n const configPath = findProjectTsconfig(projectRootAbs);\n if (!configPath) return buildProgramFromSrc(projectRootAbs, {});\n const host = Object.assign({}, ts.sys, {\n onUnRecoverableConfigFileDiagnostic: (): void => undefined,\n }) as ts.ParseConfigFileHost;\n const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host);\n if (!parsed) return null;\n if (parsed.fileNames.length > 0) return ts.createProgram(parsed.fileNames, parsed.options);\n return buildProgramFromSrc(projectRootAbs, parsed.options);\n}\n\n// webpieces-disable no-function-outside-class -- ts Program factory helper, mirrors di-graph/program.ts\nfunction buildProgramFromSrc(projectRootAbs: string, options: ts.CompilerOptions): ts.Program | null {\n const srcDir = path.join(projectRootAbs, 'src');\n if (!fs.existsSync(srcDir)) return null;\n const files = collectTsFiles(srcDir);\n return files.length > 0 ? ts.createProgram(files, options) : null;\n}\n\n// webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here\nfunction collectTsFiles(dir: string): string[] {\n const out: string[] = [];\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') out.push(...collectTsFiles(full));\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n out.push(full);\n }\n }\n return out;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isAbstractClass(cls: ts.ClassDeclaration): boolean {\n return (ts.getModifiers(cls) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === name);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nfunction calleeMethodName(call: ts.CallExpression): string | null {\n const callee = call.expression;\n if (ts.isPropertyAccessExpression(callee)) return callee.name.text;\n if (ts.isIdentifier(callee)) return callee.text;\n return null;\n}\n\n// webpieces-disable no-function-outside-class -- pure path predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isTestFile(fileName: string): boolean {\n return (\n fileName.includes('/__tests__/') ||\n fileName.includes('.spec.') ||\n fileName.includes('.test.')\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Usage Scanner\n *\n * Derives, by scanning real source (not a declaration file), how every project\n * relates to the api-lib projects it depends on. This is the single source of\n * truth for the `apiRelations` field in architecture/dependencies.json AND for\n * the runtime microservice graph.\n *\n * Signals (all resolved through the TypeScript checker, so re-exports resolve):\n * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration\n * that actually SERVES the contract over the wire. We deliberately\n * do NOT use `class Ctrl extends XxxApi`: a class can extend an API\n * as an in-process test double / simulator (e.g. Server2Simulator)\n * without ever serving it — only `addRoutes` proves a served route.\n * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client\n * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client\n * An api-lib is DETECTED, not tagged: a project exporting an `abstract class`\n * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.\n *\n * Contracts are indexed from SOURCE in a pre-pass (ApiSourceIndexBuilder) rather than\n * from wherever the checker resolves an import to. A consumer without a tsconfig.base\n * `paths` entry resolves `import { XxxApi } from '@scope/xxx-api'` through node_modules\n * to the package's BUILT `dist/**.d.ts` — and tsc ERASES decorators when emitting\n * declarations, so `@ApiPath` can never be read there. Keying off the resolved\n * declaration therefore dropped whole services from the graph, silently. See\n * `recoverFromDeclaration`.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { findProjectTsconfig } from '../di-graph/program';\nimport { resolveClassDeclaration, classDecorators, decoratorName } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiRef,\n ApiTransport,\n ApiRelation,\n ProjectApiRelations,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\n\nconst RPC_CLIENT_METHOD = 'createRpcClient';\nconst PUBSUB_CLIENT_METHOD = 'createPubSubClient';\nconst ADD_ROUTES_METHOD = 'addRoutes';\n\n/**\n * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an\n * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken\n * scan (a real api-lib whose source we never indexed), never a \"this isn't an API\" argument —\n * so it is reported loudly instead of collapsing into a silent `return null`.\n */\nexport class UnresolvedApiCall {\n constructor(\n /** The project whose source makes the call. */\n public readonly project: string,\n /** The contract class name as written at the call site. */\n public readonly api: string,\n /** `path/to/file.ts:LINE` of the call site, workspace-relative. */\n public readonly at: string,\n /** The declaration file the checker resolved to (where decorators are erased). */\n public readonly declaredIn: string,\n ) {}\n}\n\n/** The whole-workspace result of a scan. */\nexport interface ApiScanResult {\n /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */\n relationsByProject: Map<string, ProjectApiRelations>;\n /** Every project that owns ≥1 API contract class. */\n apiLibProjects: Set<string>;\n /** apiClassName -> where it lives + its transport. */\n apiIndex: Map<string, ApiClassInfo>;\n /**\n * Projects whose production (non-test) source was actually scanned. A project with only test\n * files (e.g. an e2e harness), or one the compiler couldn't load, is ABSENT — callers must not\n * conclude \"no implements/uses\" for it, because its behavior was never observed.\n */\n scannedProjects: Set<string>;\n /**\n * Call sites naming a contract we could not map back to workspace source. Non-empty means the\n * graph is INCOMPLETE — callers must surface these rather than emit a green, wrong graph.\n */\n unresolvedApiCalls: UnresolvedApiCall[];\n}\n\n/** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */\nclass ProjectLocator {\n private readonly roots: ProjectRoot[];\n\n constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>) {\n const roots: ProjectRoot[] = [];\n for (const info of projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n roots.push(new ProjectRoot(info.name, path.resolve(workspaceRoot, info.root)));\n }\n // Longest root first so a nested project wins over its parent.\n this.roots = roots.sort((a: ProjectRoot, b: ProjectRoot) => b.abs.length - a.abs.length);\n }\n\n projectOf(absFile: string): string | null {\n const normalized = path.resolve(absFile);\n for (const root of this.roots) {\n if (normalized === root.abs || normalized.startsWith(root.abs + path.sep)) return root.name;\n }\n return null;\n }\n}\n\nclass ProjectRoot {\n constructor(\n public readonly name: string,\n public readonly abs: string,\n ) {}\n}\n\n/**\n * Every API contract in the workspace, keyed by class name, read from SOURCE.\n *\n * Name-keyed because a call site only ever gives us a name once its import has resolved into a\n * decorator-erased declaration. Two api-libs exporting the same class name collide (last wins) —\n * the same collision the published `apiIndex` has always had.\n */\nclass ApiSourceIndex {\n constructor(\n public readonly byName: Map<string, ApiClassInfo>,\n public readonly owners: Set<string>,\n ) {}\n\n lookup(api: string): ApiClassInfo | null {\n return this.byName.get(api) ?? null;\n }\n}\n\n/**\n * Builds the ApiSourceIndex by parsing each project's own `src/**` directly.\n *\n * Deliberately parser-only (no ts.Program, no checker): we need the decorators exactly as\n * written, and a plain parse cannot be diverted to a `.d.ts` by module resolution — which is\n * the entire bug this guards against. It is also cheap enough to run over every project.\n */\nclass ApiSourceIndexBuilder {\n private readonly byName = new Map<string, ApiClassInfo>();\n private readonly owners = new Set<string>();\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n ) {}\n\n build(): ApiSourceIndex {\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.indexProject(info);\n }\n return new ApiSourceIndex(this.byName, this.owners);\n }\n\n private indexProject(info: ProjectInfo): void {\n const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');\n if (!fs.existsSync(srcDir)) return;\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue; // tests are not production topology\n const text = fs.readFileSync(file, 'utf8');\n const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);\n this.indexNode(sourceFile, info.name);\n }\n }\n\n private indexNode(node: ts.Node, project: string): void {\n if (ts.isClassDeclaration(node)) {\n const info = apiClassInfoFrom(node, project);\n if (info) {\n this.owners.add(project);\n this.byName.set(info.api, info);\n }\n }\n ts.forEachChild(node, (child: ts.Node) => this.indexNode(child, project));\n }\n}\n\n/** Per-owner accumulator that dedupes API refs while a single project is scanned. */\nclass RelationAccumulator {\n private readonly implementsByOwner = new Map<string, Map<string, ApiRef>>();\n private readonly usesByOwner = new Map<string, Map<string, ApiRef>>();\n\n addImplements(owner: string, ref: ApiRef): void {\n ensureRefMap(this.implementsByOwner, owner).set(ref.api, ref);\n }\n\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(ref.api, ref);\n }\n\n /** Build the deterministic { owner -> relation } record, owners in sorted order. */\n toRelations(): ProjectApiRelations {\n const owners = new Set<string>([...this.implementsByOwner.keys(), ...this.usesByOwner.keys()]);\n const relations: ProjectApiRelations = {};\n for (const owner of [...owners].sort()) {\n const implementsRefs = sortApiRefs([...(this.implementsByOwner.get(owner)?.values() ?? [])]);\n const usesRefs = sortApiRefs([...(this.usesByOwner.get(owner)?.values() ?? [])]);\n const relation: ApiRelation = {\n kind: deriveApiRelationKind(implementsRefs, usesRefs),\n implements: implementsRefs,\n uses: usesRefs,\n };\n relations[owner] = relation;\n }\n return relations;\n }\n\n isEmpty(): boolean {\n return this.implementsByOwner.size === 0 && this.usesByOwner.size === 0;\n }\n}\n\n// webpieces-disable no-function-outside-class -- tiny map helper, matching the AST-helper style of di-graph/bindings.ts\nfunction ensureRefMap(map: Map<string, Map<string, ApiRef>>, owner: string): Map<string, ApiRef> {\n let inner = map.get(owner);\n if (!inner) {\n inner = new Map<string, ApiRef>();\n map.set(owner, inner);\n }\n return inner;\n}\n\n/** Statically scans every project for its api-lib implements/uses relationships. */\nexport class ApiUsageScanner {\n private readonly locator: ProjectLocator;\n private readonly relationsByProject = new Map<string, ProjectApiRelations>();\n private readonly scannedProjects = new Set<string>();\n private readonly unresolvedApiCalls: UnresolvedApiCall[] = [];\n private sourceIndex = new ApiSourceIndex(new Map<string, ApiClassInfo>(), new Set<string>());\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n }\n\n scan(): ApiScanResult {\n // Pre-pass: every contract, from source, BEFORE any call site is resolved — a call site in\n // one project routinely names a contract owned by a project we have not walked yet.\n this.sourceIndex = new ApiSourceIndexBuilder(this.workspaceRoot, this.projectInfos).build();\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.scanProject(info);\n }\n return {\n relationsByProject: this.relationsByProject,\n apiLibProjects: this.sourceIndex.owners,\n apiIndex: this.sourceIndex.byName,\n scannedProjects: this.scannedProjects,\n unresolvedApiCalls: this.unresolvedApiCalls,\n };\n }\n\n private scanProject(info: ProjectInfo): void {\n const program = createScanProgram(path.resolve(this.workspaceRoot, info.root));\n if (!program) return;\n const checker = program.getTypeChecker();\n const accumulator = new RelationAccumulator();\n let scannedProductionFile = false;\n\n for (const sourceFile of program.getSourceFiles()) {\n if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/')) continue;\n if (isTestFile(sourceFile.fileName)) continue; // tests are not production topology\n // Only this project's OWN files — imported api-lib source is in the program too.\n if (this.locator.projectOf(sourceFile.fileName) !== info.name) continue;\n scannedProductionFile = true;\n this.visit(sourceFile, checker, info.name, accumulator);\n }\n\n // Record coverage only when we actually saw production source — an all-test project (e2e)\n // stays absent so the validator won't wrongly flag its api-lib deps as unused.\n if (scannedProductionFile) this.scannedProjects.add(info.name);\n if (!accumulator.isEmpty()) this.relationsByProject.set(info.name, accumulator.toRelations());\n }\n\n private visit(node: ts.Node, checker: ts.TypeChecker, project: string, acc: RelationAccumulator): void {\n // Contract classes are indexed by the source pre-pass, so only calls matter here.\n if (ts.isCallExpression(node)) this.recordCall(node, checker, project, acc);\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n private recordCall(\n call: ts.CallExpression,\n checker: ts.TypeChecker,\n project: string,\n acc: RelationAccumulator,\n ): void {\n const method = calleeMethodName(call);\n if (method === null || call.arguments.length === 0) return;\n if (method === ADD_ROUTES_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker, project);\n if (info) acc.addImplements(info.owner, { api: info.api, type: info.type });\n return;\n }\n if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker, project);\n if (info) acc.addUses(info.owner, { api: info.api, type: info.type });\n }\n }\n\n /** Resolve an expression to the API contract it names, or null if it is not one. */\n private apiInfoFromExpr(expr: ts.Expression, checker: ts.TypeChecker, project: string): ApiClassInfo | null {\n const decl = resolveClassDeclaration(expr, checker);\n if (!decl) return null;\n const fromSource = this.apiClassInfoFor(decl);\n return fromSource ?? this.recoverFromDeclaration(decl, expr, project);\n }\n\n /**\n * The checker landed on a BUILT declaration instead of source — the consumer has no\n * tsconfig.base `paths` entry for the api-lib, so the import went through node_modules to\n * `dist/**.d.ts`. tsc erases decorators when emitting declarations, so `@ApiPath` is simply\n * not there and never will be. Recover the contract by name from the source index; the graph\n * is then correct no matter how the consumer's tsconfig is laid out.\n */\n private recoverFromDeclaration(\n decl: ts.ClassDeclaration,\n expr: ts.Expression,\n project: string,\n ): ApiClassInfo | null {\n // An abstract class is the shape of a contract; a non-abstract argument is genuinely not one.\n if (!decl.getSourceFile().isDeclarationFile || !isAbstractClass(decl) || !decl.name) return null;\n const recovered = this.sourceIndex.lookup(decl.name.text);\n if (recovered) return recovered;\n // Abstract, in a .d.ts, yet no workspace source owns it — the scan is blind here. Say so.\n this.unresolvedApiCalls.push(\n new UnresolvedApiCall(\n project,\n decl.name.text,\n this.relativeLocation(expr),\n this.relativePath(decl.getSourceFile().fileName),\n ),\n );\n return null;\n }\n\n /** `path/to/file.ts:LINE` for `node`, workspace-relative, for a human-readable report. */\n private relativeLocation(node: ts.Node): string {\n const sourceFile = node.getSourceFile();\n const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n return `${this.relativePath(sourceFile.fileName)}:${position.line + 1}`;\n }\n\n private relativePath(absFile: string): string {\n return path.relative(this.workspaceRoot, absFile);\n }\n\n /** {api, owner, type} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE, else null. */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n return { api: cls.name.text, owner, type: apiTransport(cls) };\n }\n}\n\n/**\n * Run the scan and attach the derived `apiRelations` onto each graph entry in\n * place. Shared by `architecture:generate` (which then saves) and\n * `architecture:validate-architecture-unchanged` (which regenerates in memory\n * and must attach the SAME field, or it would see a phantom diff). Returns the\n * full scan so callers (validators, runtime graph) can reuse the api index.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings\nexport function scanAndAttachApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ApiScanResult {\n const result = new ApiUsageScanner(workspaceRoot, projectInfos).scan();\n for (const projectName of result.relationsByProject.keys()) {\n const entry = graph[projectName];\n if (entry) entry.apiRelations = result.relationsByProject.get(projectName);\n }\n return result;\n}\n\n/**\n * Loud, actionable report for contracts the scan could not map to source. Callers print this\n * instead of emitting a green graph that is quietly missing relations. Not fatal: a contract\n * from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnclassifiedApiDep\nexport function describeUnresolvedApiCalls(calls: UnresolvedApiCall[]): string {\n const lines = [\n `⚠️ ${calls.length} API contract(s) resolved to a declaration file with no matching workspace source.`,\n ` Decorators (@ApiPath) are ERASED in .d.ts output, so these relations are MISSING from the graph:`,\n ];\n for (const call of calls) {\n lines.push(` • ${call.api} at ${call.at} (${call.project}) → resolved to ${call.declaredIn}`);\n }\n lines.push(\n ` If the api-lib IS in this workspace, add a tsconfig.base.json 'paths' entry mapping it to its`,\n ` src/index.ts, or confirm its project root is registered. If it is a published external package,`,\n ` this relation cannot be derived and the graph edge will not appear.`,\n );\n return lines.join('\\n');\n}\n\n/**\n * Build a program for scanning ONE project. Prefers the project's compile tsconfig; but when that\n * is a solution-style tsconfig (only `references`, no `files`/`include` — e.g. legacy-server), it\n * yields zero files, so we fall back to globbing the project's own `src/**` and reuse the resolved\n * compiler options (which carry tsconfig.base `paths` for cross-package @webpieces resolution).\n *\n * `paths` is a PREFERENCE, not a precondition: it lets imports resolve straight to source. Without\n * it they land on a decorator-erased `dist/**.d.ts`, which the source index recovers from — see\n * ApiUsageScanner.recoverFromDeclaration.\n */\n// webpieces-disable no-function-outside-class -- ts Program factory, mirrors di-graph/program.ts\nfunction createScanProgram(projectRootAbs: string): ts.Program | null {\n const configPath = findProjectTsconfig(projectRootAbs);\n if (!configPath) return buildProgramFromSrc(projectRootAbs, {});\n const host = Object.assign({}, ts.sys, {\n onUnRecoverableConfigFileDiagnostic: (): void => undefined,\n }) as ts.ParseConfigFileHost;\n const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host);\n if (!parsed) return null;\n if (parsed.fileNames.length > 0) return ts.createProgram(parsed.fileNames, parsed.options);\n return buildProgramFromSrc(projectRootAbs, parsed.options);\n}\n\n// webpieces-disable no-function-outside-class -- ts Program factory helper, mirrors di-graph/program.ts\nfunction buildProgramFromSrc(projectRootAbs: string, options: ts.CompilerOptions): ts.Program | null {\n const srcDir = path.join(projectRootAbs, 'src');\n if (!fs.existsSync(srcDir)) return null;\n const files = collectTsFiles(srcDir);\n return files.length > 0 ? ts.createProgram(files, options) : null;\n}\n\n// webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here\nfunction collectTsFiles(dir: string): string[] {\n const out: string[] = [];\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') out.push(...collectTsFiles(full));\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n out.push(full);\n }\n }\n return out;\n}\n\n/** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nfunction apiClassInfoFrom(cls: ts.ClassDeclaration, project: string): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n return { api: cls.name.text, owner: project, type: apiTransport(cls) };\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction apiTransport(cls: ts.ClassDeclaration): ApiTransport {\n return hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isAbstractClass(cls: ts.ClassDeclaration): boolean {\n return (ts.getModifiers(cls) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === name);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nfunction calleeMethodName(call: ts.CallExpression): string | null {\n const callee = call.expression;\n if (ts.isPropertyAccessExpression(callee)) return callee.name.text;\n if (ts.isIdentifier(callee)) return callee.text;\n return null;\n}\n\n// webpieces-disable no-function-outside-class -- pure path predicate, matching the sibling helpers in di-graph/bindings.ts\nfunction isTestFile(fileName: string): boolean {\n return (\n fileName.includes('/__tests__/') ||\n fileName.includes('.spec.') ||\n fileName.includes('.test.')\n );\n}\n"]}
|