@webpieces/nx-webpieces-rules 0.4.499 → 0.4.501
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/lib/api-usage/api-ast.d.ts +24 -4
- package/src/lib/api-usage/api-ast.js +41 -3
- package/src/lib/api-usage/api-ast.js.map +1 -1
- package/src/lib/api-usage/api-contract-errors.d.ts +43 -0
- package/src/lib/api-usage/api-contract-errors.js +84 -0
- package/src/lib/api-usage/api-contract-errors.js.map +1 -0
- package/src/lib/api-usage/api-relations.d.ts +52 -0
- package/src/lib/api-usage/api-relations.js +57 -1
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +21 -12
- package/src/lib/api-usage/api-scanner.js +20 -22
- 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.501",
|
|
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.501",
|
|
25
|
+
"@webpieces/code-rules": "0.4.501",
|
|
26
|
+
"@webpieces/eslint-rules": "0.4.501",
|
|
27
|
+
"@webpieces/pr-gate": "0.4.501",
|
|
28
|
+
"@webpieces/rules-config": "0.4.501",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* api-scanner's source pre-pass exists to guard against.
|
|
11
11
|
*/
|
|
12
12
|
import * as ts from 'typescript';
|
|
13
|
-
import { ApiClassInfo, ApiMethodMeta, ApiTransport, NonLiteralDecoratorArg } from './api-relations';
|
|
13
|
+
import { ApiClassInfo, ApiMethodMeta, ApiTransport, EmptiedApiContract, NonLiteralDecoratorArg, UnresolvedEndpointPath } from './api-relations';
|
|
14
14
|
/**
|
|
15
15
|
* The module-scope `const NAME = '<string literal>'` bindings of ONE source file.
|
|
16
16
|
*
|
|
@@ -50,20 +50,34 @@ export declare class DecoratorArgValue {
|
|
|
50
50
|
/** Read one decorator argument as a string, resolving same-module constants. */
|
|
51
51
|
export declare function decoratorArgValue(expr: ts.Expression | undefined, constants: ModuleStringConstants): DecoratorArgValue;
|
|
52
52
|
/**
|
|
53
|
-
* Collects
|
|
53
|
+
* Collects everything this parser-only pass had to drop: decorator arguments it could not reduce to
|
|
54
|
+
* a string, plus the two of those that are FATAL rather than merely lossy.
|
|
54
55
|
*
|
|
55
56
|
* A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`)
|
|
56
57
|
* genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible:
|
|
57
58
|
* the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it
|
|
58
59
|
* turns a silent drop into a named one, pointing at the exact file, line and identifier.
|
|
60
|
+
*
|
|
61
|
+
* Three sinks, because the consequences differ. `record` is the warning stream (a @Queue name falls
|
|
62
|
+
* back to a derived one, so the graph is degraded, not wrong). `recordUnresolvedPath` and
|
|
63
|
+
* `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every
|
|
64
|
+
* offender, because an author fixing five constants wants all five in one run.
|
|
59
65
|
*/
|
|
60
66
|
export declare class DecoratorArgDiagnostics {
|
|
61
67
|
private readonly workspaceRoot;
|
|
62
68
|
private readonly found;
|
|
69
|
+
private readonly unresolvedPaths;
|
|
70
|
+
private readonly emptied;
|
|
63
71
|
constructor(workspaceRoot: string);
|
|
64
72
|
/** Record `argument` (as written) as unresolvable at `node`'s location. */
|
|
65
73
|
record(api: string, decorator: string, method: string | null, argument: string, node: ts.Node): void;
|
|
74
|
+
/** Record an `@Endpoint` whose path argument is unreadable — fatal, see UnresolvedEndpointPathError. */
|
|
75
|
+
recordUnresolvedPath(api: string, method: string, argument: string, node: ts.Node): void;
|
|
76
|
+
/** Record a class that declared `declared` `@Endpoint` methods and kept none of them. */
|
|
77
|
+
recordEmptiedContract(api: string, declared: number, node: ts.Node): void;
|
|
66
78
|
all(): NonLiteralDecoratorArg[];
|
|
79
|
+
unresolvedEndpointPaths(): UnresolvedEndpointPath[];
|
|
80
|
+
emptiedContracts(): EmptiedApiContract[];
|
|
67
81
|
private locate;
|
|
68
82
|
}
|
|
69
83
|
/** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */
|
|
@@ -77,8 +91,14 @@ export declare function apiTransport(cls: ts.ClassDeclaration): ApiTransport;
|
|
|
77
91
|
* would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
|
|
78
92
|
* the blindness the required argument exists to remove.
|
|
79
93
|
*
|
|
80
|
-
* `path` may be a same-module constant; an argument that
|
|
81
|
-
*
|
|
94
|
+
* `path` is NOT skippable. It may be a same-module constant; an argument that is present but still
|
|
95
|
+
* cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation
|
|
96
|
+
* later. Upstream components need the URL — a client computes its request as `basePath + path` — so
|
|
97
|
+
* dropping the method here shipped a contract missing routing information, and a class whose every
|
|
98
|
+
* path was a constant lost every method and disappeared from the graph entirely.
|
|
99
|
+
*
|
|
100
|
+
* A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips
|
|
101
|
+
* zero-method classes, which is the door a gutted contract used to leave through unannounced.
|
|
82
102
|
*/
|
|
83
103
|
export declare function endpointMethodsOf(cls: ts.ClassDeclaration, api: string, constants?: ModuleStringConstants, diagnostics?: DecoratorArgDiagnostics | null): ApiMethodMeta[];
|
|
84
104
|
/** `@Queue('...')` override when present and resolvable, else the derived `${Api}-${method}`. */
|
|
@@ -150,16 +150,24 @@ function decoratorArgValue(expr, constants) {
|
|
|
150
150
|
return new DecoratorArgValue(null, expr.getText());
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
|
-
* Collects
|
|
153
|
+
* Collects everything this parser-only pass had to drop: decorator arguments it could not reduce to
|
|
154
|
+
* a string, plus the two of those that are FATAL rather than merely lossy.
|
|
154
155
|
*
|
|
155
156
|
* A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`)
|
|
156
157
|
* genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible:
|
|
157
158
|
* the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it
|
|
158
159
|
* turns a silent drop into a named one, pointing at the exact file, line and identifier.
|
|
160
|
+
*
|
|
161
|
+
* Three sinks, because the consequences differ. `record` is the warning stream (a @Queue name falls
|
|
162
|
+
* back to a derived one, so the graph is degraded, not wrong). `recordUnresolvedPath` and
|
|
163
|
+
* `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every
|
|
164
|
+
* offender, because an author fixing five constants wants all five in one run.
|
|
159
165
|
*/
|
|
160
166
|
class DecoratorArgDiagnostics {
|
|
161
167
|
workspaceRoot;
|
|
162
168
|
found = [];
|
|
169
|
+
unresolvedPaths = [];
|
|
170
|
+
emptied = [];
|
|
163
171
|
constructor(workspaceRoot) {
|
|
164
172
|
this.workspaceRoot = workspaceRoot;
|
|
165
173
|
}
|
|
@@ -167,9 +175,23 @@ class DecoratorArgDiagnostics {
|
|
|
167
175
|
record(api, decorator, method, argument, node) {
|
|
168
176
|
this.found.push(new api_relations_1.NonLiteralDecoratorArg(api, decorator, method, argument, this.locate(node)));
|
|
169
177
|
}
|
|
178
|
+
/** Record an `@Endpoint` whose path argument is unreadable — fatal, see UnresolvedEndpointPathError. */
|
|
179
|
+
recordUnresolvedPath(api, method, argument, node) {
|
|
180
|
+
this.unresolvedPaths.push(new api_relations_1.UnresolvedEndpointPath(api, method, argument, this.locate(node)));
|
|
181
|
+
}
|
|
182
|
+
/** Record a class that declared `declared` `@Endpoint` methods and kept none of them. */
|
|
183
|
+
recordEmptiedContract(api, declared, node) {
|
|
184
|
+
this.emptied.push(new api_relations_1.EmptiedApiContract(api, declared, this.locate(node)));
|
|
185
|
+
}
|
|
170
186
|
all() {
|
|
171
187
|
return this.found;
|
|
172
188
|
}
|
|
189
|
+
unresolvedEndpointPaths() {
|
|
190
|
+
return this.unresolvedPaths;
|
|
191
|
+
}
|
|
192
|
+
emptiedContracts() {
|
|
193
|
+
return this.emptied;
|
|
194
|
+
}
|
|
173
195
|
locate(node) {
|
|
174
196
|
const sourceFile = node.getSourceFile();
|
|
175
197
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
@@ -209,24 +231,35 @@ const QUEUED_KINDS = ['cloudtasks', 'cron'];
|
|
|
209
231
|
* would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
|
|
210
232
|
* the blindness the required argument exists to remove.
|
|
211
233
|
*
|
|
212
|
-
* `path` may be a same-module constant; an argument that
|
|
213
|
-
*
|
|
234
|
+
* `path` is NOT skippable. It may be a same-module constant; an argument that is present but still
|
|
235
|
+
* cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation
|
|
236
|
+
* later. Upstream components need the URL — a client computes its request as `basePath + path` — so
|
|
237
|
+
* dropping the method here shipped a contract missing routing information, and a class whose every
|
|
238
|
+
* path was a constant lost every method and disappeared from the graph entirely.
|
|
239
|
+
*
|
|
240
|
+
* A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips
|
|
241
|
+
* zero-method classes, which is the door a gutted contract used to leave through unannounced.
|
|
214
242
|
*/
|
|
215
243
|
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
216
244
|
function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new Map()), diagnostics = null) {
|
|
217
245
|
const methods = [];
|
|
246
|
+
let declared = 0;
|
|
218
247
|
for (const member of cls.members) {
|
|
219
248
|
if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name))
|
|
220
249
|
continue;
|
|
221
250
|
const endpoint = memberDecorator(member, 'Endpoint');
|
|
222
251
|
if (endpoint === null)
|
|
223
252
|
continue;
|
|
253
|
+
declared++;
|
|
224
254
|
const name = member.name.text;
|
|
225
255
|
const args = decoratorArgs(endpoint);
|
|
226
256
|
const pathArg = decoratorArgValue(args[0], constants);
|
|
227
257
|
const kindArg = decoratorArgValue(args[1], constants);
|
|
228
258
|
reportUnresolved(diagnostics, api, 'Endpoint', name, pathArg, endpoint);
|
|
229
259
|
reportUnresolved(diagnostics, api, 'Endpoint', name, kindArg, endpoint);
|
|
260
|
+
if (diagnostics !== null && pathArg.unresolvedName !== null) {
|
|
261
|
+
diagnostics.recordUnresolvedPath(api, name, pathArg.unresolvedName, endpoint);
|
|
262
|
+
}
|
|
230
263
|
const kind = kindArg.value;
|
|
231
264
|
if (pathArg.value === null || kind === null || !ENDPOINT_KINDS.includes(kind))
|
|
232
265
|
continue;
|
|
@@ -239,6 +272,11 @@ function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new M
|
|
|
239
272
|
}
|
|
240
273
|
methods.push(method);
|
|
241
274
|
}
|
|
275
|
+
// Declared endpoints, kept none: the class is about to be skipped as "zero methods" and would
|
|
276
|
+
// leave no trace. Never legitimate — a routeless contract declares no @Endpoint at all.
|
|
277
|
+
if (diagnostics !== null && declared > 0 && methods.length === 0) {
|
|
278
|
+
diagnostics.recordEmptiedContract(api, declared, cls);
|
|
279
|
+
}
|
|
242
280
|
return methods;
|
|
243
281
|
}
|
|
244
282
|
/** `@Queue('...')` override when present and resolvable, else the derived `${Api}-${method}`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-ast.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-ast.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API contract AST accessors\n *\n * The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /\n * injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project\n * programs, the source index, relation accumulation) and had grown past the file-size limit.\n *\n * Everything here is parser-level on purpose. Decorators must be read exactly as written, and a\n * plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug\n * api-scanner's source pre-pass exists to guard against.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { classDecorators, decoratorName } from '../di-graph/bindings';\nimport { ApiClassInfo, ApiMethodMeta, ApiTransport, EndpointKind, NonLiteralDecoratorArg } from './api-relations';\n\n/** Legal `@Endpoint(path, kind)` values; anything else is a source error, not a kind we invent. */\nconst ENDPOINT_KINDS: readonly EndpointKind[] = ['rpc', 'cloudtasks', 'cron', 'external'];\n\n/**\n * Name suffix that marks an exported type in an `externalApiPaths` project as a vendor CONTRACT\n * (`GmailApi`, `StorageApi`) rather than one of the DTOs, configs or clients sitting beside it.\n * The same convention the in-repo contracts already follow, applied where no decorator can be read.\n */\nconst EXTERNAL_CONTRACT_SUFFIX = 'Api';\n\n/**\n * Client-config class-name suffix whose FIRST constructor argument is the target service name —\n * `ClientConfig('helper-fsdb')` (rpc) and `TaskClientConfig('helper-fsdb')` (pubsub) both take\n * `svcName` first, and a consumer's own `XxxClientConfig` follows the same shape.\n */\nconst CLIENT_CONFIG_SUFFIX = 'ClientConfig';\n\n/**\n * The module-scope `const NAME = '<string literal>'` bindings of ONE source file.\n *\n * A contract that hoists its route to a constant (`@ApiPath(WHATSAPP_API_PATH)`) is good practice —\n * it lets a sibling contract and its callers share the symbol — but a decorator argument is read as\n * TEXT here, with no checker to constant-fold it. Without this table such an argument resolved to\n * nothing: the class lost its basePath, and a class whose every @Endpoint path was a constant\n * resolved to zero methods and was dropped from the graph entirely.\n *\n * Deliberately SAME-MODULE only. Following an import would mean resolving modules, which is exactly\n * what the source pre-pass avoids (it can be diverted to a decorator-erased `.d.ts`). A cross-module\n * constant is therefore still unresolvable — and is REPORTED rather than silently dropped, see\n * DecoratorArgDiagnostics.\n */\nexport class ModuleStringConstants {\n constructor(private readonly byName: Map<string, string>) {}\n\n lookup(name: string): string | null {\n return this.byName.get(name) ?? null;\n }\n}\n\n/** Parsed constants per source file — every class in a file shares one table. */\nconst CONSTANTS_BY_FILE = new WeakMap<ts.SourceFile, ModuleStringConstants>();\n\n/** The module-scope string constants of `sourceFile`, parsed once per file. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function stringConstantsOf(sourceFile: ts.SourceFile): ModuleStringConstants {\n const cached = CONSTANTS_BY_FILE.get(sourceFile);\n if (cached !== undefined) return cached;\n const byName = new Map<string, string>();\n for (const statement of sourceFile.statements) {\n if (!ts.isVariableStatement(statement)) continue;\n if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;\n for (const declaration of statement.declarationList.declarations) {\n if (!ts.isIdentifier(declaration.name)) continue;\n const text = stringValueOf(declaration.initializer);\n if (text !== null) byName.set(declaration.name.text, text);\n }\n }\n const constants = new ModuleStringConstants(byName);\n CONSTANTS_BY_FILE.set(sourceFile, constants);\n return constants;\n}\n\n/** The string an initializer denotes, unwrapping `as const` / parentheses, else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function stringValueOf(expr: ts.Expression | undefined): string | null {\n if (expr === undefined) return null;\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text;\n if (ts.isAsExpression(expr) || ts.isParenthesizedExpression(expr)) return stringValueOf(expr.expression);\n return null;\n}\n\n/**\n * ONE decorator argument that had to be a string, and what came of it.\n *\n * `value` is the string when it was a literal or resolved through a same-module constant.\n * `unresolvedName` is the argument as written (`WHATSAPP_API_PATH`) when it is present but could not\n * be reduced — the case that must be reported, never silently dropped. Both are null when the\n * argument is simply absent.\n */\nexport class DecoratorArgValue {\n constructor(\n public readonly value: string | null,\n public readonly unresolvedName: string | null,\n ) {}\n}\n\n/** Read one decorator argument as a string, resolving same-module constants. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorArgValue(\n expr: ts.Expression | undefined,\n constants: ModuleStringConstants,\n): DecoratorArgValue {\n if (expr === undefined) return new DecoratorArgValue(null, null);\n const literal = stringValueOf(expr);\n if (literal !== null) return new DecoratorArgValue(literal, null);\n if (ts.isIdentifier(expr)) {\n const resolved = constants.lookup(expr.text);\n if (resolved !== null) return new DecoratorArgValue(resolved, null);\n return new DecoratorArgValue(null, expr.text);\n }\n return new DecoratorArgValue(null, expr.getText());\n}\n\n/**\n * Collects every decorator argument the scan could not reduce to a string.\n *\n * A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`)\n * genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible:\n * the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it\n * turns a silent drop into a named one, pointing at the exact file, line and identifier.\n */\nexport class DecoratorArgDiagnostics {\n private readonly found: NonLiteralDecoratorArg[] = [];\n\n constructor(private readonly workspaceRoot: string) {}\n\n /** Record `argument` (as written) as unresolvable at `node`'s location. */\n record(api: string, decorator: string, method: string | null, argument: string, node: ts.Node): void {\n this.found.push(new NonLiteralDecoratorArg(api, decorator, method, argument, this.locate(node)));\n }\n\n all(): NonLiteralDecoratorArg[] {\n return this.found;\n }\n\n private locate(node: ts.Node): string {\n const sourceFile = node.getSourceFile();\n const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n return `${path.relative(this.workspaceRoot, sourceFile.fileName)}:${position.line + 1}`;\n }\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\nexport function apiClassInfoFrom(\n cls: ts.ClassDeclaration,\n project: string,\n diagnostics: DecoratorArgDiagnostics | null = null,\n): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n const api = cls.name.text;\n const constants = stringConstantsOf(cls.getSourceFile());\n const info: ApiClassInfo = {\n api,\n owner: project,\n type: apiTransport(cls),\n methods: endpointMethodsOf(cls, api, constants, diagnostics),\n };\n const basePath = decoratorStringArg(cls, 'ApiPath', constants, diagnostics, api);\n if (basePath !== null) info.basePath = basePath;\n return info;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function apiTransport(cls: ts.ClassDeclaration): ApiTransport {\n return hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';\n}\n\n/** The @Endpoint kinds that are actually DELIVERED through a named queue or schedule. */\nconst QUEUED_KINDS: readonly EndpointKind[] = ['cloudtasks', 'cron'];\n\n/**\n * Every `@Endpoint(path, kind)` method on a contract class, in declaration order.\n *\n * `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the\n * source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting\n * would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely\n * the blindness the required argument exists to remove.\n *\n * `path` may be a same-module constant; an argument that still cannot be reduced is recorded on\n * `diagnostics` before the method is skipped, so the hole is named rather than merely absent.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function endpointMethodsOf(\n cls: ts.ClassDeclaration,\n api: string,\n constants: ModuleStringConstants = new ModuleStringConstants(new Map<string, string>()),\n diagnostics: DecoratorArgDiagnostics | null = null,\n): ApiMethodMeta[] {\n const methods: ApiMethodMeta[] = [];\n for (const member of cls.members) {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;\n const endpoint = memberDecorator(member, 'Endpoint');\n if (endpoint === null) continue;\n const name = member.name.text;\n const args = decoratorArgs(endpoint);\n const pathArg = decoratorArgValue(args[0], constants);\n const kindArg = decoratorArgValue(args[1], constants);\n reportUnresolved(diagnostics, api, 'Endpoint', name, pathArg, endpoint);\n reportUnresolved(diagnostics, api, 'Endpoint', name, kindArg, endpoint);\n const kind = kindArg.value;\n if (pathArg.value === null || kind === null || !ENDPOINT_KINDS.includes(kind as EndpointKind)) continue;\n const method: ApiMethodMeta = { name, path: pathArg.value, kind: kind as EndpointKind };\n // Only a queued or scheduled endpoint HAS a queue. Naming one for a synchronous rpc invited a\n // tool to read `methods.map(m => m.queueName)` as a provisioning list and create queues that\n // nothing will ever deliver to.\n if (QUEUED_KINDS.includes(method.kind)) {\n method.queueName = queueNameOf(member, api, name, constants, diagnostics);\n }\n methods.push(method);\n }\n return methods;\n}\n\n/** `@Queue('...')` override when present and resolvable, else the derived `${Api}-${method}`. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function queueNameOf(\n member: ts.MethodDeclaration,\n api: string,\n name: string,\n constants: ModuleStringConstants,\n diagnostics: DecoratorArgDiagnostics | null,\n): string {\n const override = memberDecorator(member, 'Queue');\n if (override === null) return `${api}-${name}`;\n const queueArg = decoratorArgValue(decoratorArgs(override)[0], constants);\n reportUnresolved(diagnostics, api, 'Queue', name, queueArg, override);\n return queueArg.value ?? `${api}-${name}`;\n}\n\n/** Record an argument that is present but unresolvable; a resolved or absent one is silent. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function reportUnresolved(\n diagnostics: DecoratorArgDiagnostics | null,\n api: string,\n decorator: string,\n method: string | null,\n arg: DecoratorArgValue,\n node: ts.Node,\n): void {\n if (diagnostics === null || arg.unresolvedName === null) return;\n diagnostics.record(api, decorator, method, arg.unresolvedName, node);\n}\n\n/** The arguments of a decorator's call expression, or [] when it is a bare `@Foo` reference. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorArgs(decorator: ts.Decorator): ts.NodeArray<ts.Expression> | ts.Expression[] {\n return ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];\n}\n\n/** The named decorator on a class member, or null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function memberDecorator(member: ts.ClassElement, name: string): ts.Decorator | null {\n const decorators = ts.getDecorators(member as ts.HasDecorators) ?? [];\n return decorators.find((d: ts.Decorator) => decoratorName(d) === name) ?? null;\n}\n\n/**\n * The first argument of a class decorator as a string (`@ApiPath('/x')`, `@ApiPath(X_PATH)`), else\n * null. A same-module constant resolves; anything else is recorded on `diagnostics`.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorStringArg(\n cls: ts.ClassDeclaration,\n name: string,\n constants: ModuleStringConstants = new ModuleStringConstants(new Map<string, string>()),\n diagnostics: DecoratorArgDiagnostics | null = null,\n api: string = name,\n): string | null {\n const decorator = classDecorators(cls).find((d: ts.Decorator) => decoratorName(d) === name);\n if (decorator === undefined) return null;\n const arg = decoratorArgValue(decoratorArgs(decorator)[0], constants);\n reportUnresolved(diagnostics, api, name, null, arg, decorator);\n return arg.value;\n}\n\n/** The constructor's parameters, or [] when the class declares no constructor. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function constructorParamsOf(cls: ts.ClassDeclaration): readonly ts.ParameterDeclaration[] {\n for (const member of cls.members) {\n if (ts.isConstructorDeclaration(member)) return member.parameters;\n }\n return [];\n}\n\n/**\n * The bare name of a type reference (`GmailApi`, or `gmail.GmailApi` -> `GmailApi`), else null.\n * Generic wrappers are deliberately NOT unwrapped: `Provider<GmailApi>` hands out the contract\n * lazily, which is still a use, but it is not the shape any of these seams take today and guessing\n * at type arguments would start matching things that merely mention a contract.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function typeReferenceName(type: ts.TypeNode | undefined): string | null {\n if (type === undefined || !ts.isTypeReferenceNode(type)) return null;\n const name = type.typeName;\n if (ts.isIdentifier(name)) return name.text;\n return ts.isQualifiedName(name) && ts.isIdentifier(name.right) ? name.right.text : null;\n}\n\n/** Every type name in the class's `implements` clause — the contracts this class IS, not ones it calls. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function implementedTypeNames(cls: ts.ClassDeclaration): Set<string> {\n const names = new Set<string>();\n for (const clause of cls.heritageClauses ?? []) {\n if (clause.token !== ts.SyntaxKind.ImplementsKeyword) continue;\n for (const type of clause.types) {\n if (ts.isIdentifier(type.expression)) names.add(type.expression.text);\n }\n }\n return names;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function 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\nexport function hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === name);\n}\n\n/**\n * The service a client-factory call aims at, from its config argument:\n * `createRpcClient(WarmupApi, new ClientConfig('helper-fsdb'))` → `'helper-fsdb'`.\n *\n * Only a `new <Xxx>ClientConfig('<string literal>')` yields a name. A variable, a template string\n * or a computed expression yields null — the target is genuinely unknown at scan time, and the\n * runtime graph must fall back to fan-out (loudly) rather than guess.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function targetServiceOf(call: ts.CallExpression): string | null {\n if (call.arguments.length < 2) return null;\n const config = call.arguments[1];\n if (!ts.isNewExpression(config) || !ts.isIdentifier(config.expression)) return null;\n if (!config.expression.text.endsWith(CLIENT_CONFIG_SUFFIX)) return null;\n const first = config.arguments?.[0];\n if (first === undefined || !ts.isStringLiteral(first)) return null;\n return first.text.length > 0 ? first.text : null;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function 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\nexport function isTestFile(fileName: string): boolean {\n return (\n fileName.includes('/__tests__/') ||\n fileName.includes('.spec.') ||\n fileName.includes('.test.')\n );\n}\n\n\n/** {api, owner, type:'rpc'|'pubsub'} for an in-repo contract class, else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function apiClassInfoFromNode(\n node: ts.Node,\n project: string,\n diagnostics: DecoratorArgDiagnostics | null = null,\n): ApiClassInfo | null {\n return ts.isClassDeclaration(node) ? apiClassInfoFrom(node, project, diagnostics) : null;\n}\n\n/**\n * {api, owner, type:'external'} for a VENDOR contract, else null.\n *\n * A vendor contract cannot be detected the way an in-repo one is. It carries no @ApiPath (there is\n * no route — the call leaves through a vendor SDK), and it is usually a plain `interface` bound to a\n * Symbol token, which is not even a class. So inside a project the workspace has DECLARED external\n * (`runtime-architecture.externalApiPaths`) the signal is structural instead: an exported\n * `interface`/`abstract class` whose name ends in `Api`. That deliberately picks up `GmailApi` and\n * `StorageApi` while leaving their DTOs, `*Config` types and `*Client` implementations alone.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function externalApiInfoFrom(node: ts.Node, project: string): ApiClassInfo | null {\n const named = ts.isInterfaceDeclaration(node) || (ts.isClassDeclaration(node) && isAbstractClass(node));\n if (!named || !node.name || !isExported(node)) return null;\n const api = node.name.text;\n if (!api.endsWith(EXTERNAL_CONTRACT_SUFFIX)) return null;\n return { api, owner: project, type: 'external', methods: [] };\n}\n\n/** True when the declaration carries an `export` modifier. */\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function isExported(node: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean {\n return (ts.getModifiers(node) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here\nexport function 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"]}
|
|
1
|
+
{"version":3,"file":"api-ast.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-ast.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API contract AST accessors\n *\n * The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /\n * injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project\n * programs, the source index, relation accumulation) and had grown past the file-size limit.\n *\n * Everything here is parser-level on purpose. Decorators must be read exactly as written, and a\n * plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug\n * api-scanner's source pre-pass exists to guard against.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { classDecorators, decoratorName } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiMethodMeta,\n ApiTransport,\n EmptiedApiContract,\n EndpointKind,\n NonLiteralDecoratorArg,\n UnresolvedEndpointPath,\n} from './api-relations';\n\n/** Legal `@Endpoint(path, kind)` values; anything else is a source error, not a kind we invent. */\nconst ENDPOINT_KINDS: readonly EndpointKind[] = ['rpc', 'cloudtasks', 'cron', 'external'];\n\n/**\n * Name suffix that marks an exported type in an `externalApiPaths` project as a vendor CONTRACT\n * (`GmailApi`, `StorageApi`) rather than one of the DTOs, configs or clients sitting beside it.\n * The same convention the in-repo contracts already follow, applied where no decorator can be read.\n */\nconst EXTERNAL_CONTRACT_SUFFIX = 'Api';\n\n/**\n * Client-config class-name suffix whose FIRST constructor argument is the target service name —\n * `ClientConfig('helper-fsdb')` (rpc) and `TaskClientConfig('helper-fsdb')` (pubsub) both take\n * `svcName` first, and a consumer's own `XxxClientConfig` follows the same shape.\n */\nconst CLIENT_CONFIG_SUFFIX = 'ClientConfig';\n\n/**\n * The module-scope `const NAME = '<string literal>'` bindings of ONE source file.\n *\n * A contract that hoists its route to a constant (`@ApiPath(WHATSAPP_API_PATH)`) is good practice —\n * it lets a sibling contract and its callers share the symbol — but a decorator argument is read as\n * TEXT here, with no checker to constant-fold it. Without this table such an argument resolved to\n * nothing: the class lost its basePath, and a class whose every @Endpoint path was a constant\n * resolved to zero methods and was dropped from the graph entirely.\n *\n * Deliberately SAME-MODULE only. Following an import would mean resolving modules, which is exactly\n * what the source pre-pass avoids (it can be diverted to a decorator-erased `.d.ts`). A cross-module\n * constant is therefore still unresolvable — and is REPORTED rather than silently dropped, see\n * DecoratorArgDiagnostics.\n */\nexport class ModuleStringConstants {\n constructor(private readonly byName: Map<string, string>) {}\n\n lookup(name: string): string | null {\n return this.byName.get(name) ?? null;\n }\n}\n\n/** Parsed constants per source file — every class in a file shares one table. */\nconst CONSTANTS_BY_FILE = new WeakMap<ts.SourceFile, ModuleStringConstants>();\n\n/** The module-scope string constants of `sourceFile`, parsed once per file. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function stringConstantsOf(sourceFile: ts.SourceFile): ModuleStringConstants {\n const cached = CONSTANTS_BY_FILE.get(sourceFile);\n if (cached !== undefined) return cached;\n const byName = new Map<string, string>();\n for (const statement of sourceFile.statements) {\n if (!ts.isVariableStatement(statement)) continue;\n if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;\n for (const declaration of statement.declarationList.declarations) {\n if (!ts.isIdentifier(declaration.name)) continue;\n const text = stringValueOf(declaration.initializer);\n if (text !== null) byName.set(declaration.name.text, text);\n }\n }\n const constants = new ModuleStringConstants(byName);\n CONSTANTS_BY_FILE.set(sourceFile, constants);\n return constants;\n}\n\n/** The string an initializer denotes, unwrapping `as const` / parentheses, else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function stringValueOf(expr: ts.Expression | undefined): string | null {\n if (expr === undefined) return null;\n if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text;\n if (ts.isAsExpression(expr) || ts.isParenthesizedExpression(expr)) return stringValueOf(expr.expression);\n return null;\n}\n\n/**\n * ONE decorator argument that had to be a string, and what came of it.\n *\n * `value` is the string when it was a literal or resolved through a same-module constant.\n * `unresolvedName` is the argument as written (`WHATSAPP_API_PATH`) when it is present but could not\n * be reduced — the case that must be reported, never silently dropped. Both are null when the\n * argument is simply absent.\n */\nexport class DecoratorArgValue {\n constructor(\n public readonly value: string | null,\n public readonly unresolvedName: string | null,\n ) {}\n}\n\n/** Read one decorator argument as a string, resolving same-module constants. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorArgValue(\n expr: ts.Expression | undefined,\n constants: ModuleStringConstants,\n): DecoratorArgValue {\n if (expr === undefined) return new DecoratorArgValue(null, null);\n const literal = stringValueOf(expr);\n if (literal !== null) return new DecoratorArgValue(literal, null);\n if (ts.isIdentifier(expr)) {\n const resolved = constants.lookup(expr.text);\n if (resolved !== null) return new DecoratorArgValue(resolved, null);\n return new DecoratorArgValue(null, expr.text);\n }\n return new DecoratorArgValue(null, expr.getText());\n}\n\n/**\n * Collects everything this parser-only pass had to drop: decorator arguments it could not reduce to\n * a string, plus the two of those that are FATAL rather than merely lossy.\n *\n * A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`)\n * genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible:\n * the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it\n * turns a silent drop into a named one, pointing at the exact file, line and identifier.\n *\n * Three sinks, because the consequences differ. `record` is the warning stream (a @Queue name falls\n * back to a derived one, so the graph is degraded, not wrong). `recordUnresolvedPath` and\n * `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every\n * offender, because an author fixing five constants wants all five in one run.\n */\nexport class DecoratorArgDiagnostics {\n private readonly found: NonLiteralDecoratorArg[] = [];\n private readonly unresolvedPaths: UnresolvedEndpointPath[] = [];\n private readonly emptied: EmptiedApiContract[] = [];\n\n constructor(private readonly workspaceRoot: string) {}\n\n /** Record `argument` (as written) as unresolvable at `node`'s location. */\n record(api: string, decorator: string, method: string | null, argument: string, node: ts.Node): void {\n this.found.push(new NonLiteralDecoratorArg(api, decorator, method, argument, this.locate(node)));\n }\n\n /** Record an `@Endpoint` whose path argument is unreadable — fatal, see UnresolvedEndpointPathError. */\n recordUnresolvedPath(api: string, method: string, argument: string, node: ts.Node): void {\n this.unresolvedPaths.push(new UnresolvedEndpointPath(api, method, argument, this.locate(node)));\n }\n\n /** Record a class that declared `declared` `@Endpoint` methods and kept none of them. */\n recordEmptiedContract(api: string, declared: number, node: ts.Node): void {\n this.emptied.push(new EmptiedApiContract(api, declared, this.locate(node)));\n }\n\n all(): NonLiteralDecoratorArg[] {\n return this.found;\n }\n\n unresolvedEndpointPaths(): UnresolvedEndpointPath[] {\n return this.unresolvedPaths;\n }\n\n emptiedContracts(): EmptiedApiContract[] {\n return this.emptied;\n }\n\n private locate(node: ts.Node): string {\n const sourceFile = node.getSourceFile();\n const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n return `${path.relative(this.workspaceRoot, sourceFile.fileName)}:${position.line + 1}`;\n }\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\nexport function apiClassInfoFrom(\n cls: ts.ClassDeclaration,\n project: string,\n diagnostics: DecoratorArgDiagnostics | null = null,\n): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n const api = cls.name.text;\n const constants = stringConstantsOf(cls.getSourceFile());\n const info: ApiClassInfo = {\n api,\n owner: project,\n type: apiTransport(cls),\n methods: endpointMethodsOf(cls, api, constants, diagnostics),\n };\n const basePath = decoratorStringArg(cls, 'ApiPath', constants, diagnostics, api);\n if (basePath !== null) info.basePath = basePath;\n return info;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function apiTransport(cls: ts.ClassDeclaration): ApiTransport {\n return hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';\n}\n\n/** The @Endpoint kinds that are actually DELIVERED through a named queue or schedule. */\nconst QUEUED_KINDS: readonly EndpointKind[] = ['cloudtasks', 'cron'];\n\n/**\n * Every `@Endpoint(path, kind)` method on a contract class, in declaration order.\n *\n * `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the\n * source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting\n * would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely\n * the blindness the required argument exists to remove.\n *\n * `path` is NOT skippable. It may be a same-module constant; an argument that is present but still\n * cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation\n * later. Upstream components need the URL — a client computes its request as `basePath + path` — so\n * dropping the method here shipped a contract missing routing information, and a class whose every\n * path was a constant lost every method and disappeared from the graph entirely.\n *\n * A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips\n * zero-method classes, which is the door a gutted contract used to leave through unannounced.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function endpointMethodsOf(\n cls: ts.ClassDeclaration,\n api: string,\n constants: ModuleStringConstants = new ModuleStringConstants(new Map<string, string>()),\n diagnostics: DecoratorArgDiagnostics | null = null,\n): ApiMethodMeta[] {\n const methods: ApiMethodMeta[] = [];\n let declared = 0;\n for (const member of cls.members) {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;\n const endpoint = memberDecorator(member, 'Endpoint');\n if (endpoint === null) continue;\n declared++;\n const name = member.name.text;\n const args = decoratorArgs(endpoint);\n const pathArg = decoratorArgValue(args[0], constants);\n const kindArg = decoratorArgValue(args[1], constants);\n reportUnresolved(diagnostics, api, 'Endpoint', name, pathArg, endpoint);\n reportUnresolved(diagnostics, api, 'Endpoint', name, kindArg, endpoint);\n if (diagnostics !== null && pathArg.unresolvedName !== null) {\n diagnostics.recordUnresolvedPath(api, name, pathArg.unresolvedName, endpoint);\n }\n const kind = kindArg.value;\n if (pathArg.value === null || kind === null || !ENDPOINT_KINDS.includes(kind as EndpointKind)) continue;\n const method: ApiMethodMeta = { name, path: pathArg.value, kind: kind as EndpointKind };\n // Only a queued or scheduled endpoint HAS a queue. Naming one for a synchronous rpc invited a\n // tool to read `methods.map(m => m.queueName)` as a provisioning list and create queues that\n // nothing will ever deliver to.\n if (QUEUED_KINDS.includes(method.kind)) {\n method.queueName = queueNameOf(member, api, name, constants, diagnostics);\n }\n methods.push(method);\n }\n // Declared endpoints, kept none: the class is about to be skipped as \"zero methods\" and would\n // leave no trace. Never legitimate — a routeless contract declares no @Endpoint at all.\n if (diagnostics !== null && declared > 0 && methods.length === 0) {\n diagnostics.recordEmptiedContract(api, declared, cls);\n }\n return methods;\n}\n\n/** `@Queue('...')` override when present and resolvable, else the derived `${Api}-${method}`. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function queueNameOf(\n member: ts.MethodDeclaration,\n api: string,\n name: string,\n constants: ModuleStringConstants,\n diagnostics: DecoratorArgDiagnostics | null,\n): string {\n const override = memberDecorator(member, 'Queue');\n if (override === null) return `${api}-${name}`;\n const queueArg = decoratorArgValue(decoratorArgs(override)[0], constants);\n reportUnresolved(diagnostics, api, 'Queue', name, queueArg, override);\n return queueArg.value ?? `${api}-${name}`;\n}\n\n/** Record an argument that is present but unresolvable; a resolved or absent one is silent. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function reportUnresolved(\n diagnostics: DecoratorArgDiagnostics | null,\n api: string,\n decorator: string,\n method: string | null,\n arg: DecoratorArgValue,\n node: ts.Node,\n): void {\n if (diagnostics === null || arg.unresolvedName === null) return;\n diagnostics.record(api, decorator, method, arg.unresolvedName, node);\n}\n\n/** The arguments of a decorator's call expression, or [] when it is a bare `@Foo` reference. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorArgs(decorator: ts.Decorator): ts.NodeArray<ts.Expression> | ts.Expression[] {\n return ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];\n}\n\n/** The named decorator on a class member, or null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function memberDecorator(member: ts.ClassElement, name: string): ts.Decorator | null {\n const decorators = ts.getDecorators(member as ts.HasDecorators) ?? [];\n return decorators.find((d: ts.Decorator) => decoratorName(d) === name) ?? null;\n}\n\n/**\n * The first argument of a class decorator as a string (`@ApiPath('/x')`, `@ApiPath(X_PATH)`), else\n * null. A same-module constant resolves; anything else is recorded on `diagnostics`.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorStringArg(\n cls: ts.ClassDeclaration,\n name: string,\n constants: ModuleStringConstants = new ModuleStringConstants(new Map<string, string>()),\n diagnostics: DecoratorArgDiagnostics | null = null,\n api: string = name,\n): string | null {\n const decorator = classDecorators(cls).find((d: ts.Decorator) => decoratorName(d) === name);\n if (decorator === undefined) return null;\n const arg = decoratorArgValue(decoratorArgs(decorator)[0], constants);\n reportUnresolved(diagnostics, api, name, null, arg, decorator);\n return arg.value;\n}\n\n/** The constructor's parameters, or [] when the class declares no constructor. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function constructorParamsOf(cls: ts.ClassDeclaration): readonly ts.ParameterDeclaration[] {\n for (const member of cls.members) {\n if (ts.isConstructorDeclaration(member)) return member.parameters;\n }\n return [];\n}\n\n/**\n * The bare name of a type reference (`GmailApi`, or `gmail.GmailApi` -> `GmailApi`), else null.\n * Generic wrappers are deliberately NOT unwrapped: `Provider<GmailApi>` hands out the contract\n * lazily, which is still a use, but it is not the shape any of these seams take today and guessing\n * at type arguments would start matching things that merely mention a contract.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function typeReferenceName(type: ts.TypeNode | undefined): string | null {\n if (type === undefined || !ts.isTypeReferenceNode(type)) return null;\n const name = type.typeName;\n if (ts.isIdentifier(name)) return name.text;\n return ts.isQualifiedName(name) && ts.isIdentifier(name.right) ? name.right.text : null;\n}\n\n/** Every type name in the class's `implements` clause — the contracts this class IS, not ones it calls. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function implementedTypeNames(cls: ts.ClassDeclaration): Set<string> {\n const names = new Set<string>();\n for (const clause of cls.heritageClauses ?? []) {\n if (clause.token !== ts.SyntaxKind.ImplementsKeyword) continue;\n for (const type of clause.types) {\n if (ts.isIdentifier(type.expression)) names.add(type.expression.text);\n }\n }\n return names;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function 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\nexport function hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === name);\n}\n\n/**\n * The service a client-factory call aims at, from its config argument:\n * `createRpcClient(WarmupApi, new ClientConfig('helper-fsdb'))` → `'helper-fsdb'`.\n *\n * Only a `new <Xxx>ClientConfig('<string literal>')` yields a name. A variable, a template string\n * or a computed expression yields null — the target is genuinely unknown at scan time, and the\n * runtime graph must fall back to fan-out (loudly) rather than guess.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function targetServiceOf(call: ts.CallExpression): string | null {\n if (call.arguments.length < 2) return null;\n const config = call.arguments[1];\n if (!ts.isNewExpression(config) || !ts.isIdentifier(config.expression)) return null;\n if (!config.expression.text.endsWith(CLIENT_CONFIG_SUFFIX)) return null;\n const first = config.arguments?.[0];\n if (first === undefined || !ts.isStringLiteral(first)) return null;\n return first.text.length > 0 ? first.text : null;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function 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\nexport function isTestFile(fileName: string): boolean {\n return (\n fileName.includes('/__tests__/') ||\n fileName.includes('.spec.') ||\n fileName.includes('.test.')\n );\n}\n\n\n/** {api, owner, type:'rpc'|'pubsub'} for an in-repo contract class, else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function apiClassInfoFromNode(\n node: ts.Node,\n project: string,\n diagnostics: DecoratorArgDiagnostics | null = null,\n): ApiClassInfo | null {\n return ts.isClassDeclaration(node) ? apiClassInfoFrom(node, project, diagnostics) : null;\n}\n\n/**\n * {api, owner, type:'external'} for a VENDOR contract, else null.\n *\n * A vendor contract cannot be detected the way an in-repo one is. It carries no @ApiPath (there is\n * no route — the call leaves through a vendor SDK), and it is usually a plain `interface` bound to a\n * Symbol token, which is not even a class. So inside a project the workspace has DECLARED external\n * (`runtime-architecture.externalApiPaths`) the signal is structural instead: an exported\n * `interface`/`abstract class` whose name ends in `Api`. That deliberately picks up `GmailApi` and\n * `StorageApi` while leaving their DTOs, `*Config` types and `*Client` implementations alone.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function externalApiInfoFrom(node: ts.Node, project: string): ApiClassInfo | null {\n const named = ts.isInterfaceDeclaration(node) || (ts.isClassDeclaration(node) && isAbstractClass(node));\n if (!named || !node.name || !isExported(node)) return null;\n const api = node.name.text;\n if (!api.endsWith(EXTERNAL_CONTRACT_SUFFIX)) return null;\n return { api, owner: project, type: 'external', methods: [] };\n}\n\n/** True when the declaration carries an `export` modifier. */\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function isExported(node: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean {\n return (ts.getModifiers(node) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here\nexport function 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"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three ways `buildApiContracts` refuses to emit a green, wrong `apiContracts` table.
|
|
3
|
+
*
|
|
4
|
+
* All three share one rule: an entry that is PRESENT but incomplete is worse than an absent one.
|
|
5
|
+
* Every other entry in the table is complete, so a consumer has no reason to suspect the one that
|
|
6
|
+
* lost a field — it just computes a confidently wrong URL, or draws a service with no queues.
|
|
7
|
+
*
|
|
8
|
+
* Each aggregates EVERY offender into one message rather than throwing on the first: an author who
|
|
9
|
+
* moved a constants module broke five decorators at once and wants all five named in one run.
|
|
10
|
+
*
|
|
11
|
+
* Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.
|
|
12
|
+
*/
|
|
13
|
+
import { EmptiedApiContract, UnresolvedEndpointPath } from './api-relations';
|
|
14
|
+
/**
|
|
15
|
+
* A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the
|
|
16
|
+
* entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.
|
|
17
|
+
*/
|
|
18
|
+
export declare class MissingBasePathError extends Error {
|
|
19
|
+
readonly contracts: readonly string[];
|
|
20
|
+
constructor(contracts: readonly string[]);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the
|
|
24
|
+
* two arguments are the two halves of ONE url. An http client builds its request as
|
|
25
|
+
* `basePath + path`, so a contract shipped without a method's path is missing routing information,
|
|
26
|
+
* and the consumer computes a confidently wrong URL. Skipping the method instead was worse still —
|
|
27
|
+
* a class whose every path was an unreadable constant lost every method and vanished from the graph.
|
|
28
|
+
*/
|
|
29
|
+
export declare class UnresolvedEndpointPathError extends Error {
|
|
30
|
+
readonly paths: readonly UnresolvedEndpointPath[];
|
|
31
|
+
constructor(paths: readonly UnresolvedEndpointPath[]);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the
|
|
35
|
+
* alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips
|
|
36
|
+
* a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator
|
|
37
|
+
* arguments used the very same exit — which is how a service lost two real Cloud Tasks queues and an
|
|
38
|
+
* inbound webhook without a single line of output.
|
|
39
|
+
*/
|
|
40
|
+
export declare class EmptiedApiContractError extends Error {
|
|
41
|
+
readonly contracts: readonly EmptiedApiContract[];
|
|
42
|
+
constructor(contracts: readonly EmptiedApiContract[]);
|
|
43
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The three ways `buildApiContracts` refuses to emit a green, wrong `apiContracts` table.
|
|
4
|
+
*
|
|
5
|
+
* All three share one rule: an entry that is PRESENT but incomplete is worse than an absent one.
|
|
6
|
+
* Every other entry in the table is complete, so a consumer has no reason to suspect the one that
|
|
7
|
+
* lost a field — it just computes a confidently wrong URL, or draws a service with no queues.
|
|
8
|
+
*
|
|
9
|
+
* Each aggregates EVERY offender into one message rather than throwing on the first: an author who
|
|
10
|
+
* moved a constants module broke five decorators at once and wants all five named in one run.
|
|
11
|
+
*
|
|
12
|
+
* Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.EmptiedApiContractError = exports.UnresolvedEndpointPathError = exports.MissingBasePathError = void 0;
|
|
16
|
+
/**
|
|
17
|
+
* A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the
|
|
18
|
+
* entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.
|
|
19
|
+
*/
|
|
20
|
+
class MissingBasePathError extends Error {
|
|
21
|
+
contracts;
|
|
22
|
+
constructor(contracts) {
|
|
23
|
+
super(`${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\n` +
|
|
24
|
+
contracts.map((c) => ` • ${c}`).join('\n') +
|
|
25
|
+
`\n basePath is REQUIRED in apiContracts — an entry without it makes every consumer\n` +
|
|
26
|
+
` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\n` +
|
|
27
|
+
` or move the constant into the same module as the contract class.`);
|
|
28
|
+
this.contracts = contracts;
|
|
29
|
+
this.name = 'MissingBasePathError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.MissingBasePathError = MissingBasePathError;
|
|
33
|
+
/**
|
|
34
|
+
* `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the
|
|
35
|
+
* two arguments are the two halves of ONE url. An http client builds its request as
|
|
36
|
+
* `basePath + path`, so a contract shipped without a method's path is missing routing information,
|
|
37
|
+
* and the consumer computes a confidently wrong URL. Skipping the method instead was worse still —
|
|
38
|
+
* a class whose every path was an unreadable constant lost every method and vanished from the graph.
|
|
39
|
+
*/
|
|
40
|
+
class UnresolvedEndpointPathError extends Error {
|
|
41
|
+
paths;
|
|
42
|
+
constructor(paths) {
|
|
43
|
+
super(`${paths.length} @Endpoint path(s) could not be read as a string:\n` +
|
|
44
|
+
paths
|
|
45
|
+
.map((p) => ` • ${p.api}.${p.method} — @Endpoint(${p.argument}, ...) at ${p.at}`)
|
|
46
|
+
.join('\n') +
|
|
47
|
+
`\n path is REQUIRED in apiContracts — every consumer builds its request URL as\n` +
|
|
48
|
+
` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\n` +
|
|
49
|
+
` and a class whose every path is unreadable drops out of the graph entirely.\n` +
|
|
50
|
+
` Inline the @Endpoint string literal, or move the constant into the SAME module as\n` +
|
|
51
|
+
` the contract class — a same-module const IS resolved, one imported from another\n` +
|
|
52
|
+
` module is NOT (this scan is parser-only by design: module resolution can land on a\n` +
|
|
53
|
+
` decorator-erased .d.ts).`);
|
|
54
|
+
this.paths = paths;
|
|
55
|
+
this.name = 'UnresolvedEndpointPathError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.UnresolvedEndpointPathError = UnresolvedEndpointPathError;
|
|
59
|
+
/**
|
|
60
|
+
* Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the
|
|
61
|
+
* alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips
|
|
62
|
+
* a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator
|
|
63
|
+
* arguments used the very same exit — which is how a service lost two real Cloud Tasks queues and an
|
|
64
|
+
* inbound webhook without a single line of output.
|
|
65
|
+
*/
|
|
66
|
+
class EmptiedApiContractError extends Error {
|
|
67
|
+
contracts;
|
|
68
|
+
constructor(contracts) {
|
|
69
|
+
super(`${contracts.length} API contract class(es) declare @Endpoint methods but kept NONE of them:\n` +
|
|
70
|
+
contracts
|
|
71
|
+
.map((c) => ` • ${c.api} — ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`)
|
|
72
|
+
.join('\n') +
|
|
73
|
+
`\n A contract with zero usable methods is DROPPED from apiContracts, so the class,\n` +
|
|
74
|
+
` its queues and its triggers disappear from the architecture graph with no error.\n` +
|
|
75
|
+
` Both @Endpoint arguments must be readable: the path as a string literal or a\n` +
|
|
76
|
+
` SAME-module const, and the kind as a literal 'rpc' | 'cloudtasks' | 'cron' |\n` +
|
|
77
|
+
` 'external'. Fix the arguments above, or remove the @Endpoint decorators if the\n` +
|
|
78
|
+
` class is genuinely not routed.`);
|
|
79
|
+
this.contracts = contracts;
|
|
80
|
+
this.name = 'EmptiedApiContractError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.EmptiedApiContractError = EmptiedApiContractError;
|
|
84
|
+
//# sourceMappingURL=api-contract-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-contract-errors.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-contract-errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAIH;;;GAGG;AACH,MAAa,oBAAqB,SAAQ,KAAK;IACf;IAA5B,YAA4B,SAA4B;QACpD,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,8EAA8E;YAC7F,SAAS,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YACtD,wFAAwF;YACxF,0FAA0F;YAC1F,qEAAqE,CAC5E,CAAC;QAPsB,cAAS,GAAT,SAAS,CAAmB;QAQpD,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACvC,CAAC;CACJ;AAXD,oDAWC;AAED;;;;;;GAMG;AACH,MAAa,2BAA4B,SAAQ,KAAK;IACtB;IAA5B,YAA4B,KAAwC;QAChE,KAAK,CACD,GAAG,KAAK,CAAC,MAAM,qDAAqD;YAChE,KAAK;iBACA,GAAG,CACA,CAAC,CAAyB,EAAE,EAAE,CAC1B,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,gBAAgB,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,EAAE,EAAE,CAC/E;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,oFAAoF;YACpF,wFAAwF;YACxF,kFAAkF;YAClF,wFAAwF;YACxF,sFAAsF;YACtF,yFAAyF;YACzF,6BAA6B,CACpC,CAAC;QAhBsB,UAAK,GAAL,KAAK,CAAmC;QAiBhE,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;IAC9C,CAAC;CACJ;AApBD,kEAoBC;AAED;;;;;;GAMG;AACH,MAAa,uBAAwB,SAAQ,KAAK;IAClB;IAA5B,YAA4B,SAAwC;QAChE,KAAK,CACD,GAAG,SAAS,CAAC,MAAM,4EAA4E;YAC3F,SAAS;iBACJ,GAAG,CACA,CAAC,CAAqB,EAAE,EAAE,CACtB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,QAAQ,+CAA+C,CAAC,CAAC,EAAE,EAAE,CAC3F;iBACA,IAAI,CAAC,IAAI,CAAC;YACf,wFAAwF;YACxF,uFAAuF;YACvF,mFAAmF;YACnF,mFAAmF;YACnF,qFAAqF;YACrF,mCAAmC,CAC1C,CAAC;QAfsB,cAAS,GAAT,SAAS,CAA+B;QAgBhE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IAC1C,CAAC;CACJ;AAnBD,0DAmBC","sourcesContent":["/**\n * The three ways `buildApiContracts` refuses to emit a green, wrong `apiContracts` table.\n *\n * All three share one rule: an entry that is PRESENT but incomplete is worse than an absent one.\n * Every other entry in the table is complete, so a consumer has no reason to suspect the one that\n * lost a field — it just computes a confidently wrong URL, or draws a service with no queues.\n *\n * Each aggregates EVERY offender into one message rather than throwing on the first: an author who\n * moved a constants module broke five decorators at once and wants all five named in one run.\n *\n * Split out of api-scanner.ts, which owns the scan itself and is at its file-size limit.\n */\n\nimport { EmptiedApiContract, UnresolvedEndpointPath } from './api-relations';\n\n/**\n * A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the\n * entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.\n */\nexport class MissingBasePathError extends Error {\n constructor(public readonly contracts: readonly string[]) {\n super(\n `${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\\n` +\n contracts.map((c: string) => ` • ${c}`).join('\\n') +\n `\\n basePath is REQUIRED in apiContracts — an entry without it makes every consumer\\n` +\n ` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\\n` +\n ` or move the constant into the same module as the contract class.`,\n );\n this.name = 'MissingBasePathError';\n }\n}\n\n/**\n * `@Endpoint` paths the scan could not read. Fatal for the same reason MissingBasePathError is: the\n * two arguments are the two halves of ONE url. An http client builds its request as\n * `basePath + path`, so a contract shipped without a method's path is missing routing information,\n * and the consumer computes a confidently wrong URL. Skipping the method instead was worse still —\n * a class whose every path was an unreadable constant lost every method and vanished from the graph.\n */\nexport class UnresolvedEndpointPathError extends Error {\n constructor(public readonly paths: readonly UnresolvedEndpointPath[]) {\n super(\n `${paths.length} @Endpoint path(s) could not be read as a string:\\n` +\n paths\n .map(\n (p: UnresolvedEndpointPath) =>\n ` • ${p.api}.${p.method} — @Endpoint(${p.argument}, ...) at ${p.at}`,\n )\n .join('\\n') +\n `\\n path is REQUIRED in apiContracts — every consumer builds its request URL as\\n` +\n ` basePath + path, so an unreadable path is MISSING ROUTING, not cosmetic metadata,\\n` +\n ` and a class whose every path is unreadable drops out of the graph entirely.\\n` +\n ` Inline the @Endpoint string literal, or move the constant into the SAME module as\\n` +\n ` the contract class — a same-module const IS resolved, one imported from another\\n` +\n ` module is NOT (this scan is parser-only by design: module resolution can land on a\\n` +\n ` decorator-erased .d.ts).`,\n );\n this.name = 'UnresolvedEndpointPathError';\n }\n}\n\n/**\n * Contract classes that declared `@Endpoint` methods and kept none of them. Fatal because the\n * alternative is the silent drop the api scan exists to close: buildApiContracts legitimately skips\n * a zero-method class (a vendor seam has no routes), and a class gutted by unreadable decorator\n * arguments used the very same exit — which is how a service lost two real Cloud Tasks queues and an\n * inbound webhook without a single line of output.\n */\nexport class EmptiedApiContractError extends Error {\n constructor(public readonly contracts: readonly EmptiedApiContract[]) {\n super(\n `${contracts.length} API contract class(es) declare @Endpoint methods but kept NONE of them:\\n` +\n contracts\n .map(\n (c: EmptiedApiContract) =>\n ` • ${c.api} — ${c.declared} @Endpoint method(s) declared, 0 usable, at ${c.at}`,\n )\n .join('\\n') +\n `\\n A contract with zero usable methods is DROPPED from apiContracts, so the class,\\n` +\n ` its queues and its triggers disappear from the architecture graph with no error.\\n` +\n ` Both @Endpoint arguments must be readable: the path as a string literal or a\\n` +\n ` SAME-module const, and the kind as a literal 'rpc' | 'cloudtasks' | 'cron' |\\n` +\n ` 'external'. Fix the arguments above, or remove the @Endpoint decorators if the\\n` +\n ` class is genuinely not routed.`,\n );\n this.name = 'EmptiedApiContractError';\n }\n}\n"]}
|
|
@@ -175,6 +175,58 @@ export declare class NonLiteralDecoratorArg {
|
|
|
175
175
|
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
176
176
|
at: string);
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.
|
|
180
|
+
*
|
|
181
|
+
* Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because
|
|
182
|
+
* this one is FATAL. Upstream components need the URL: an http client builds its request as
|
|
183
|
+
* `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same
|
|
184
|
+
* reasoning that already makes basePath required. Skipping the method instead used to delete it, and
|
|
185
|
+
* a class whose every path was a constant lost every method and vanished from `apiContracts` with
|
|
186
|
+
* nothing printed anywhere.
|
|
187
|
+
*/
|
|
188
|
+
export declare class UnresolvedEndpointPath {
|
|
189
|
+
/** The contract class the method is declared on. */
|
|
190
|
+
readonly api: string;
|
|
191
|
+
/** The method name. */
|
|
192
|
+
readonly method: string;
|
|
193
|
+
/** The path argument exactly as written, e.g. `PROCESS_PATH`. */
|
|
194
|
+
readonly argument: string;
|
|
195
|
+
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
196
|
+
readonly at: string;
|
|
197
|
+
constructor(
|
|
198
|
+
/** The contract class the method is declared on. */
|
|
199
|
+
api: string,
|
|
200
|
+
/** The method name. */
|
|
201
|
+
method: string,
|
|
202
|
+
/** The path argument exactly as written, e.g. `PROCESS_PATH`. */
|
|
203
|
+
argument: string,
|
|
204
|
+
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
205
|
+
at: string);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* A contract class that DECLARED `@Endpoint` methods and kept none of them.
|
|
209
|
+
*
|
|
210
|
+
* The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method
|
|
211
|
+
* class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator
|
|
212
|
+
* arguments left through the same door as a legitimately routeless one. A class that declared
|
|
213
|
+
* endpoints and produced none is never legitimate, so it is named instead.
|
|
214
|
+
*/
|
|
215
|
+
export declare class EmptiedApiContract {
|
|
216
|
+
/** The contract class name. */
|
|
217
|
+
readonly api: string;
|
|
218
|
+
/** How many `@Endpoint` decorators were written on it. */
|
|
219
|
+
readonly declared: number;
|
|
220
|
+
/** `path/to/file.ts:LINE` of the class, workspace-relative. */
|
|
221
|
+
readonly at: string;
|
|
222
|
+
constructor(
|
|
223
|
+
/** The contract class name. */
|
|
224
|
+
api: string,
|
|
225
|
+
/** How many `@Endpoint` decorators were written on it. */
|
|
226
|
+
declared: number,
|
|
227
|
+
/** `path/to/file.ts:LINE` of the class, workspace-relative. */
|
|
228
|
+
at: string);
|
|
229
|
+
}
|
|
178
230
|
/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */
|
|
179
231
|
export declare function deriveApiRelationKind(implementsRefs: ApiRef[], usesRefs: ApiRef[]): ApiRelationKind;
|
|
180
232
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* only as binding identifiers, not as member names).
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.NonLiteralDecoratorArg = void 0;
|
|
18
|
+
exports.EmptiedApiContract = exports.UnresolvedEndpointPath = exports.NonLiteralDecoratorArg = void 0;
|
|
19
19
|
exports.apiRefKey = apiRefKey;
|
|
20
20
|
exports.deriveApiRelationKind = deriveApiRelationKind;
|
|
21
21
|
exports.sortApiRefs = sortApiRefs;
|
|
@@ -61,6 +61,62 @@ class NonLiteralDecoratorArg {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
exports.NonLiteralDecoratorArg = NonLiteralDecoratorArg;
|
|
64
|
+
/**
|
|
65
|
+
* ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.
|
|
66
|
+
*
|
|
67
|
+
* Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because
|
|
68
|
+
* this one is FATAL. Upstream components need the URL: an http client builds its request as
|
|
69
|
+
* `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same
|
|
70
|
+
* reasoning that already makes basePath required. Skipping the method instead used to delete it, and
|
|
71
|
+
* a class whose every path was a constant lost every method and vanished from `apiContracts` with
|
|
72
|
+
* nothing printed anywhere.
|
|
73
|
+
*/
|
|
74
|
+
class UnresolvedEndpointPath {
|
|
75
|
+
api;
|
|
76
|
+
method;
|
|
77
|
+
argument;
|
|
78
|
+
at;
|
|
79
|
+
constructor(
|
|
80
|
+
/** The contract class the method is declared on. */
|
|
81
|
+
api,
|
|
82
|
+
/** The method name. */
|
|
83
|
+
method,
|
|
84
|
+
/** The path argument exactly as written, e.g. `PROCESS_PATH`. */
|
|
85
|
+
argument,
|
|
86
|
+
/** `path/to/file.ts:LINE`, workspace-relative. */
|
|
87
|
+
at) {
|
|
88
|
+
this.api = api;
|
|
89
|
+
this.method = method;
|
|
90
|
+
this.argument = argument;
|
|
91
|
+
this.at = at;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.UnresolvedEndpointPath = UnresolvedEndpointPath;
|
|
95
|
+
/**
|
|
96
|
+
* A contract class that DECLARED `@Endpoint` methods and kept none of them.
|
|
97
|
+
*
|
|
98
|
+
* The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method
|
|
99
|
+
* class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator
|
|
100
|
+
* arguments left through the same door as a legitimately routeless one. A class that declared
|
|
101
|
+
* endpoints and produced none is never legitimate, so it is named instead.
|
|
102
|
+
*/
|
|
103
|
+
class EmptiedApiContract {
|
|
104
|
+
api;
|
|
105
|
+
declared;
|
|
106
|
+
at;
|
|
107
|
+
constructor(
|
|
108
|
+
/** The contract class name. */
|
|
109
|
+
api,
|
|
110
|
+
/** How many `@Endpoint` decorators were written on it. */
|
|
111
|
+
declared,
|
|
112
|
+
/** `path/to/file.ts:LINE` of the class, workspace-relative. */
|
|
113
|
+
at) {
|
|
114
|
+
this.api = api;
|
|
115
|
+
this.declared = declared;
|
|
116
|
+
this.at = at;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.EmptiedApiContract = EmptiedApiContract;
|
|
64
120
|
/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */
|
|
65
121
|
// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs
|
|
66
122
|
function deriveApiRelationKind(implementsRefs, usesRefs) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAyDH,8BAEC;AAiHD,sDAIC;AAOD,kCAKC;AAxID;;;GAGG;AACH,+FAA+F;AAC/F,SAAgB,SAAS,CAAC,GAAW;IACjC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;AACnD,CAAC;AAuFD;;;;;;;;GAQG;AACH,MAAa,sBAAsB;IAGX;IAEA;IAEA;IAEA;IAEA;IAVpB;IACI,sDAAsD;IACtC,GAAW;IAC3B,wCAAwC;IACxB,SAAiB;IACjC,0EAA0E;IAC1D,MAAqB;IACrC,iEAAiE;IACjD,QAAgB;IAChC,kDAAkD;IAClC,EAAU;QARV,QAAG,GAAH,GAAG,CAAQ;QAEX,cAAS,GAAT,SAAS,CAAQ;QAEjB,WAAM,GAAN,MAAM,CAAe;QAErB,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,OAAE,GAAF,EAAE,CAAQ;IAC3B,CAAC;CACP;AAbD,wDAaC;AAED,oFAAoF;AACpF,+FAA+F;AAC/F,SAAgB,qBAAqB,CAAC,cAAwB,EAAE,QAAkB;IAC9E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,iBAAiB,CAAC;IAC/E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IACnD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,+FAA+F;AAC/F,SAAgB,WAAW,CAAC,IAAc;IACtC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CACjB,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CACrB,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CACjG,CAAC;AACN,CAAC","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /**\n * ONLY on a `pubsub` uses ref. True means \"this producer was attributed to EVERY cloudtasks\n * method of the contract, not to the methods it actually enqueues\".\n *\n * A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)\n * and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely — often after\n * the client has been stored in a DI binding — so WHICH methods it enqueues is not statically\n * recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording\n * the difference keeps a producer-side queue from being read as proof that queue is used.\n */\n methodsInferred?: boolean;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n}\n\n/**\n * The committed, per-contract view written to `architecture/dependencies.json` under `apiContracts`.\n *\n * The runtime graph is derived SOLELY from dependencies.json so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED there, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it — every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as the `apiContracts` key. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class — with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(implementsRefs: ApiRef[], usesRefs: ApiRef[]): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) || (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAyDH,8BAEC;AA2JD,sDAIC;AAOD,kCAKC;AAlLD;;;GAGG;AACH,+FAA+F;AAC/F,SAAgB,SAAS,CAAC,GAAW;IACjC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;AACnD,CAAC;AAuFD;;;;;;;;GAQG;AACH,MAAa,sBAAsB;IAGX;IAEA;IAEA;IAEA;IAEA;IAVpB;IACI,sDAAsD;IACtC,GAAW;IAC3B,wCAAwC;IACxB,SAAiB;IACjC,0EAA0E;IAC1D,MAAqB;IACrC,iEAAiE;IACjD,QAAgB;IAChC,kDAAkD;IAClC,EAAU;QARV,QAAG,GAAH,GAAG,CAAQ;QAEX,cAAS,GAAT,SAAS,CAAQ;QAEjB,WAAM,GAAN,MAAM,CAAe;QAErB,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,OAAE,GAAF,EAAE,CAAQ;IAC3B,CAAC;CACP;AAbD,wDAaC;AAED;;;;;;;;;GASG;AACH,MAAa,sBAAsB;IAGX;IAEA;IAEA;IAEA;IARpB;IACI,oDAAoD;IACpC,GAAW;IAC3B,uBAAuB;IACP,MAAc;IAC9B,iEAAiE;IACjD,QAAgB;IAChC,kDAAkD;IAClC,EAAU;QANV,QAAG,GAAH,GAAG,CAAQ;QAEX,WAAM,GAAN,MAAM,CAAQ;QAEd,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,OAAE,GAAF,EAAE,CAAQ;IAC3B,CAAC;CACP;AAXD,wDAWC;AAED;;;;;;;GAOG;AACH,MAAa,kBAAkB;IAGP;IAEA;IAEA;IANpB;IACI,+BAA+B;IACf,GAAW;IAC3B,0DAA0D;IAC1C,QAAgB;IAChC,+DAA+D;IAC/C,EAAU;QAJV,QAAG,GAAH,GAAG,CAAQ;QAEX,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,OAAE,GAAF,EAAE,CAAQ;IAC3B,CAAC;CACP;AATD,gDASC;AAED,oFAAoF;AACpF,+FAA+F;AAC/F,SAAgB,qBAAqB,CAAC,cAAwB,EAAE,QAAkB;IAC9E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,iBAAiB,CAAC;IAC/E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IACnD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,+FAA+F;AAC/F,SAAgB,WAAW,CAAC,IAAc;IACtC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CACjB,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CACrB,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CACjG,CAAC;AACN,CAAC","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /**\n * ONLY on a `pubsub` uses ref. True means \"this producer was attributed to EVERY cloudtasks\n * method of the contract, not to the methods it actually enqueues\".\n *\n * A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)\n * and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely — often after\n * the client has been stored in a DI binding — so WHICH methods it enqueues is not statically\n * recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording\n * the difference keeps a producer-side queue from being read as proof that queue is used.\n */\n methodsInferred?: boolean;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`.\n *\n * ONLY on a `cloudtasks` or `cron` method — those are the kinds actually delivered through a\n * named queue or schedule, and Terraform matches on this string. A synchronous `rpc` (or an\n * inbound `external`) endpoint has no queue and needs none; emitting a plausible-looking name for\n * one put every synchronous endpoint one naive `methods.map(m => m.queueName)` away from being\n * provisioned as a queue.\n */\n queueName?: string;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n}\n\n/**\n * The committed, per-contract view written to `architecture/dependencies.json` under `apiContracts`.\n *\n * The runtime graph is derived SOLELY from dependencies.json so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED there, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n /**\n * REQUIRED. Every routed contract carries `@ApiPath`, so every entry in this table must carry the\n * base path its methods hang off. Optional was worse than absent: a consumer joining\n * `basePath + path` for the ONE entry that lost it computed `/test` where the real route was\n * `/whatsapp/test`, and had no reason to suspect it — every other entry had the field. Generation\n * now FAILS instead of shipping an entry that computes a confidently wrong URL.\n */\n basePath: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as the `apiContracts` key. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/**\n * ONE decorator argument the scan saw but could not reduce to a string — `@ApiPath(SOME_CONST)`\n * where SOME_CONST is imported from another module, a computed expression, an enum member, ...\n *\n * Recorded rather than dropped. Before this existed, an unresolvable argument cost the contract its\n * basePath, or a method, or (when EVERY method's path was one) the whole class — with nothing\n * printed anywhere. Same-module constants now resolve, so what remains here is the genuinely\n * unresolvable, which the author can fix by inlining the literal or moving the constant in-module.\n */\nexport class NonLiteralDecoratorArg {\n constructor(\n /** The contract class the argument was written on. */\n public readonly api: string,\n /** `ApiPath` | `Endpoint` | `Queue`. */\n public readonly decorator: string,\n /** The method name for a member decorator, null for a class decorator. */\n public readonly method: string | null,\n /** The argument exactly as written, e.g. `WHATSAPP_API_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * ONE `@Endpoint(path, kind)` whose PATH argument was present but could not be reduced to a string.\n *\n * Split out of NonLiteralDecoratorArg (which stays a warning, covering @Queue and the rest) because\n * this one is FATAL. Upstream components need the URL: an http client builds its request as\n * `basePath + path`, so an unreadable path is missing ROUTING, not missing metadata — the same\n * reasoning that already makes basePath required. Skipping the method instead used to delete it, and\n * a class whose every path was a constant lost every method and vanished from `apiContracts` with\n * nothing printed anywhere.\n */\nexport class UnresolvedEndpointPath {\n constructor(\n /** The contract class the method is declared on. */\n public readonly api: string,\n /** The method name. */\n public readonly method: string,\n /** The path argument exactly as written, e.g. `PROCESS_PATH`. */\n public readonly argument: string,\n /** `path/to/file.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/**\n * A contract class that DECLARED `@Endpoint` methods and kept none of them.\n *\n * The backstop for the mechanism that hid an entire service: `buildApiContracts` skips a zero-method\n * class (correctly — a vendor seam has no routes), so a class gutted by unreadable decorator\n * arguments left through the same door as a legitimately routeless one. A class that declared\n * endpoints and produced none is never legitimate, so it is named instead.\n */\nexport class EmptiedApiContract {\n constructor(\n /** The contract class name. */\n public readonly api: string,\n /** How many `@Endpoint` decorators were written on it. */\n public readonly declared: number,\n /** `path/to/file.ts:LINE` of the class, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(implementsRefs: ApiRef[], usesRefs: ApiRef[]): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) || (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import type { EnhancedGraph } from '../graph-sorter';
|
|
31
31
|
import { ProjectInfo } from '../project-info';
|
|
32
|
-
import { ApiClassInfo, ApiContracts, NonLiteralDecoratorArg, ProjectApiRelations } from './api-relations';
|
|
32
|
+
import { ApiClassInfo, ApiContracts, EmptiedApiContract, NonLiteralDecoratorArg, ProjectApiRelations, UnresolvedEndpointPath } from './api-relations';
|
|
33
33
|
/**
|
|
34
34
|
* An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an
|
|
35
35
|
* abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken
|
|
@@ -80,6 +80,17 @@ export interface ApiScanResult {
|
|
|
80
80
|
* takes out every method of a class — the whole contract, so they must be surfaced.
|
|
81
81
|
*/
|
|
82
82
|
nonLiteralDecoratorArgs: NonLiteralDecoratorArg[];
|
|
83
|
+
/**
|
|
84
|
+
* The subset of the above that is FATAL: an `@Endpoint` path that could not be read. Every client
|
|
85
|
+
* builds its URL as `basePath + path`, so this is missing routing, not missing metadata —
|
|
86
|
+
* buildApiContracts throws on a non-empty list rather than shipping a contract without it.
|
|
87
|
+
*/
|
|
88
|
+
unresolvedEndpointPaths: UnresolvedEndpointPath[];
|
|
89
|
+
/**
|
|
90
|
+
* Contract classes that declared `@Endpoint` methods and kept none — the exact shape that used to
|
|
91
|
+
* slip out through buildApiContracts' zero-method skip, taking a whole service's queues with it.
|
|
92
|
+
*/
|
|
93
|
+
emptiedApiContracts: EmptiedApiContract[];
|
|
83
94
|
}
|
|
84
95
|
/** Statically scans every project for its api-lib implements/uses relationships. */
|
|
85
96
|
export declare class ApiUsageScanner {
|
|
@@ -151,19 +162,17 @@ export declare function scanAndAttachApiRelations(workspaceRoot: string, graph:
|
|
|
151
162
|
* would be an empty shell, and its identity is already carried by the `external` refs in
|
|
152
163
|
* apiRelations. Sorted by api name, methods left in declaration order, so the file is deterministic.
|
|
153
164
|
*
|
|
154
|
-
* THROWS
|
|
155
|
-
*
|
|
156
|
-
*
|
|
165
|
+
* THROWS on the three ways an entry can be wrong-but-green, checked root cause first:
|
|
166
|
+
* 1. an `@Endpoint` path the scan could not read (UnresolvedEndpointPathError) — the other half of
|
|
167
|
+
* the URL a consumer computes, and the cause of most emptied contracts;
|
|
168
|
+
* 2. a class that declared endpoints and kept none (EmptiedApiContractError), which would otherwise
|
|
169
|
+
* leave silently through the zero-method skip above;
|
|
170
|
+
* 3. a routed contract with no basePath (MissingBasePathError).
|
|
171
|
+
* All three are worse than an absent entry: a consumer joining `basePath + path` computes a
|
|
172
|
+
* confidently wrong URL with no signal that anything is off, because every other entry is complete.
|
|
173
|
+
* Each error aggregates EVERY offender, so a developer fixing five constants sees five in one run.
|
|
157
174
|
*/
|
|
158
175
|
export declare function buildApiContracts(scan: ApiScanResult): ApiContracts;
|
|
159
|
-
/**
|
|
160
|
-
* A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the
|
|
161
|
-
* entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.
|
|
162
|
-
*/
|
|
163
|
-
export declare class MissingBasePathError extends Error {
|
|
164
|
-
readonly contracts: readonly string[];
|
|
165
|
-
constructor(contracts: readonly string[]);
|
|
166
|
-
}
|
|
167
176
|
/**
|
|
168
177
|
* Loud, actionable report for decorator arguments the scan could not reduce to a string.
|
|
169
178
|
*
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* `recoverFromDeclaration`.
|
|
30
30
|
*/
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.
|
|
32
|
+
exports.ApiUsageScanner = exports.UnresolvedApiCall = void 0;
|
|
33
33
|
exports.scanAndAttachApiRelations = scanAndAttachApiRelations;
|
|
34
34
|
exports.buildApiContracts = buildApiContracts;
|
|
35
35
|
exports.describeNonLiteralDecoratorArgs = describeNonLiteralDecoratorArgs;
|
|
@@ -43,6 +43,7 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
43
43
|
const program_1 = require("../di-graph/program");
|
|
44
44
|
const bindings_1 = require("../di-graph/bindings");
|
|
45
45
|
const api_relations_1 = require("./api-relations");
|
|
46
|
+
const api_contract_errors_1 = require("./api-contract-errors");
|
|
46
47
|
const api_ast_1 = require("./api-ast");
|
|
47
48
|
const RPC_CLIENT_METHOD = 'createRpcClient';
|
|
48
49
|
const PUBSUB_CLIENT_METHOD = 'createPubSubClient';
|
|
@@ -257,6 +258,8 @@ class ApiUsageScanner {
|
|
|
257
258
|
scannedProjects: this.scannedProjects,
|
|
258
259
|
unresolvedApiCalls: this.unresolvedApiCalls,
|
|
259
260
|
nonLiteralDecoratorArgs: this.decoratorArgDiagnostics.all(),
|
|
261
|
+
unresolvedEndpointPaths: this.decoratorArgDiagnostics.unresolvedEndpointPaths(),
|
|
262
|
+
emptiedApiContracts: this.decoratorArgDiagnostics.emptiedContracts(),
|
|
260
263
|
};
|
|
261
264
|
}
|
|
262
265
|
scanProject(info) {
|
|
@@ -416,12 +419,24 @@ function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalA
|
|
|
416
419
|
* would be an empty shell, and its identity is already carried by the `external` refs in
|
|
417
420
|
* apiRelations. Sorted by api name, methods left in declaration order, so the file is deterministic.
|
|
418
421
|
*
|
|
419
|
-
* THROWS
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
+
* THROWS on the three ways an entry can be wrong-but-green, checked root cause first:
|
|
423
|
+
* 1. an `@Endpoint` path the scan could not read (UnresolvedEndpointPathError) — the other half of
|
|
424
|
+
* the URL a consumer computes, and the cause of most emptied contracts;
|
|
425
|
+
* 2. a class that declared endpoints and kept none (EmptiedApiContractError), which would otherwise
|
|
426
|
+
* leave silently through the zero-method skip above;
|
|
427
|
+
* 3. a routed contract with no basePath (MissingBasePathError).
|
|
428
|
+
* All three are worse than an absent entry: a consumer joining `basePath + path` computes a
|
|
429
|
+
* confidently wrong URL with no signal that anything is off, because every other entry is complete.
|
|
430
|
+
* Each error aggregates EVERY offender, so a developer fixing five constants sees five in one run.
|
|
422
431
|
*/
|
|
423
432
|
// webpieces-disable no-function-outside-class -- module entry point, mirrors scanAndAttachApiRelations
|
|
424
433
|
function buildApiContracts(scan) {
|
|
434
|
+
// Root cause before symptom: an unreadable path is what empties a contract, so naming the paths
|
|
435
|
+
// is what the author can actually act on.
|
|
436
|
+
if (scan.unresolvedEndpointPaths.length > 0)
|
|
437
|
+
throw new api_contract_errors_1.UnresolvedEndpointPathError(scan.unresolvedEndpointPaths);
|
|
438
|
+
if (scan.emptiedApiContracts.length > 0)
|
|
439
|
+
throw new api_contract_errors_1.EmptiedApiContractError(scan.emptiedApiContracts);
|
|
425
440
|
const contracts = {};
|
|
426
441
|
const missing = [];
|
|
427
442
|
for (const api of [...scan.apiIndex.keys()].sort()) {
|
|
@@ -441,26 +456,9 @@ function buildApiContracts(scan) {
|
|
|
441
456
|
contracts[api] = contract;
|
|
442
457
|
}
|
|
443
458
|
if (missing.length > 0)
|
|
444
|
-
throw new MissingBasePathError(missing);
|
|
459
|
+
throw new api_contract_errors_1.MissingBasePathError(missing);
|
|
445
460
|
return contracts;
|
|
446
461
|
}
|
|
447
|
-
/**
|
|
448
|
-
* A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the
|
|
449
|
-
* entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.
|
|
450
|
-
*/
|
|
451
|
-
class MissingBasePathError extends Error {
|
|
452
|
-
contracts;
|
|
453
|
-
constructor(contracts) {
|
|
454
|
-
super(`${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\n` +
|
|
455
|
-
contracts.map((c) => ` • ${c}`).join('\n') +
|
|
456
|
-
`\n basePath is REQUIRED in apiContracts — an entry without it makes every consumer\n` +
|
|
457
|
-
` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\n` +
|
|
458
|
-
` or move the constant into the same module as the contract class.`);
|
|
459
|
-
this.contracts = contracts;
|
|
460
|
-
this.name = 'MissingBasePathError';
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
exports.MissingBasePathError = MissingBasePathError;
|
|
464
462
|
/**
|
|
465
463
|
* Loud, actionable report for decorator arguments the scan could not reduce to a string.
|
|
466
464
|
*
|
|
@@ -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 * The config argument (`new ClientConfig('helper-fsdb')`) names WHICH service the\n * client talks to and is kept as `ApiRef.targetService` — see targetServiceOf.\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 { matchesAnyGlob } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { findProjectTsconfig } from '../di-graph/program';\nimport { resolveClassDeclaration } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiContract,\n ApiContracts,\n ApiRef,\n ApiRelation,\n EndpointKind,\n NonLiteralDecoratorArg,\n ProjectApiRelations,\n apiRefKey,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\nimport {\n DecoratorArgDiagnostics,\n apiClassInfoFrom,\n apiClassInfoFromNode,\n calleeMethodName,\n collectTsFiles,\n constructorParamsOf,\n externalApiInfoFrom,\n implementedTypeNames,\n isAbstractClass,\n isTestFile,\n targetServiceOf,\n typeReferenceName,\n} from './api-ast';\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 * Decorator arguments that were present but could not be reduced to a string (a cross-module\n * constant, a computed expression). Each one costs the graph a basePath, a method, or — when it\n * takes out every method of a class — the whole contract, so they must be surfaced.\n */\n nonLiteralDecoratorArgs: NonLiteralDecoratorArg[];\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 /** Globs of project roots holding vendor contracts — see ExternalApiIndex. */\n private readonly externalApiPaths: readonly string[],\n /** Sink for decorator arguments this parser-only pass cannot reduce to a string. */\n private readonly diagnostics: DecoratorArgDiagnostics,\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 const external = matchesAnyGlob(info.root, this.externalApiPaths);\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, external);\n }\n }\n\n private indexNode(node: ts.Node, project: string, external: boolean): void {\n const info = external\n ? externalApiInfoFrom(node, project)\n : apiClassInfoFromNode(node, project, this.diagnostics);\n if (info) {\n this.owners.add(project);\n this.byName.set(info.api, info);\n }\n ts.forEachChild(node, (child: ts.Node) => this.indexNode(child, project, external));\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(apiRefKey(ref), ref);\n }\n\n /**\n * Keyed by api + targetService: one project legitimately binds the SAME contract against two\n * different services (a WarmupApi client per data server), and those are two relations, not one.\n */\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(apiRefKey(ref), 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 readonly decoratorArgDiagnostics: DecoratorArgDiagnostics;\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 /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */\n private readonly externalApiPaths: readonly string[] = [],\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n this.decoratorArgDiagnostics = new DecoratorArgDiagnostics(workspaceRoot);\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(\n this.workspaceRoot,\n this.projectInfos,\n this.externalApiPaths,\n this.decoratorArgDiagnostics,\n ).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 nonLiteralDecoratorArgs: this.decoratorArgDiagnostics.all(),\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 // In-repo contract classes are indexed by the source pre-pass, so only calls matter for them.\n if (ts.isCallExpression(node)) this.recordCall(node, checker, project, acc);\n // A VENDOR contract has no client-factory call site to key off — it arrives by injection —\n // so classes have to be inspected too.\n if (ts.isClassDeclaration(node)) this.recordExternalUses(node, acc);\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n /**\n * Record a `uses` for every vendor contract this class receives by CONSTRUCTOR INJECTION —\n * `constructor(@inject(GMAIL_TYPES.GmailApi) private readonly gmail: GmailApi)`.\n *\n * The parameter TYPE is the signal, not the token: a token is an opaque Symbol whose name we\n * would have to guess at, while the type is written right there and is what the class actually\n * calls. Matching happens by name against the external index, so an import that resolves to a\n * built `.d.ts` works exactly as well as one resolving to source.\n *\n * A class that IMPLEMENTS the contract is skipped — that is the vendor adapter (`GmailClient`)\n * or a test double (`InMemoryFirestore`, `MockTts`), which IS the seam rather than a caller of\n * it. Counting those would draw an edge from every service embedding a fake to a vendor it never\n * actually reaches.\n */\n private recordExternalUses(cls: ts.ClassDeclaration, acc: RelationAccumulator): void {\n const implemented = implementedTypeNames(cls);\n for (const param of constructorParamsOf(cls)) {\n const typeName = typeReferenceName(param.type);\n if (typeName === null || implemented.has(typeName)) continue;\n const info = this.sourceIndex.lookup(typeName);\n if (info === null || info.type !== 'external') continue;\n acc.addUses(info.owner, { api: info.api, type: 'external' });\n }\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) return;\n // Argument 2 names WHICH service this client talks to. Keeping it is what lets the\n // runtime graph draw ONE edge instead of one per implementer of the contract.\n const targetService = targetServiceOf(call);\n const ref: ApiRef = { api: info.api, type: info.type };\n if (targetService !== null) ref.targetService = targetService;\n acc.addUses(info.owner, ref);\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 /**\n * {api, owner, type, methods} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE,\n * else null. Only the OWNER differs from the index pre-pass — here it comes from the file's\n * location rather than from the project being walked — so the contract test itself is delegated\n * to apiClassInfoFrom, keeping one definition of \"this is a contract\".\n */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n return apiClassInfoFrom(cls, owner);\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 externalApiPaths: readonly string[] = [],\n): ApiScanResult {\n const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths).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 * The committed `apiContracts` table for architecture/dependencies.json, from a completed scan.\n *\n * Only contracts with ≥1 endpoint are emitted: a vendor seam has no routes, so a table entry for it\n * would be an empty shell, and its identity is already carried by the `external` refs in\n * apiRelations. Sorted by api name, methods left in declaration order, so the file is deterministic.\n *\n * THROWS when a routed contract has no basePath. `basePath` is required on ApiContract, and an entry\n * missing it is worse than an absent entry: a consumer joining `basePath + path` computes a\n * confidently wrong URL with no signal that anything is off, because every other entry has the field.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors scanAndAttachApiRelations\nexport function buildApiContracts(scan: ApiScanResult): ApiContracts {\n const contracts: ApiContracts = {};\n const missing: string[] = [];\n for (const api of [...scan.apiIndex.keys()].sort()) {\n const info = scan.apiIndex.get(api)!;\n if (info.methods.length === 0) continue;\n if (info.basePath === undefined) {\n missing.push(`${api} (owner ${info.owner})`);\n continue;\n }\n const contract: ApiContract = {\n owner: info.owner,\n apiKind: info.type,\n basePath: info.basePath,\n methods: info.methods,\n };\n contracts[api] = contract;\n }\n if (missing.length > 0) throw new MissingBasePathError(missing);\n return contracts;\n}\n\n/**\n * A routed contract whose `@ApiPath` argument the scan could not read. Fatal on purpose: shipping the\n * entry without its basePath is what made `/whatsapp/test` render as `/test` in a downstream runbook.\n */\nexport class MissingBasePathError extends Error {\n constructor(public readonly contracts: readonly string[]) {\n super(\n `${contracts.length} API contract(s) have @Endpoint methods but no readable @ApiPath basePath:\\n` +\n contracts.map((c: string) => ` • ${c}`).join('\\n') +\n `\\n basePath is REQUIRED in apiContracts — an entry without it makes every consumer\\n` +\n ` compute basePath + path as just path, silently. Inline the @ApiPath string literal,\\n` +\n ` or move the constant into the same module as the contract class.`,\n );\n this.name = 'MissingBasePathError';\n }\n}\n\n/**\n * Loud, actionable report for decorator arguments the scan could not reduce to a string.\n *\n * Same-module constants resolve, so anything reaching here is genuinely out of reach of a\n * parser-only pass — and every one of them silently shrinks the graph. Empty string when there is\n * nothing to say, so callers can test it without special-casing.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeNonLiteralDecoratorArgs(args: readonly NonLiteralDecoratorArg[]): string {\n if (args.length === 0) return '';\n const lines = [\n `⚠️ ${args.length} decorator argument(s) are not string literals and could not be resolved.`,\n ` Each one drops data from the graph: a missing basePath, a missing method, or a whole contract:`,\n ];\n for (const arg of args) {\n const where = arg.method === null ? arg.api : `${arg.api}.${arg.method}`;\n lines.push(` • @${arg.decorator}(${arg.argument}) on ${where} at ${arg.at}`);\n }\n lines.push(\n ` A constant declared in the SAME module resolves. One imported from another module does not —`,\n ` this scan is parser-only by design (module resolution can land on a decorator-erased .d.ts).`,\n ` Fix by inlining the string literal, or by moving the constant into the contract's own module.`,\n );\n return lines.join('\\n');\n}\n\n/**\n * Every contract method whose declared @Endpoint kind its api kind cannot deliver — an rpc method on\n * a @PubSub contract (nothing calls a queue synchronously), or a cloudtasks/cron method on an @Rpc\n * contract (naming a queue or schedule nothing could deliver to). Mirrors core-util's\n * ENDPOINT_KINDS_BY_API_KIND at BUILD time, where it can name the file instead of throwing at wiring.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeMismatchedEndpointKinds(contracts: ApiContracts): string[] {\n const allowedByKind: Record<string, readonly EndpointKind[]> = {\n rpc: ['rpc', 'external'],\n pubsub: ['cloudtasks', 'cron', 'external'],\n };\n const problems: string[] = [];\n for (const api of Object.keys(contracts)) {\n const contract = contracts[api];\n const allowed = allowedByKind[contract.apiKind];\n if (allowed === undefined) continue;\n for (const method of contract.methods) {\n if (allowed.includes(method.kind)) continue;\n problems.push(\n `${api}.${method.name} declares @Endpoint('${method.path}', '${method.kind}') but ${api} is ` +\n `@${contract.apiKind === 'pubsub' ? 'PubSub' : 'Rpc'} — allowed kinds are ${allowed.join(' | ')}.`,\n );\n }\n }\n return problems;\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"]}
|
|
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 * The config argument (`new ClientConfig('helper-fsdb')`) names WHICH service the\n * client talks to and is kept as `ApiRef.targetService` — see targetServiceOf.\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 { matchesAnyGlob } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { findProjectTsconfig } from '../di-graph/program';\nimport { resolveClassDeclaration } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiContract,\n ApiContracts,\n ApiRef,\n ApiRelation,\n EmptiedApiContract,\n EndpointKind,\n NonLiteralDecoratorArg,\n ProjectApiRelations,\n UnresolvedEndpointPath,\n apiRefKey,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\nimport {\n EmptiedApiContractError,\n MissingBasePathError,\n UnresolvedEndpointPathError,\n} from './api-contract-errors';\nimport {\n DecoratorArgDiagnostics,\n apiClassInfoFrom,\n apiClassInfoFromNode,\n calleeMethodName,\n collectTsFiles,\n constructorParamsOf,\n externalApiInfoFrom,\n implementedTypeNames,\n isAbstractClass,\n isTestFile,\n targetServiceOf,\n typeReferenceName,\n} from './api-ast';\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 * Decorator arguments that were present but could not be reduced to a string (a cross-module\n * constant, a computed expression). Each one costs the graph a basePath, a method, or — when it\n * takes out every method of a class — the whole contract, so they must be surfaced.\n */\n nonLiteralDecoratorArgs: NonLiteralDecoratorArg[];\n /**\n * The subset of the above that is FATAL: an `@Endpoint` path that could not be read. Every client\n * builds its URL as `basePath + path`, so this is missing routing, not missing metadata —\n * buildApiContracts throws on a non-empty list rather than shipping a contract without it.\n */\n unresolvedEndpointPaths: UnresolvedEndpointPath[];\n /**\n * Contract classes that declared `@Endpoint` methods and kept none — the exact shape that used to\n * slip out through buildApiContracts' zero-method skip, taking a whole service's queues with it.\n */\n emptiedApiContracts: EmptiedApiContract[];\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 /** Globs of project roots holding vendor contracts — see ExternalApiIndex. */\n private readonly externalApiPaths: readonly string[],\n /** Sink for decorator arguments this parser-only pass cannot reduce to a string. */\n private readonly diagnostics: DecoratorArgDiagnostics,\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 const external = matchesAnyGlob(info.root, this.externalApiPaths);\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, external);\n }\n }\n\n private indexNode(node: ts.Node, project: string, external: boolean): void {\n const info = external\n ? externalApiInfoFrom(node, project)\n : apiClassInfoFromNode(node, project, this.diagnostics);\n if (info) {\n this.owners.add(project);\n this.byName.set(info.api, info);\n }\n ts.forEachChild(node, (child: ts.Node) => this.indexNode(child, project, external));\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(apiRefKey(ref), ref);\n }\n\n /**\n * Keyed by api + targetService: one project legitimately binds the SAME contract against two\n * different services (a WarmupApi client per data server), and those are two relations, not one.\n */\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(apiRefKey(ref), 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 readonly decoratorArgDiagnostics: DecoratorArgDiagnostics;\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 /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */\n private readonly externalApiPaths: readonly string[] = [],\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n this.decoratorArgDiagnostics = new DecoratorArgDiagnostics(workspaceRoot);\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(\n this.workspaceRoot,\n this.projectInfos,\n this.externalApiPaths,\n this.decoratorArgDiagnostics,\n ).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 nonLiteralDecoratorArgs: this.decoratorArgDiagnostics.all(),\n unresolvedEndpointPaths: this.decoratorArgDiagnostics.unresolvedEndpointPaths(),\n emptiedApiContracts: this.decoratorArgDiagnostics.emptiedContracts(),\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 // In-repo contract classes are indexed by the source pre-pass, so only calls matter for them.\n if (ts.isCallExpression(node)) this.recordCall(node, checker, project, acc);\n // A VENDOR contract has no client-factory call site to key off — it arrives by injection —\n // so classes have to be inspected too.\n if (ts.isClassDeclaration(node)) this.recordExternalUses(node, acc);\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n /**\n * Record a `uses` for every vendor contract this class receives by CONSTRUCTOR INJECTION —\n * `constructor(@inject(GMAIL_TYPES.GmailApi) private readonly gmail: GmailApi)`.\n *\n * The parameter TYPE is the signal, not the token: a token is an opaque Symbol whose name we\n * would have to guess at, while the type is written right there and is what the class actually\n * calls. Matching happens by name against the external index, so an import that resolves to a\n * built `.d.ts` works exactly as well as one resolving to source.\n *\n * A class that IMPLEMENTS the contract is skipped — that is the vendor adapter (`GmailClient`)\n * or a test double (`InMemoryFirestore`, `MockTts`), which IS the seam rather than a caller of\n * it. Counting those would draw an edge from every service embedding a fake to a vendor it never\n * actually reaches.\n */\n private recordExternalUses(cls: ts.ClassDeclaration, acc: RelationAccumulator): void {\n const implemented = implementedTypeNames(cls);\n for (const param of constructorParamsOf(cls)) {\n const typeName = typeReferenceName(param.type);\n if (typeName === null || implemented.has(typeName)) continue;\n const info = this.sourceIndex.lookup(typeName);\n if (info === null || info.type !== 'external') continue;\n acc.addUses(info.owner, { api: info.api, type: 'external' });\n }\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) return;\n // Argument 2 names WHICH service this client talks to. Keeping it is what lets the\n // runtime graph draw ONE edge instead of one per implementer of the contract.\n const targetService = targetServiceOf(call);\n const ref: ApiRef = { api: info.api, type: info.type };\n if (targetService !== null) ref.targetService = targetService;\n acc.addUses(info.owner, ref);\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 /**\n * {api, owner, type, methods} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE,\n * else null. Only the OWNER differs from the index pre-pass — here it comes from the file's\n * location rather than from the project being walked — so the contract test itself is delegated\n * to apiClassInfoFrom, keeping one definition of \"this is a contract\".\n */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n return apiClassInfoFrom(cls, owner);\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 externalApiPaths: readonly string[] = [],\n): ApiScanResult {\n const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths).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 * The committed `apiContracts` table for architecture/dependencies.json, from a completed scan.\n *\n * Only contracts with ≥1 endpoint are emitted: a vendor seam has no routes, so a table entry for it\n * would be an empty shell, and its identity is already carried by the `external` refs in\n * apiRelations. Sorted by api name, methods left in declaration order, so the file is deterministic.\n *\n * THROWS on the three ways an entry can be wrong-but-green, checked root cause first:\n * 1. an `@Endpoint` path the scan could not read (UnresolvedEndpointPathError) — the other half of\n * the URL a consumer computes, and the cause of most emptied contracts;\n * 2. a class that declared endpoints and kept none (EmptiedApiContractError), which would otherwise\n * leave silently through the zero-method skip above;\n * 3. a routed contract with no basePath (MissingBasePathError).\n * All three are worse than an absent entry: a consumer joining `basePath + path` computes a\n * confidently wrong URL with no signal that anything is off, because every other entry is complete.\n * Each error aggregates EVERY offender, so a developer fixing five constants sees five in one run.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors scanAndAttachApiRelations\nexport function buildApiContracts(scan: ApiScanResult): ApiContracts {\n // Root cause before symptom: an unreadable path is what empties a contract, so naming the paths\n // is what the author can actually act on.\n if (scan.unresolvedEndpointPaths.length > 0) throw new UnresolvedEndpointPathError(scan.unresolvedEndpointPaths);\n if (scan.emptiedApiContracts.length > 0) throw new EmptiedApiContractError(scan.emptiedApiContracts);\n const contracts: ApiContracts = {};\n const missing: string[] = [];\n for (const api of [...scan.apiIndex.keys()].sort()) {\n const info = scan.apiIndex.get(api)!;\n if (info.methods.length === 0) continue;\n if (info.basePath === undefined) {\n missing.push(`${api} (owner ${info.owner})`);\n continue;\n }\n const contract: ApiContract = {\n owner: info.owner,\n apiKind: info.type,\n basePath: info.basePath,\n methods: info.methods,\n };\n contracts[api] = contract;\n }\n if (missing.length > 0) throw new MissingBasePathError(missing);\n return contracts;\n}\n\n/**\n * Loud, actionable report for decorator arguments the scan could not reduce to a string.\n *\n * Same-module constants resolve, so anything reaching here is genuinely out of reach of a\n * parser-only pass — and every one of them silently shrinks the graph. Empty string when there is\n * nothing to say, so callers can test it without special-casing.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeNonLiteralDecoratorArgs(args: readonly NonLiteralDecoratorArg[]): string {\n if (args.length === 0) return '';\n const lines = [\n `⚠️ ${args.length} decorator argument(s) are not string literals and could not be resolved.`,\n ` Each one drops data from the graph: a missing basePath, a missing method, or a whole contract:`,\n ];\n for (const arg of args) {\n const where = arg.method === null ? arg.api : `${arg.api}.${arg.method}`;\n lines.push(` • @${arg.decorator}(${arg.argument}) on ${where} at ${arg.at}`);\n }\n lines.push(\n ` A constant declared in the SAME module resolves. One imported from another module does not —`,\n ` this scan is parser-only by design (module resolution can land on a decorator-erased .d.ts).`,\n ` Fix by inlining the string literal, or by moving the constant into the contract's own module.`,\n );\n return lines.join('\\n');\n}\n\n/**\n * Every contract method whose declared @Endpoint kind its api kind cannot deliver — an rpc method on\n * a @PubSub contract (nothing calls a queue synchronously), or a cloudtasks/cron method on an @Rpc\n * contract (naming a queue or schedule nothing could deliver to). Mirrors core-util's\n * ENDPOINT_KINDS_BY_API_KIND at BUILD time, where it can name the file instead of throwing at wiring.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeMismatchedEndpointKinds(contracts: ApiContracts): string[] {\n const allowedByKind: Record<string, readonly EndpointKind[]> = {\n rpc: ['rpc', 'external'],\n pubsub: ['cloudtasks', 'cron', 'external'],\n };\n const problems: string[] = [];\n for (const api of Object.keys(contracts)) {\n const contract = contracts[api];\n const allowed = allowedByKind[contract.apiKind];\n if (allowed === undefined) continue;\n for (const method of contract.methods) {\n if (allowed.includes(method.kind)) continue;\n problems.push(\n `${api}.${method.name} declares @Endpoint('${method.path}', '${method.kind}') but ${api} is ` +\n `@${contract.apiKind === 'pubsub' ? 'PubSub' : 'Rpc'} — allowed kinds are ${allowed.join(' | ')}.`,\n );\n }\n }\n return problems;\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"]}
|