@webpieces/nx-webpieces-rules 0.4.833 → 0.4.834

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.833",
3
+ "version": "0.4.834",
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",
@@ -18,13 +18,13 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@webpieces/ai-hook-rules": "0.4.833",
22
- "@webpieces/api-doc-model": "0.4.833",
23
- "@webpieces/code-rules": "0.4.833",
24
- "@webpieces/core-util": "0.4.833",
25
- "@webpieces/eslint-rules": "0.4.833",
26
- "@webpieces/pr-gate": "0.4.833",
27
- "@webpieces/rules-config": "0.4.833",
21
+ "@webpieces/ai-hook-rules": "0.4.834",
22
+ "@webpieces/api-doc-model": "0.4.834",
23
+ "@webpieces/code-rules": "0.4.834",
24
+ "@webpieces/core-util": "0.4.834",
25
+ "@webpieces/eslint-rules": "0.4.834",
26
+ "@webpieces/pr-gate": "0.4.834",
27
+ "@webpieces/rules-config": "0.4.834",
28
28
  "madge": "8.0.0"
29
29
  },
30
30
  "peerDependencies": {
@@ -8,9 +8,10 @@
8
8
  * - a project tagged `role:api-lib` MUST export a contract or ONLY wire types — else the tag is a lie.
9
9
  * A contract is any of: an `@ApiPath`/`@Rpc`/`@PubSub` class (from the scan), an IPC contract
10
10
  * (`@WpInternal` / `@WpIpcEndpoint`), or an in-process abstract `…Api` behind a DI token. A DTO-only
11
- * library exports interfaces, type aliases, enums and data classes (no methods) and nothing else;
12
- * - a project tagged `role:api-client` MUST export a contract (its abstract `XxxApi`) — the bundled
13
- * `XxxClient` beside it is the point of the role, so it is not judged as a wire type.
11
+ * library exports interfaces, type aliases, enums, data classes (no methods), and data-only constants;
12
+ * - a project tagged `role:api-client` MUST export a contract: either an abstract `XxxApi`, or an
13
+ * exported `XxxApi` interface implemented by a client registered with `@provideSingletonDefaultForApi`.
14
+ * The bundled client beside it is the point of the role, so it is not judged as a wire type.
14
15
  *
15
16
  * "Exports an @ApiPath contract" is answered by the same source scan that owns apiRelations
16
17
  * (scan.apiLibProjects), so the tag can never drift from the code. The IPC / in-process / DTO-only
@@ -9,9 +9,10 @@
9
9
  * - a project tagged `role:api-lib` MUST export a contract or ONLY wire types — else the tag is a lie.
10
10
  * A contract is any of: an `@ApiPath`/`@Rpc`/`@PubSub` class (from the scan), an IPC contract
11
11
  * (`@WpInternal` / `@WpIpcEndpoint`), or an in-process abstract `…Api` behind a DI token. A DTO-only
12
- * library exports interfaces, type aliases, enums and data classes (no methods) and nothing else;
13
- * - a project tagged `role:api-client` MUST export a contract (its abstract `XxxApi`) — the bundled
14
- * `XxxClient` beside it is the point of the role, so it is not judged as a wire type.
12
+ * library exports interfaces, type aliases, enums, data classes (no methods), and data-only constants;
13
+ * - a project tagged `role:api-client` MUST export a contract: either an abstract `XxxApi`, or an
14
+ * exported `XxxApi` interface implemented by a client registered with `@provideSingletonDefaultForApi`.
15
+ * The bundled client beside it is the point of the role, so it is not judged as a wire type.
15
16
  *
16
17
  * "Exports an @ApiPath contract" is answered by the same source scan that owns apiRelations
17
18
  * (scan.apiLibProjects), so the tag can never drift from the code. The IPC / in-process / DTO-only
@@ -98,6 +99,8 @@ class ExportShapeAccumulator {
98
99
  contract = false;
99
100
  wire = false;
100
101
  offenders = [];
102
+ apiInterfaces = new Set();
103
+ registeredApiImplementations = new Set();
101
104
  constructor(projectDir) {
102
105
  this.projectDir = projectDir;
103
106
  }
@@ -105,7 +108,12 @@ class ExportShapeAccumulator {
105
108
  if (!this.isExported(statement))
106
109
  return;
107
110
  const where = path.relative(this.projectDir, file);
108
- if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {
111
+ if (ts.isInterfaceDeclaration(statement)) {
112
+ this.wire = true;
113
+ if (statement.name.text.endsWith('Api'))
114
+ this.apiInterfaces.add(statement.name.text);
115
+ }
116
+ else if (ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {
109
117
  this.wire = true;
110
118
  }
111
119
  else if (ts.isClassDeclaration(statement)) {
@@ -113,7 +121,7 @@ class ExportShapeAccumulator {
113
121
  }
114
122
  else if (ts.isVariableStatement(statement)) {
115
123
  for (const decl of statement.declarationList.declarations) {
116
- if (this.isDiToken(decl.initializer))
124
+ if (this.isDiToken(decl.initializer) || this.isDataConstant(decl.initializer))
117
125
  continue;
118
126
  this.offenders.push(`const ${decl.name.getText()} (${where})`);
119
127
  }
@@ -123,10 +131,22 @@ class ExportShapeAccumulator {
123
131
  }
124
132
  }
125
133
  shape() {
126
- return new ApiLibExportShape(this.contract, this.wire, [...this.offenders].sort());
134
+ const registeredInterface = [...this.apiInterfaces].some((name) => this.registeredApiImplementations.has(name));
135
+ return new ApiLibExportShape(this.contract || registeredInterface, this.wire, [...this.offenders].sort());
127
136
  }
128
137
  addClass(cls, where) {
129
138
  const name = cls.name?.text ?? '<default>';
139
+ if (this.hasDecorator(cls, 'provideSingletonDefaultForApi')) {
140
+ for (const heritage of cls.heritageClauses ?? []) {
141
+ if (heritage.token !== ts.SyntaxKind.ImplementsKeyword)
142
+ continue;
143
+ for (const type of heritage.types) {
144
+ const implemented = type.expression.getText().split('.').pop();
145
+ if (implemented !== undefined)
146
+ this.registeredApiImplementations.add(implemented);
147
+ }
148
+ }
149
+ }
130
150
  const isAbstract = (ts.getModifiers(cls) ?? []).some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
131
151
  if (isAbstract && (name.endsWith('Api') || this.carriesIpc(cls))) {
132
152
  this.contract = true;
@@ -141,13 +161,74 @@ class ExportShapeAccumulator {
141
161
  }
142
162
  /** `@WpInternal(...)` on the class or `@WpIpcEndpoint(...)` on any member. */
143
163
  carriesIpc(cls) {
144
- const named = (node) => (ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []).some((d) => {
145
- const expr = ts.isCallExpression(d.expression) ? d.expression.expression : d.expression;
146
- const text = ts.isPropertyAccessExpression(expr) ? expr.name.text : expr.getText();
147
- return IPC_DECORATORS.includes(text);
148
- });
164
+ const named = (node) => IPC_DECORATORS.some((name) => this.hasDecorator(node, name));
149
165
  return named(cls) || cls.members.some((member) => named(member));
150
166
  }
167
+ hasDecorator(node, expected) {
168
+ return (ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []).some((decorator) => {
169
+ const expr = ts.isCallExpression(decorator.expression)
170
+ ? decorator.expression.expression
171
+ : decorator.expression;
172
+ const name = ts.isPropertyAccessExpression(expr) ? expr.name.text : expr.getText();
173
+ return name === expected;
174
+ });
175
+ }
176
+ /** Literal/structural protocol data is contract support, not an executable implementation. */
177
+ isDataConstant(init) {
178
+ if (init === undefined)
179
+ return false;
180
+ const expr = this.unwrap(init);
181
+ if (ts.isStringLiteralLike(expr) ||
182
+ ts.isNumericLiteral(expr) ||
183
+ expr.kind === ts.SyntaxKind.TrueKeyword ||
184
+ expr.kind === ts.SyntaxKind.FalseKeyword ||
185
+ expr.kind === ts.SyntaxKind.NullKeyword ||
186
+ ts.isIdentifier(expr) ||
187
+ ts.isPropertyAccessExpression(expr)) {
188
+ return true;
189
+ }
190
+ if (ts.isPrefixUnaryExpression(expr))
191
+ return this.isDataConstant(expr.operand);
192
+ if (ts.isBinaryExpression(expr)) {
193
+ return this.isDataConstant(expr.left) && this.isDataConstant(expr.right);
194
+ }
195
+ if (ts.isConditionalExpression(expr)) {
196
+ return (this.isDataConstant(expr.condition) &&
197
+ this.isDataConstant(expr.whenTrue) &&
198
+ this.isDataConstant(expr.whenFalse));
199
+ }
200
+ if (ts.isTemplateExpression(expr)) {
201
+ return expr.templateSpans.every((span) => this.isDataConstant(span.expression));
202
+ }
203
+ if (ts.isArrayLiteralExpression(expr)) {
204
+ return expr.elements.every((element) => ts.isOmittedExpression(element) ||
205
+ (ts.isSpreadElement(element)
206
+ ? this.isDataConstant(element.expression)
207
+ : this.isDataConstant(element)));
208
+ }
209
+ if (ts.isObjectLiteralExpression(expr)) {
210
+ return expr.properties.every((prop) => {
211
+ if (ts.isPropertyAssignment(prop))
212
+ return this.isDataConstant(prop.initializer);
213
+ if (ts.isShorthandPropertyAssignment(prop))
214
+ return true;
215
+ if (ts.isSpreadAssignment(prop))
216
+ return this.isDataConstant(prop.expression);
217
+ return false;
218
+ });
219
+ }
220
+ return false;
221
+ }
222
+ unwrap(expr) {
223
+ if (ts.isAsExpression(expr) ||
224
+ ts.isTypeAssertionExpression(expr) ||
225
+ ts.isSatisfiesExpression(expr) ||
226
+ ts.isParenthesizedExpression(expr) ||
227
+ ts.isNonNullExpression(expr)) {
228
+ return this.unwrap(expr.expression);
229
+ }
230
+ return expr;
231
+ }
151
232
  /** `Symbol('x')`, `Symbol.for('x')`, or an object literal of them — the token an abstract `…Api` binds behind. */
152
233
  isDiToken(init) {
153
234
  if (init === undefined)
@@ -158,7 +239,8 @@ class ExportShapeAccumulator {
158
239
  return callee === 'Symbol' || callee === 'Symbol.for';
159
240
  }
160
241
  if (ts.isObjectLiteralExpression(expr)) {
161
- return expr.properties.length > 0 && expr.properties.every((prop) => ts.isPropertyAssignment(prop) && this.isDiToken(prop.initializer));
242
+ return (expr.properties.length > 0 &&
243
+ expr.properties.every((prop) => ts.isPropertyAssignment(prop) && this.isDiToken(prop.initializer)));
162
244
  }
163
245
  return false;
164
246
  }
@@ -202,9 +284,10 @@ function describeApiLibTagViolation(violation) {
202
284
  }
203
285
  if (violation.role === API_CLIENT_ROLE) {
204
286
  return (` ❌ '${violation.project}' is tagged 'role:api-client' but exports NO contract (no abstract ` +
205
- `…Api class, no @WpInternal/@WpIpcEndpoint contract).\n` +
206
- ` An api-client is a contract PLUS its default implementation: export the abstract XxxApi it ` +
207
- `implements, or retag it (e.g. "role:lib").`);
287
+ `…Api class, no @WpInternal/@WpIpcEndpoint contract, and no exported …Api interface implemented ` +
288
+ `by an @provideSingletonDefaultForApi client).\n` +
289
+ ` An api-client is a contract PLUS its default implementation: export the XxxApi the registered ` +
290
+ `client implements, or retag it (e.g. "role:lib").`);
208
291
  }
209
292
  const offenders = violation.offenders.length === 0
210
293
  ? ''
@@ -1 +1 @@
1
- {"version":3,"file":"api-lib-tag-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-lib-tag-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API-lib tag validator (two-way)\n *\n * Keeps the two api roles and the CODE in sync, both directions (#1064, D1 + D3):\n * - a project that exports an `@ApiPath`/`@Rpc`/`@PubSub` contract (or a vendor contract under\n * `externalApiPaths`) MUST be tagged `role:api-lib` or `role:api-client` — else the arch graph, the\n * edge line-styles, and validate-api-relations can't treat it as one;\n * - a project tagged `role:api-lib` MUST export a contract or ONLY wire types — else the tag is a lie.\n * A contract is any of: an `@ApiPath`/`@Rpc`/`@PubSub` class (from the scan), an IPC contract\n * (`@WpInternal` / `@WpIpcEndpoint`), or an in-process abstract `…Api` behind a DI token. A DTO-only\n * library exports interfaces, type aliases, enums and data classes (no methods) and nothing else;\n * - a project tagged `role:api-client` MUST export a contract (its abstract `XxxApi`) — the bundled\n * `XxxClient` beside it is the point of the role, so it is not judged as a wire type.\n *\n * \"Exports an @ApiPath contract\" is answered by the same source scan that owns apiRelations\n * (scan.apiLibProjects), so the tag can never drift from the code. The IPC / in-process / DTO-only\n * reading is a parser-only pass over the project's own non-test `src/**` — {@link ApiLibExportShape}.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult } from './api-scanner';\nimport { collectTsFiles, isTestFile } from './api-ast';\n\nconst API_LIB_ROLE = 'api-lib';\nconst API_CLIENT_ROLE = 'api-client';\n\n/** The decorators that make an abstract class an IPC contract. */\nconst IPC_DECORATORS: readonly string[] = ['WpInternal', 'WpIpcEndpoint'];\n\n/**\n * A tag/code mismatch:\n * - 'missing-tag' — exports an API contract but is tagged neither role:api-lib nor role:api-client.\n * - 'unnecessary-tag' — tagged role:api-lib / role:api-client but exports no contract, and (for\n * api-lib) is not a DTO-only library either; `offenders` names what it exports.\n */\nexport class ApiLibTagViolation {\n constructor(\n public readonly project: string,\n public readonly kind: 'missing-tag' | 'unnecessary-tag',\n /** The role it carries (for 'unnecessary-tag'), else the role it should carry. */\n public readonly role: string,\n /** Exports that are neither a contract nor a wire type — empty for 'missing-tag'. */\n public readonly offenders: readonly string[] = [],\n ) {}\n}\n\n/** What a library's own source exports, sorted into contracts, wire types and everything else. */\nexport class ApiLibExportShape {\n constructor(\n /** True when it exports an IPC contract or an in-process abstract `…Api`. */\n public readonly exportsContract: boolean,\n /** True when it exports at least one wire type (interface, alias, enum, data class). */\n public readonly exportsWireType: boolean,\n /** `kind Name (file)` of every export that is neither — the implementation an api-lib must not hold. */\n public readonly offenders: readonly string[],\n ) {}\n\n /** A role:api-lib passes when it holds a contract or wire types, and nothing else. */\n isApiLib(): boolean {\n return (this.exportsContract || this.exportsWireType) && this.offenders.length === 0;\n }\n\n /** Parser-only: every top-level EXPORTED declaration in the project's non-test `src/**`. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static read(projectDir: string): ApiLibExportShape {\n const srcDir = path.join(projectDir, 'src');\n const acc = new ExportShapeAccumulator(projectDir);\n if (fs.existsSync(srcDir)) {\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue;\n const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);\n for (const statement of source.statements) acc.add(statement, file);\n }\n }\n return acc.shape();\n }\n}\n\n/** Sorts each exported statement of one library into contract / wire type / offender. */\nclass ExportShapeAccumulator {\n private contract = false;\n private wire = false;\n private readonly offenders: string[] = [];\n\n constructor(private readonly projectDir: string) {}\n\n add(statement: ts.Statement, file: string): void {\n if (!this.isExported(statement)) return;\n const where = path.relative(this.projectDir, file);\n if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {\n this.wire = true;\n } else if (ts.isClassDeclaration(statement)) {\n this.addClass(statement, where);\n } else if (ts.isVariableStatement(statement)) {\n for (const decl of statement.declarationList.declarations) {\n if (this.isDiToken(decl.initializer)) continue;\n this.offenders.push(`const ${decl.name.getText()} (${where})`);\n }\n } else if (ts.isFunctionDeclaration(statement)) {\n this.offenders.push(`function ${statement.name?.text ?? '<default>'} (${where})`);\n }\n }\n\n shape(): ApiLibExportShape {\n return new ApiLibExportShape(this.contract, this.wire, [...this.offenders].sort());\n }\n\n private addClass(cls: ts.ClassDeclaration, where: string): void {\n const name = cls.name?.text ?? '<default>';\n const isAbstract = (ts.getModifiers(cls) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword);\n if (isAbstract && (name.endsWith('Api') || this.carriesIpc(cls))) {\n this.contract = true;\n return;\n }\n const hasMethod = cls.members.some(\n (member: ts.ClassElement) =>\n ts.isMethodDeclaration(member) &&\n !(ts.getModifiers(member) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.StaticKeyword),\n );\n if (hasMethod) this.offenders.push(`class ${name} (${where}) — it has methods, so it is an implementation`);\n else this.wire = true;\n }\n\n /** `@WpInternal(...)` on the class or `@WpIpcEndpoint(...)` on any member. */\n private carriesIpc(cls: ts.ClassDeclaration): boolean {\n const named = (node: ts.Node): boolean =>\n (ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []).some((d: ts.Decorator) => {\n const expr = ts.isCallExpression(d.expression) ? d.expression.expression : d.expression;\n const text = ts.isPropertyAccessExpression(expr) ? expr.name.text : expr.getText();\n return IPC_DECORATORS.includes(text);\n });\n return named(cls) || cls.members.some((member: ts.ClassElement) => named(member));\n }\n\n /** `Symbol('x')`, `Symbol.for('x')`, or an object literal of them — the token an abstract `…Api` binds behind. */\n private isDiToken(init: ts.Expression | undefined): boolean {\n if (init === undefined) return false;\n const expr = ts.isAsExpression(init) || ts.isSatisfiesExpression(init) ? init.expression : init;\n if (ts.isCallExpression(expr)) {\n const callee = expr.expression.getText();\n return callee === 'Symbol' || callee === 'Symbol.for';\n }\n if (ts.isObjectLiteralExpression(expr)) {\n return expr.properties.length > 0 && expr.properties.every(\n (prop: ts.ObjectLiteralElementLike) =>\n ts.isPropertyAssignment(prop) && this.isDiToken(prop.initializer),\n );\n }\n return false;\n }\n\n private isExported(statement: ts.Statement): boolean {\n if (!ts.canHaveModifiers(statement)) return false;\n return (ts.getModifiers(statement) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword);\n }\n}\n\n/** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth for @ApiPath. */\n// webpieces-disable no-function-outside-class -- validator-lib entry point, matches api-relations-validator.ts\nexport function findApiLibTagViolations(\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n workspaceRoot: string,\n): ApiLibTagViolation[] {\n const violations: ApiLibTagViolation[] = [];\n for (const projectName of projectInfos.keys()) {\n const info = projectInfos.get(projectName)!;\n const role = resolveRole(info).role;\n const isApiRole = role === API_LIB_ROLE || role === API_CLIENT_ROLE;\n const exportsApi = scan.apiLibProjects.has(projectName);\n\n if (exportsApi && !isApiRole) {\n violations.push(new ApiLibTagViolation(projectName, 'missing-tag', API_LIB_ROLE));\n }\n // Only judge a tag when we actually SCANNED the project's source — otherwise we can't prove it\n // exports no contract (an unscannable project would falsely look empty).\n if (!isApiRole || exportsApi || !scan.scannedProjects.has(projectName)) continue;\n const shape = ApiLibExportShape.read(path.resolve(workspaceRoot, info.root));\n const ok = role === API_CLIENT_ROLE ? shape.exportsContract : shape.isApiLib();\n if (!ok) violations.push(new ApiLibTagViolation(projectName, 'unnecessary-tag', role, shape.offenders));\n }\n return violations.sort((a: ApiLibTagViolation, b: ApiLibTagViolation) => a.project.localeCompare(b.project));\n}\n\n/** Human-readable, fix-oriented report for one tag/code mismatch. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches api-relations-validator.ts\nexport function describeApiLibTagViolation(violation: ApiLibTagViolation): string {\n if (violation.kind === 'missing-tag') {\n return (\n ` ❌ '${violation.project}' exports an API contract (an abstract @ApiPath/@Rpc/@PubSub class) ` +\n `but is tagged neither 'role:api-lib' nor 'role:api-client'.\\n` +\n ` Add \"role:api-lib\" to its project.json \"tags\" (replacing any \"role:lib\"), or \"role:api-client\" ` +\n `when it also bundles the default implementation that talks to an outside system.`\n );\n }\n if (violation.role === API_CLIENT_ROLE) {\n return (\n ` ❌ '${violation.project}' is tagged 'role:api-client' but exports NO contract (no abstract ` +\n `…Api class, no @WpInternal/@WpIpcEndpoint contract).\\n` +\n ` An api-client is a contract PLUS its default implementation: export the abstract XxxApi it ` +\n `implements, or retag it (e.g. \"role:lib\").`\n );\n }\n const offenders =\n violation.offenders.length === 0\n ? ''\n : `\\n It exports, besides contracts and wire types: ${violation.offenders.join('; ')}.`;\n return (\n ` ❌ '${violation.project}' is tagged 'role:api-lib' but is neither a contract library (@ApiPath/@Rpc/` +\n `@PubSub, @WpInternal/@WpIpcEndpoint, or an abstract …Api behind a DI token) nor a DTO-only library ` +\n `(interfaces, type aliases, enums and data classes only).${offenders}\\n` +\n ` Move the implementation out to a role:lib / role:designed-lib project, or retag it.`\n );\n}\n"]}
1
+ {"version":3,"file":"api-lib-tag-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-lib-tag-validator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AAmQH,0DA4BC;AAID,gEA4BC;;AA7TD,+CAAyB;AACzB,mDAA6B;AAC7B,uDAAiC;AAEjC,oDAA+C;AAE/C,uCAAuD;AAEvD,MAAM,YAAY,GAAG,SAAS,CAAC;AAC/B,MAAM,eAAe,GAAG,YAAY,CAAC;AAErC,kEAAkE;AAClE,MAAM,cAAc,GAAsB,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;AAE1E;;;;;GAKG;AACH,MAAa,kBAAkB;IAEP;IACA;IAEA;IAEA;IANpB,YACoB,OAAe,EACf,IAAuC;IACvD,kFAAkF;IAClE,IAAY;IAC5B,qFAAqF;IACrE,YAA+B,EAAE;QALjC,YAAO,GAAP,OAAO,CAAQ;QACf,SAAI,GAAJ,IAAI,CAAmC;QAEvC,SAAI,GAAJ,IAAI,CAAQ;QAEZ,cAAS,GAAT,SAAS,CAAwB;IAClD,CAAC;CACP;AATD,gDASC;AAED,kGAAkG;AAClG,MAAa,iBAAiB;IAGN;IAEA;IAEA;IANpB;IACI,6EAA6E;IAC7D,eAAwB;IACxC,wFAAwF;IACxE,eAAwB;IACxC,wGAAwG;IACxF,SAA4B;QAJ5B,oBAAe,GAAf,eAAe,CAAS;QAExB,oBAAe,GAAf,eAAe,CAAS;QAExB,cAAS,GAAT,SAAS,CAAmB;IAC7C,CAAC;IAEJ,sFAAsF;IACtF,QAAQ;QACJ,OAAO,CAAC,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC;IACzF,CAAC;IAED,4FAA4F;IAC5F,8EAA8E;IAC9E,MAAM,CAAC,IAAI,CAAC,UAAkB;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,IAAI,sBAAsB,CAAC,UAAU,CAAC,CAAC;QACnD,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACxB,KAAK,MAAM,IAAI,IAAI,IAAA,wBAAc,EAAC,MAAM,CAAC,EAAE,CAAC;gBACxC,IAAI,IAAA,oBAAU,EAAC,IAAI,CAAC;oBAAE,SAAS;gBAC/B,MAAM,MAAM,GAAG,EAAE,CAAC,gBAAgB,CAC9B,IAAI,EACJ,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAC7B,EAAE,CAAC,YAAY,CAAC,MAAM,EACtB,IAAI,CACP,CAAC;gBACF,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,UAAU;oBAAE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YACxE,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACJ;AAlCD,8CAkCC;AAED,yFAAyF;AACzF,MAAM,sBAAsB;IAOK;IANrB,QAAQ,GAAG,KAAK,CAAC;IACjB,IAAI,GAAG,KAAK,CAAC;IACJ,SAAS,GAAa,EAAE,CAAC;IACzB,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,4BAA4B,GAAG,IAAI,GAAG,EAAU,CAAC;IAElE,YAA6B,UAAkB;QAAlB,eAAU,GAAV,UAAU,CAAQ;IAAG,CAAC;IAEnD,GAAG,CAAC,SAAuB,EAAE,IAAY;QACrC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACnD,IAAI,EAAE,CAAC,sBAAsB,CAAC,SAAS,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;YACjB,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzF,CAAC;aAAM,IAAI,EAAE,CAAC,sBAAsB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,CAAC;YACjF,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACrB,CAAC;aAAM,IAAI,EAAE,CAAC,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;aAAM,IAAI,EAAE,CAAC,mBAAmB,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3C,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,eAAe,CAAC,YAAY,EAAE,CAAC;gBACxD,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC;oBACzE,SAAS;gBACb,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,KAAK,GAAG,CAAC,CAAC;YACnE,CAAC;QACL,CAAC;aAAM,IAAI,EAAE,CAAC,qBAAqB,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,SAAS,CAAC,IAAI,EAAE,IAAI,IAAI,WAAW,KAAK,KAAK,GAAG,CAAC,CAAC;QACtF,CAAC;IACL,CAAC;IAED,KAAK;QACD,MAAM,mBAAmB,GAAG,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CACtE,IAAI,CAAC,4BAA4B,CAAC,GAAG,CAAC,IAAI,CAAC,CAC9C,CAAC;QACF,OAAO,IAAI,iBAAiB,CACxB,IAAI,CAAC,QAAQ,IAAI,mBAAmB,EACpC,IAAI,CAAC,IAAI,EACT,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAC7B,CAAC;IACN,CAAC;IAEO,QAAQ,CAAC,GAAwB,EAAE,KAAa;QACpD,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,IAAI,IAAI,WAAW,CAAC;QAC3C,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,+BAA+B,CAAC,EAAE,CAAC;YAC1D,KAAK,MAAM,QAAQ,IAAI,GAAG,CAAC,eAAe,IAAI,EAAE,EAAE,CAAC;gBAC/C,IAAI,QAAQ,CAAC,KAAK,KAAK,EAAE,CAAC,UAAU,CAAC,iBAAiB;oBAAE,SAAS;gBACjE,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;oBAChC,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;oBAC/D,IAAI,WAAW,KAAK,SAAS;wBACzB,IAAI,CAAC,4BAA4B,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBAC3D,CAAC;YACL,CAAC;QACL,CAAC;QACD,MAAM,UAAU,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAChD,CAAC,CAAc,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,eAAe,CAC/D,CAAC;QACF,IAAI,UAAU,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC/D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,OAAO;QACX,CAAC;QACD,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAC9B,CAAC,MAAuB,EAAE,EAAE,CACxB,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC;YAC9B,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CACjC,CAAC,CAAc,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,aAAa,CAC7D,CACR,CAAC;QACF,IAAI,SAAS;YACT,IAAI,CAAC,SAAS,CAAC,IAAI,CACf,SAAS,IAAI,KAAK,KAAK,gDAAgD,CAC1E,CAAC;;YACD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED,8EAA8E;IACtE,UAAU,CAAC,GAAwB;QACvC,MAAM,KAAK,GAAG,CAAC,IAAa,EAAW,EAAE,CACrC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACzE,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAuB,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IACtF,CAAC;IAEO,YAAY,CAAC,IAAa,EAAE,QAAgB;QAChD,OAAO,CAAC,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAC1E,CAAC,SAAuB,EAAE,EAAE;YACxB,MAAM,IAAI,GAAG,EAAE,CAAC,gBAAgB,CAAC,SAAS,CAAC,UAAU,CAAC;gBAClD,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU;gBACjC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC;YAC3B,MAAM,IAAI,GAAG,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACnF,OAAO,IAAI,KAAK,QAAQ,CAAC;QAC7B,CAAC,CACJ,CAAC;IACN,CAAC;IAED,8FAA8F;IACtF,cAAc,CAAC,IAA+B;QAClD,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/B,IACI,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC;YAC5B,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC;YACzB,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,WAAW;YACvC,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,YAAY;YACxC,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,WAAW;YACvC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC;YACrB,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,EACrC,CAAC;YACC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/E,IAAI,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,OAAO,CACH,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC;gBACnC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAClC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CACtC,CAAC;QACN,CAAC;QACD,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,IAAqB,EAAE,EAAE,CACtD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,CACvC,CAAC;QACN,CAAC;QACD,IAAI,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CACtB,CAAC,OAAsB,EAAE,EAAE,CACvB,EAAE,CAAC,mBAAmB,CAAC,OAAO,CAAC;gBAC/B,CAAC,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC;oBACxB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC;oBACzC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAC1C,CAAC;QACN,CAAC;QACD,IAAI,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAiC,EAAE,EAAE;gBAC/D,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC;oBAAE,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBAChF,IAAI,EAAE,CAAC,6BAA6B,CAAC,IAAI,CAAC;oBAAE,OAAO,IAAI,CAAC;gBACxD,IAAI,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC;oBAAE,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAC7E,OAAO,KAAK,CAAC;YACjB,CAAC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,MAAM,CAAC,IAAmB;QAC9B,IACI,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC;YACvB,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC;YAClC,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC;YAC9B,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC;YAClC,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAC9B,CAAC;YACC,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,kHAAkH;IAC1G,SAAS,CAAC,IAA+B;QAC7C,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QACrC,MAAM,IAAI,GACN,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;QACvF,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YACzC,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,YAAY,CAAC;QAC1D,CAAC;QACD,IAAI,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO,CACH,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;gBAC1B,IAAI,CAAC,UAAU,CAAC,KAAK,CACjB,CAAC,IAAiC,EAAE,EAAE,CAClC,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CACxE,CACJ,CAAC;QACN,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,UAAU,CAAC,SAAuB;QACtC,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,SAAS,CAAC;YAAE,OAAO,KAAK,CAAC;QAClD,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAC1C,CAAC,CAAc,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,aAAa,CAC7D,CAAC;IACN,CAAC;CACJ;AAED,2GAA2G;AAC3G,+GAA+G;AAC/G,SAAgB,uBAAuB,CACnC,YAAsC,EACtC,IAAmB,EACnB,aAAqB;IAErB,MAAM,UAAU,GAAyB,EAAE,CAAC;IAC5C,KAAK,MAAM,WAAW,IAAI,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,WAAW,CAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,IAAA,2BAAW,EAAC,IAAI,CAAC,CAAC,IAAI,CAAC;QACpC,MAAM,SAAS,GAAG,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,eAAe,CAAC;QACpE,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAExD,IAAI,UAAU,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3B,UAAU,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,WAAW,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC;QACtF,CAAC;QACD,+FAA+F;QAC/F,yEAAyE;QACzE,IAAI,CAAC,SAAS,IAAI,UAAU,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,SAAS;QACjF,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7E,MAAM,EAAE,GAAG,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/E,IAAI,CAAC,EAAE;YACH,UAAU,CAAC,IAAI,CACX,IAAI,kBAAkB,CAAC,WAAW,EAAE,iBAAiB,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,CAChF,CAAC;IACV,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAqB,EAAE,CAAqB,EAAE,EAAE,CACpE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CACrC,CAAC;AACN,CAAC;AAED,qEAAqE;AACrE,oGAAoG;AACpG,SAAgB,0BAA0B,CAAC,SAA6B;IACpE,IAAI,SAAS,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACnC,OAAO,CACH,QAAQ,SAAS,CAAC,OAAO,sEAAsE;YAC/F,+DAA+D;YAC/D,sGAAsG;YACtG,kFAAkF,CACrF,CAAC;IACN,CAAC;IACD,IAAI,SAAS,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;QACrC,OAAO,CACH,QAAQ,SAAS,CAAC,OAAO,qEAAqE;YAC9F,iGAAiG;YACjG,iDAAiD;YACjD,qGAAqG;YACrG,mDAAmD,CACtD,CAAC;IACN,CAAC;IACD,MAAM,SAAS,GACX,SAAS,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;QAC5B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,wDAAwD,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACpG,OAAO,CACH,QAAQ,SAAS,CAAC,OAAO,8EAA8E;QACvG,qGAAqG;QACrG,2DAA2D,SAAS,IAAI;QACxE,0FAA0F,CAC7F,CAAC;AACN,CAAC","sourcesContent":["/**\n * API-lib tag validator (two-way)\n *\n * Keeps the two api roles and the CODE in sync, both directions (#1064, D1 + D3):\n * - a project that exports an `@ApiPath`/`@Rpc`/`@PubSub` contract (or a vendor contract under\n * `externalApiPaths`) MUST be tagged `role:api-lib` or `role:api-client` — else the arch graph, the\n * edge line-styles, and validate-api-relations can't treat it as one;\n * - a project tagged `role:api-lib` MUST export a contract or ONLY wire types — else the tag is a lie.\n * A contract is any of: an `@ApiPath`/`@Rpc`/`@PubSub` class (from the scan), an IPC contract\n * (`@WpInternal` / `@WpIpcEndpoint`), or an in-process abstract `…Api` behind a DI token. A DTO-only\n * library exports interfaces, type aliases, enums, data classes (no methods), and data-only constants;\n * - a project tagged `role:api-client` MUST export a contract: either an abstract `XxxApi`, or an\n * exported `XxxApi` interface implemented by a client registered with `@provideSingletonDefaultForApi`.\n * The bundled client beside it is the point of the role, so it is not judged as a wire type.\n *\n * \"Exports an @ApiPath contract\" is answered by the same source scan that owns apiRelations\n * (scan.apiLibProjects), so the tag can never drift from the code. The IPC / in-process / DTO-only\n * reading is a parser-only pass over the project's own non-test `src/**` — {@link ApiLibExportShape}.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult } from './api-scanner';\nimport { collectTsFiles, isTestFile } from './api-ast';\n\nconst API_LIB_ROLE = 'api-lib';\nconst API_CLIENT_ROLE = 'api-client';\n\n/** The decorators that make an abstract class an IPC contract. */\nconst IPC_DECORATORS: readonly string[] = ['WpInternal', 'WpIpcEndpoint'];\n\n/**\n * A tag/code mismatch:\n * - 'missing-tag' — exports an API contract but is tagged neither role:api-lib nor role:api-client.\n * - 'unnecessary-tag' — tagged role:api-lib / role:api-client but exports no contract, and (for\n * api-lib) is not a DTO-only library either; `offenders` names what it exports.\n */\nexport class ApiLibTagViolation {\n constructor(\n public readonly project: string,\n public readonly kind: 'missing-tag' | 'unnecessary-tag',\n /** The role it carries (for 'unnecessary-tag'), else the role it should carry. */\n public readonly role: string,\n /** Exports that are neither a contract nor a wire type — empty for 'missing-tag'. */\n public readonly offenders: readonly string[] = [],\n ) {}\n}\n\n/** What a library's own source exports, sorted into contracts, wire types and everything else. */\nexport class ApiLibExportShape {\n constructor(\n /** True when it exports an IPC contract or an in-process abstract `…Api`. */\n public readonly exportsContract: boolean,\n /** True when it exports at least one wire type (interface, alias, enum, data class). */\n public readonly exportsWireType: boolean,\n /** `kind Name (file)` of every export that is neither — the implementation an api-lib must not hold. */\n public readonly offenders: readonly string[],\n ) {}\n\n /** A role:api-lib passes when it holds a contract or wire types, and nothing else. */\n isApiLib(): boolean {\n return (this.exportsContract || this.exportsWireType) && this.offenders.length === 0;\n }\n\n /** Parser-only: every top-level EXPORTED declaration in the project's non-test `src/**`. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static read(projectDir: string): ApiLibExportShape {\n const srcDir = path.join(projectDir, 'src');\n const acc = new ExportShapeAccumulator(projectDir);\n if (fs.existsSync(srcDir)) {\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue;\n const source = ts.createSourceFile(\n file,\n fs.readFileSync(file, 'utf8'),\n ts.ScriptTarget.Latest,\n true,\n );\n for (const statement of source.statements) acc.add(statement, file);\n }\n }\n return acc.shape();\n }\n}\n\n/** Sorts each exported statement of one library into contract / wire type / offender. */\nclass ExportShapeAccumulator {\n private contract = false;\n private wire = false;\n private readonly offenders: string[] = [];\n private readonly apiInterfaces = new Set<string>();\n private readonly registeredApiImplementations = new Set<string>();\n\n constructor(private readonly projectDir: string) {}\n\n add(statement: ts.Statement, file: string): void {\n if (!this.isExported(statement)) return;\n const where = path.relative(this.projectDir, file);\n if (ts.isInterfaceDeclaration(statement)) {\n this.wire = true;\n if (statement.name.text.endsWith('Api')) this.apiInterfaces.add(statement.name.text);\n } else if (ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {\n this.wire = true;\n } else if (ts.isClassDeclaration(statement)) {\n this.addClass(statement, where);\n } else if (ts.isVariableStatement(statement)) {\n for (const decl of statement.declarationList.declarations) {\n if (this.isDiToken(decl.initializer) || this.isDataConstant(decl.initializer))\n continue;\n this.offenders.push(`const ${decl.name.getText()} (${where})`);\n }\n } else if (ts.isFunctionDeclaration(statement)) {\n this.offenders.push(`function ${statement.name?.text ?? '<default>'} (${where})`);\n }\n }\n\n shape(): ApiLibExportShape {\n const registeredInterface = [...this.apiInterfaces].some((name: string) =>\n this.registeredApiImplementations.has(name),\n );\n return new ApiLibExportShape(\n this.contract || registeredInterface,\n this.wire,\n [...this.offenders].sort(),\n );\n }\n\n private addClass(cls: ts.ClassDeclaration, where: string): void {\n const name = cls.name?.text ?? '<default>';\n if (this.hasDecorator(cls, 'provideSingletonDefaultForApi')) {\n for (const heritage of cls.heritageClauses ?? []) {\n if (heritage.token !== ts.SyntaxKind.ImplementsKeyword) continue;\n for (const type of heritage.types) {\n const implemented = type.expression.getText().split('.').pop();\n if (implemented !== undefined)\n this.registeredApiImplementations.add(implemented);\n }\n }\n }\n const isAbstract = (ts.getModifiers(cls) ?? []).some(\n (m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword,\n );\n if (isAbstract && (name.endsWith('Api') || this.carriesIpc(cls))) {\n this.contract = true;\n return;\n }\n const hasMethod = cls.members.some(\n (member: ts.ClassElement) =>\n ts.isMethodDeclaration(member) &&\n !(ts.getModifiers(member) ?? []).some(\n (m: ts.Modifier) => m.kind === ts.SyntaxKind.StaticKeyword,\n ),\n );\n if (hasMethod)\n this.offenders.push(\n `class ${name} (${where}) — it has methods, so it is an implementation`,\n );\n else this.wire = true;\n }\n\n /** `@WpInternal(...)` on the class or `@WpIpcEndpoint(...)` on any member. */\n private carriesIpc(cls: ts.ClassDeclaration): boolean {\n const named = (node: ts.Node): boolean =>\n IPC_DECORATORS.some((name: string) => this.hasDecorator(node, name));\n return named(cls) || cls.members.some((member: ts.ClassElement) => named(member));\n }\n\n private hasDecorator(node: ts.Node, expected: string): boolean {\n return (ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []).some(\n (decorator: ts.Decorator) => {\n const expr = ts.isCallExpression(decorator.expression)\n ? decorator.expression.expression\n : decorator.expression;\n const name = ts.isPropertyAccessExpression(expr) ? expr.name.text : expr.getText();\n return name === expected;\n },\n );\n }\n\n /** Literal/structural protocol data is contract support, not an executable implementation. */\n private isDataConstant(init: ts.Expression | undefined): boolean {\n if (init === undefined) return false;\n const expr = this.unwrap(init);\n if (\n ts.isStringLiteralLike(expr) ||\n ts.isNumericLiteral(expr) ||\n expr.kind === ts.SyntaxKind.TrueKeyword ||\n expr.kind === ts.SyntaxKind.FalseKeyword ||\n expr.kind === ts.SyntaxKind.NullKeyword ||\n ts.isIdentifier(expr) ||\n ts.isPropertyAccessExpression(expr)\n ) {\n return true;\n }\n if (ts.isPrefixUnaryExpression(expr)) return this.isDataConstant(expr.operand);\n if (ts.isBinaryExpression(expr)) {\n return this.isDataConstant(expr.left) && this.isDataConstant(expr.right);\n }\n if (ts.isConditionalExpression(expr)) {\n return (\n this.isDataConstant(expr.condition) &&\n this.isDataConstant(expr.whenTrue) &&\n this.isDataConstant(expr.whenFalse)\n );\n }\n if (ts.isTemplateExpression(expr)) {\n return expr.templateSpans.every((span: ts.TemplateSpan) =>\n this.isDataConstant(span.expression),\n );\n }\n if (ts.isArrayLiteralExpression(expr)) {\n return expr.elements.every(\n (element: ts.Expression) =>\n ts.isOmittedExpression(element) ||\n (ts.isSpreadElement(element)\n ? this.isDataConstant(element.expression)\n : this.isDataConstant(element)),\n );\n }\n if (ts.isObjectLiteralExpression(expr)) {\n return expr.properties.every((prop: ts.ObjectLiteralElementLike) => {\n if (ts.isPropertyAssignment(prop)) return this.isDataConstant(prop.initializer);\n if (ts.isShorthandPropertyAssignment(prop)) return true;\n if (ts.isSpreadAssignment(prop)) return this.isDataConstant(prop.expression);\n return false;\n });\n }\n return false;\n }\n\n private unwrap(expr: ts.Expression): ts.Expression {\n if (\n ts.isAsExpression(expr) ||\n ts.isTypeAssertionExpression(expr) ||\n ts.isSatisfiesExpression(expr) ||\n ts.isParenthesizedExpression(expr) ||\n ts.isNonNullExpression(expr)\n ) {\n return this.unwrap(expr.expression);\n }\n return expr;\n }\n\n /** `Symbol('x')`, `Symbol.for('x')`, or an object literal of them — the token an abstract `…Api` binds behind. */\n private isDiToken(init: ts.Expression | undefined): boolean {\n if (init === undefined) return false;\n const expr =\n ts.isAsExpression(init) || ts.isSatisfiesExpression(init) ? init.expression : init;\n if (ts.isCallExpression(expr)) {\n const callee = expr.expression.getText();\n return callee === 'Symbol' || callee === 'Symbol.for';\n }\n if (ts.isObjectLiteralExpression(expr)) {\n return (\n expr.properties.length > 0 &&\n expr.properties.every(\n (prop: ts.ObjectLiteralElementLike) =>\n ts.isPropertyAssignment(prop) && this.isDiToken(prop.initializer),\n )\n );\n }\n return false;\n }\n\n private isExported(statement: ts.Statement): boolean {\n if (!ts.canHaveModifiers(statement)) return false;\n return (ts.getModifiers(statement) ?? []).some(\n (m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword,\n );\n }\n}\n\n/** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth for @ApiPath. */\n// webpieces-disable no-function-outside-class -- validator-lib entry point, matches api-relations-validator.ts\nexport function findApiLibTagViolations(\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n workspaceRoot: string,\n): ApiLibTagViolation[] {\n const violations: ApiLibTagViolation[] = [];\n for (const projectName of projectInfos.keys()) {\n const info = projectInfos.get(projectName)!;\n const role = resolveRole(info).role;\n const isApiRole = role === API_LIB_ROLE || role === API_CLIENT_ROLE;\n const exportsApi = scan.apiLibProjects.has(projectName);\n\n if (exportsApi && !isApiRole) {\n violations.push(new ApiLibTagViolation(projectName, 'missing-tag', API_LIB_ROLE));\n }\n // Only judge a tag when we actually SCANNED the project's source — otherwise we can't prove it\n // exports no contract (an unscannable project would falsely look empty).\n if (!isApiRole || exportsApi || !scan.scannedProjects.has(projectName)) continue;\n const shape = ApiLibExportShape.read(path.resolve(workspaceRoot, info.root));\n const ok = role === API_CLIENT_ROLE ? shape.exportsContract : shape.isApiLib();\n if (!ok)\n violations.push(\n new ApiLibTagViolation(projectName, 'unnecessary-tag', role, shape.offenders),\n );\n }\n return violations.sort((a: ApiLibTagViolation, b: ApiLibTagViolation) =>\n a.project.localeCompare(b.project),\n );\n}\n\n/** Human-readable, fix-oriented report for one tag/code mismatch. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches api-relations-validator.ts\nexport function describeApiLibTagViolation(violation: ApiLibTagViolation): string {\n if (violation.kind === 'missing-tag') {\n return (\n ` ❌ '${violation.project}' exports an API contract (an abstract @ApiPath/@Rpc/@PubSub class) ` +\n `but is tagged neither 'role:api-lib' nor 'role:api-client'.\\n` +\n ` Add \"role:api-lib\" to its project.json \"tags\" (replacing any \"role:lib\"), or \"role:api-client\" ` +\n `when it also bundles the default implementation that talks to an outside system.`\n );\n }\n if (violation.role === API_CLIENT_ROLE) {\n return (\n ` ❌ '${violation.project}' is tagged 'role:api-client' but exports NO contract (no abstract ` +\n `…Api class, no @WpInternal/@WpIpcEndpoint contract, and no exported …Api interface implemented ` +\n `by an @provideSingletonDefaultForApi client).\\n` +\n ` An api-client is a contract PLUS its default implementation: export the XxxApi the registered ` +\n `client implements, or retag it (e.g. \"role:lib\").`\n );\n }\n const offenders =\n violation.offenders.length === 0\n ? ''\n : `\\n It exports, besides contracts and wire types: ${violation.offenders.join('; ')}.`;\n return (\n ` ❌ '${violation.project}' is tagged 'role:api-lib' but is neither a contract library (@ApiPath/@Rpc/` +\n `@PubSub, @WpInternal/@WpIpcEndpoint, or an abstract …Api behind a DI token) nor a DTO-only library ` +\n `(interfaces, type aliases, enums and data classes only).${offenders}\\n` +\n ` Move the implementation out to a role:lib / role:designed-lib project, or retag it.`\n );\n}\n"]}
@@ -43,12 +43,16 @@ export declare class DepUsageScanner {
43
43
  */
44
44
  isDevFile(relPath: string): boolean;
45
45
  /**
46
- * The package a bare specifier belongs to, or null for relative/absolute
47
- * paths and node: builtins. `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`.
46
+ * The package a bare specifier belongs to, or null for relative/absolute paths.
47
+ * `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`; `node:fs/promises` → `fs`.
48
+ *
49
+ * Node builtins deliberately stay in the result: api-lib-dependencies treats them as outside
50
+ * runtime dependencies, and one allow-list spelling must govern both `fs` and `node:fs`.
48
51
  */
49
52
  toPackageName(specifier: string): string | null;
50
53
  private walk;
51
54
  private scanFile;
55
+ /** Parse real syntax so prose such as `different from "no field"` cannot become a phantom import. */
52
56
  private extractPackageNames;
53
57
  private readFile;
54
58
  }
@@ -24,6 +24,7 @@ exports.DepUsageScanner = exports.DepUsage = void 0;
24
24
  const tslib_1 = require("tslib");
25
25
  const fs = tslib_1.__importStar(require("fs"));
26
26
  const path = tslib_1.__importStar(require("path"));
27
+ const ts = tslib_1.__importStar(require("typescript"));
27
28
  const toError_1 = require("../toError");
28
29
  /** Directories never worth scanning (build output, vendored code, VCS). */
29
30
  const SKIP_DIRS = new Set([
@@ -38,16 +39,7 @@ const SKIP_DIRS = new Set([
38
39
  '.angular',
39
40
  ]);
40
41
  /** Source extensions whose imports we understand. */
41
- const SOURCE_EXTENSIONS = new Set([
42
- '.ts',
43
- '.tsx',
44
- '.mts',
45
- '.cts',
46
- '.js',
47
- '.jsx',
48
- '.mjs',
49
- '.cjs',
50
- ]);
42
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
51
43
  /**
52
44
  * Directory names whose contents SHIP to consumers as scaffolding (copied into a consumer repo by the
53
45
  * installer), so their imports are consumer-facing PRODUCTION deps — NOT this project's own dev-time
@@ -56,9 +48,7 @@ const SOURCE_EXTENSIONS = new Set([
56
48
  * `@webpieces/eslint-rules`, which those very templates import) as "test-only in dependencies".
57
49
  * A `templates/` segment therefore short-circuits isDevFile to production, ahead of the name heuristics.
58
50
  */
59
- const SHIPPED_DIR_NAMES = new Set([
60
- 'templates',
61
- ]);
51
+ const SHIPPED_DIR_NAMES = new Set(['templates']);
62
52
  /**
63
53
  * Directory names that make everything below them test/dev-only.
64
54
  * Kept tight on purpose — a false "this is a test dir" would let a real
@@ -79,8 +69,6 @@ const TEST_FILE_RE = /[.-](spec|test|e2e|testkit|mock|mocks|fixture|fixtures)\.[
79
69
  const DEV_CONFIG_RE = /^(vitest|vite|jest|playwright|cypress|karma|webpack|rollup|eslint|prettier)\.[\w.-]*config\.[cm]?[jt]s$/;
80
70
  /** `jest.setup.ts`, `vitest.setup.ts`, `test-setup.ts`, ... */
81
71
  const DEV_SETUP_RE = /^([\w-]*[.-])?(setup|test-setup)\.[cm]?[jt]s$/;
82
- /** Bare-import extraction: `from 'x'`, `import 'x'`, `import('x')`, `require('x')`. */
83
- const IMPORT_RE = /(?:\bfrom\s*|\bimport\s*|\brequire\s*\(\s*|\bimport\s*\(\s*)['"]([^'"]+)['"]/g;
84
72
  /**
85
73
  * Which packages a project reaches from production code vs. only from test/dev code.
86
74
  * Data-only: no logic lives here (see CLAUDE.md — data structures are classes).
@@ -141,18 +129,24 @@ class DepUsageScanner {
141
129
  return false;
142
130
  }
143
131
  /**
144
- * The package a bare specifier belongs to, or null for relative/absolute
145
- * paths and node: builtins. `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`.
132
+ * The package a bare specifier belongs to, or null for relative/absolute paths.
133
+ * `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`; `node:fs/promises` → `fs`.
134
+ *
135
+ * Node builtins deliberately stay in the result: api-lib-dependencies treats them as outside
136
+ * runtime dependencies, and one allow-list spelling must govern both `fs` and `node:fs`.
146
137
  */
147
138
  toPackageName(specifier) {
148
139
  if (specifier.length === 0)
149
140
  return null;
150
141
  if (specifier.startsWith('.') || specifier.startsWith('/'))
151
142
  return null;
152
- if (specifier.startsWith('node:'))
143
+ const normalized = specifier.startsWith('node:')
144
+ ? specifier.slice('node:'.length)
145
+ : specifier;
146
+ if (normalized.length === 0)
153
147
  return null;
154
- const parts = specifier.split('/');
155
- if (specifier.startsWith('@')) {
148
+ const parts = normalized.split('/');
149
+ if (normalized.startsWith('@')) {
156
150
  if (parts.length < 2)
157
151
  return null;
158
152
  return `${parts[0]}/${parts[1]}`;
@@ -182,20 +176,43 @@ class DepUsageScanner {
182
176
  return;
183
177
  const relPath = path.relative(projectRoot, absPath);
184
178
  const bucket = this.isDevFile(relPath) ? usage.testPackages : usage.prodPackages;
185
- for (const packageName of this.extractPackageNames(source)) {
179
+ for (const packageName of this.extractPackageNames(source, absPath)) {
186
180
  bucket.add(packageName);
187
181
  }
188
182
  }
189
- extractPackageNames(source) {
183
+ /** Parse real syntax so prose such as `different from "no field"` cannot become a phantom import. */
184
+ extractPackageNames(source, fileName) {
190
185
  const names = [];
191
- IMPORT_RE.lastIndex = 0;
192
- let match = IMPORT_RE.exec(source);
193
- while (match !== null) {
194
- const packageName = this.toPackageName(match[1]);
186
+ const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
187
+ const add = (specifier) => {
188
+ if (specifier === undefined || !ts.isStringLiteralLike(specifier))
189
+ return;
190
+ const packageName = this.toPackageName(specifier.text);
195
191
  if (packageName !== null)
196
192
  names.push(packageName);
197
- match = IMPORT_RE.exec(source);
198
- }
193
+ };
194
+ const visit = (node) => {
195
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
196
+ add(node.moduleSpecifier);
197
+ }
198
+ else if (ts.isImportEqualsDeclaration(node) &&
199
+ ts.isExternalModuleReference(node.moduleReference)) {
200
+ add(node.moduleReference.expression);
201
+ }
202
+ else if (ts.isCallExpression(node)) {
203
+ const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
204
+ const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require';
205
+ if (isDynamicImport || isRequire)
206
+ add(node.arguments[0]);
207
+ }
208
+ else if (ts.isImportTypeNode(node)) {
209
+ const argument = node.argument;
210
+ if (ts.isLiteralTypeNode(argument))
211
+ add(argument.literal);
212
+ }
213
+ ts.forEachChild(node, visit);
214
+ };
215
+ visit(sourceFile);
199
216
  return names;
200
217
  }
201
218
  readFile(absPath) {
@@ -1 +1 @@
1
- {"version":3,"file":"dep-usage-scanner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dep-usage-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dep Usage Scanner\n *\n * Answers ONE question for a project: \"is package X reached from production\n * source, or ONLY from test/dev files?\"\n *\n * WHY this exists: nx derives graph edges from ALL TypeScript sources, specs\n * included. Without this scan, a package imported only by `*.spec.ts` looks\n * identical to a package imported by a controller, so the validator forces it\n * into `dependencies` — and `pnpm deploy --prod` then ships test machinery\n * (auth-bypass hooks, canned credentials, fakes) into the production image.\n * Splitting the scan by file kind lets `devDependencies` be the REQUIRED home\n * for test-only packages.\n *\n * The scan is deliberately conservative: a package is \"test-only\" ONLY when it\n * is imported by at least one test/dev file and by ZERO production files. Any\n * doubt (no import found at all, e.g. a runtime-only/reflection dependency)\n * resolves to \"production\", so this can never push a runtime-required package\n * out of `dependencies`.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../toError';\n\n/** Directories never worth scanning (build output, vendored code, VCS). */\nconst SKIP_DIRS = new Set([\n 'node_modules',\n 'dist',\n 'build',\n 'out-tsc',\n 'coverage',\n '.nx',\n '.git',\n 'tmp',\n '.angular',\n]);\n\n/** Source extensions whose imports we understand. */\nconst SOURCE_EXTENSIONS = new Set([\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n]);\n\n/**\n * Directory names whose contents SHIP to consumers as scaffolding (copied into a consumer repo by the\n * installer), so their imports are consumer-facing PRODUCTION deps — NOT this project's own dev-time\n * tooling. A file like `templates/eslint.webpieces.config.mjs` otherwise trips DEV_CONFIG_RE on its\n * name and gets mis-bucketed as dev, which then flags a genuinely-required dependency (e.g.\n * `@webpieces/eslint-rules`, which those very templates import) as \"test-only in dependencies\".\n * A `templates/` segment therefore short-circuits isDevFile to production, ahead of the name heuristics.\n */\nconst SHIPPED_DIR_NAMES = new Set([\n 'templates',\n]);\n\n/**\n * Directory names that make everything below them test/dev-only.\n * Kept tight on purpose — a false \"this is a test dir\" would let a real\n * production import be classified as test-only.\n */\nconst TEST_DIR_NAMES = new Set([\n '__tests__',\n '__mocks__',\n '__fixtures__',\n 'test',\n 'tests',\n 'e2e',\n 'e2e-tests',\n]);\n\n/** `foo.spec.ts`, `foo.test.tsx`, `foo-e2e.spec.mts`, `foo.testkit.ts`, ... */\nconst TEST_FILE_RE = /[.-](spec|test|e2e|testkit|mock|mocks|fixture|fixtures)\\.[cm]?[jt]sx?$/;\n\n/** Tooling config/bootstrap files: dev-time by definition. */\nconst DEV_CONFIG_RE =\n /^(vitest|vite|jest|playwright|cypress|karma|webpack|rollup|eslint|prettier)\\.[\\w.-]*config\\.[cm]?[jt]s$/;\n\n/** `jest.setup.ts`, `vitest.setup.ts`, `test-setup.ts`, ... */\nconst DEV_SETUP_RE = /^([\\w-]*[.-])?(setup|test-setup)\\.[cm]?[jt]s$/;\n\n/** Bare-import extraction: `from 'x'`, `import 'x'`, `import('x')`, `require('x')`. */\nconst IMPORT_RE =\n /(?:\\bfrom\\s*|\\bimport\\s*|\\brequire\\s*\\(\\s*|\\bimport\\s*\\(\\s*)['\"]([^'\"]+)['\"]/g;\n\n/**\n * Which packages a project reaches from production code vs. only from test/dev code.\n * Data-only: no logic lives here (see CLAUDE.md — data structures are classes).\n */\nexport class DepUsage {\n prodPackages: Set<string>;\n testPackages: Set<string>;\n\n constructor(prodPackages: Set<string>, testPackages: Set<string>) {\n this.prodPackages = prodPackages;\n this.testPackages = testPackages;\n }\n\n /** True when the package is imported by tests and by no production file. */\n isTestOnly(packageName: string): boolean {\n return this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n\n /** True when we saw the package in NO file at all (kind is unknown → treat as prod). */\n isUnseen(packageName: string): boolean {\n return !this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n}\n\nexport class DepUsageScanner {\n /**\n * Walk a project directory and record every bare import specifier, bucketed\n * by whether the importing file is production or test/dev.\n */\n scan(absProjectDir: string): DepUsage {\n const usage = new DepUsage(new Set<string>(), new Set<string>());\n if (!fs.existsSync(absProjectDir)) return usage;\n this.walk(absProjectDir, absProjectDir, usage);\n return usage;\n }\n\n /**\n * Is this path a test/dev file? Path is relative to the project root and\n * uses either separator.\n */\n isDevFile(relPath: string): boolean {\n const normalized = relPath.split(path.sep).join('/');\n const segments = normalized.split('/');\n const fileName = segments[segments.length - 1];\n\n // Shipped scaffolding wins over every dev heuristic below: a template file's imports are\n // consumer-facing production deps, even when its NAME looks like a dev config (eslint.*.config.mjs).\n for (const dir of segments.slice(0, -1)) {\n if (SHIPPED_DIR_NAMES.has(dir)) return false;\n }\n for (const dir of segments.slice(0, -1)) {\n if (TEST_DIR_NAMES.has(dir)) return true;\n }\n if (TEST_FILE_RE.test(fileName)) return true;\n if (DEV_CONFIG_RE.test(fileName)) return true;\n if (DEV_SETUP_RE.test(fileName)) return true;\n return false;\n }\n\n /**\n * The package a bare specifier belongs to, or null for relative/absolute\n * paths and node: builtins. `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`.\n */\n toPackageName(specifier: string): string | null {\n if (specifier.length === 0) return null;\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null;\n if (specifier.startsWith('node:')) return null;\n const parts = specifier.split('/');\n if (specifier.startsWith('@')) {\n if (parts.length < 2) return null;\n return `${parts[0]}/${parts[1]}`;\n }\n return parts[0];\n }\n\n private walk(absDir: string, projectRoot: string, usage: DepUsage): void {\n const entries = fs.readdirSync(absDir, { withFileTypes: true });\n for (const entry of entries) {\n const absPath = path.join(absDir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n this.walk(absPath, projectRoot, usage);\n continue;\n }\n if (!entry.isFile()) continue;\n if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue;\n this.scanFile(absPath, projectRoot, usage);\n }\n }\n\n private scanFile(absPath: string, projectRoot: string, usage: DepUsage): void {\n const source = this.readFile(absPath);\n if (source === null) return;\n const relPath = path.relative(projectRoot, absPath);\n const bucket = this.isDevFile(relPath) ? usage.testPackages : usage.prodPackages;\n for (const packageName of this.extractPackageNames(source)) {\n bucket.add(packageName);\n }\n }\n\n private extractPackageNames(source: string): string[] {\n const names: string[] = [];\n IMPORT_RE.lastIndex = 0;\n let match = IMPORT_RE.exec(source);\n while (match !== null) {\n const packageName = this.toPackageName(match[1]);\n if (packageName !== null) names.push(packageName);\n match = IMPORT_RE.exec(source);\n }\n return names;\n }\n\n private readFile(absPath: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readFileSync(absPath, 'utf-8');\n } catch (err: unknown) {\n const error = toError(err);\n // An unreadable file cannot change the classification of a package; skipping it\n // only ever makes the scan MORE conservative (fewer test-only classifications).\n console.warn(`Could not read ${absPath} while classifying deps: ${error.message}`);\n return null;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"dep-usage-scanner.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dep-usage-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dep Usage Scanner\n *\n * Answers ONE question for a project: \"is package X reached from production\n * source, or ONLY from test/dev files?\"\n *\n * WHY this exists: nx derives graph edges from ALL TypeScript sources, specs\n * included. Without this scan, a package imported only by `*.spec.ts` looks\n * identical to a package imported by a controller, so the validator forces it\n * into `dependencies` — and `pnpm deploy --prod` then ships test machinery\n * (auth-bypass hooks, canned credentials, fakes) into the production image.\n * Splitting the scan by file kind lets `devDependencies` be the REQUIRED home\n * for test-only packages.\n *\n * The scan is deliberately conservative: a package is \"test-only\" ONLY when it\n * is imported by at least one test/dev file and by ZERO production files. Any\n * doubt (no import found at all, e.g. a runtime-only/reflection dependency)\n * resolves to \"production\", so this can never push a runtime-required package\n * out of `dependencies`.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport { toError } from '../toError';\n\n/** Directories never worth scanning (build output, vendored code, VCS). */\nconst SKIP_DIRS = new Set([\n 'node_modules',\n 'dist',\n 'build',\n 'out-tsc',\n 'coverage',\n '.nx',\n '.git',\n 'tmp',\n '.angular',\n]);\n\n/** Source extensions whose imports we understand. */\nconst SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);\n\n/**\n * Directory names whose contents SHIP to consumers as scaffolding (copied into a consumer repo by the\n * installer), so their imports are consumer-facing PRODUCTION deps — NOT this project's own dev-time\n * tooling. A file like `templates/eslint.webpieces.config.mjs` otherwise trips DEV_CONFIG_RE on its\n * name and gets mis-bucketed as dev, which then flags a genuinely-required dependency (e.g.\n * `@webpieces/eslint-rules`, which those very templates import) as \"test-only in dependencies\".\n * A `templates/` segment therefore short-circuits isDevFile to production, ahead of the name heuristics.\n */\nconst SHIPPED_DIR_NAMES = new Set(['templates']);\n\n/**\n * Directory names that make everything below them test/dev-only.\n * Kept tight on purpose — a false \"this is a test dir\" would let a real\n * production import be classified as test-only.\n */\nconst TEST_DIR_NAMES = new Set([\n '__tests__',\n '__mocks__',\n '__fixtures__',\n 'test',\n 'tests',\n 'e2e',\n 'e2e-tests',\n]);\n\n/** `foo.spec.ts`, `foo.test.tsx`, `foo-e2e.spec.mts`, `foo.testkit.ts`, ... */\nconst TEST_FILE_RE = /[.-](spec|test|e2e|testkit|mock|mocks|fixture|fixtures)\\.[cm]?[jt]sx?$/;\n\n/** Tooling config/bootstrap files: dev-time by definition. */\nconst DEV_CONFIG_RE =\n /^(vitest|vite|jest|playwright|cypress|karma|webpack|rollup|eslint|prettier)\\.[\\w.-]*config\\.[cm]?[jt]s$/;\n\n/** `jest.setup.ts`, `vitest.setup.ts`, `test-setup.ts`, ... */\nconst DEV_SETUP_RE = /^([\\w-]*[.-])?(setup|test-setup)\\.[cm]?[jt]s$/;\n\n/**\n * Which packages a project reaches from production code vs. only from test/dev code.\n * Data-only: no logic lives here (see CLAUDE.md — data structures are classes).\n */\nexport class DepUsage {\n prodPackages: Set<string>;\n testPackages: Set<string>;\n\n constructor(prodPackages: Set<string>, testPackages: Set<string>) {\n this.prodPackages = prodPackages;\n this.testPackages = testPackages;\n }\n\n /** True when the package is imported by tests and by no production file. */\n isTestOnly(packageName: string): boolean {\n return this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n\n /** True when we saw the package in NO file at all (kind is unknown → treat as prod). */\n isUnseen(packageName: string): boolean {\n return !this.testPackages.has(packageName) && !this.prodPackages.has(packageName);\n }\n}\n\nexport class DepUsageScanner {\n /**\n * Walk a project directory and record every bare import specifier, bucketed\n * by whether the importing file is production or test/dev.\n */\n scan(absProjectDir: string): DepUsage {\n const usage = new DepUsage(new Set<string>(), new Set<string>());\n if (!fs.existsSync(absProjectDir)) return usage;\n this.walk(absProjectDir, absProjectDir, usage);\n return usage;\n }\n\n /**\n * Is this path a test/dev file? Path is relative to the project root and\n * uses either separator.\n */\n isDevFile(relPath: string): boolean {\n const normalized = relPath.split(path.sep).join('/');\n const segments = normalized.split('/');\n const fileName = segments[segments.length - 1];\n\n // Shipped scaffolding wins over every dev heuristic below: a template file's imports are\n // consumer-facing production deps, even when its NAME looks like a dev config (eslint.*.config.mjs).\n for (const dir of segments.slice(0, -1)) {\n if (SHIPPED_DIR_NAMES.has(dir)) return false;\n }\n for (const dir of segments.slice(0, -1)) {\n if (TEST_DIR_NAMES.has(dir)) return true;\n }\n if (TEST_FILE_RE.test(fileName)) return true;\n if (DEV_CONFIG_RE.test(fileName)) return true;\n if (DEV_SETUP_RE.test(fileName)) return true;\n return false;\n }\n\n /**\n * The package a bare specifier belongs to, or null for relative/absolute paths.\n * `@scope/pkg/sub` → `@scope/pkg`; `pkg/sub` → `pkg`; `node:fs/promises` → `fs`.\n *\n * Node builtins deliberately stay in the result: api-lib-dependencies treats them as outside\n * runtime dependencies, and one allow-list spelling must govern both `fs` and `node:fs`.\n */\n toPackageName(specifier: string): string | null {\n if (specifier.length === 0) return null;\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null;\n const normalized = specifier.startsWith('node:')\n ? specifier.slice('node:'.length)\n : specifier;\n if (normalized.length === 0) return null;\n const parts = normalized.split('/');\n if (normalized.startsWith('@')) {\n if (parts.length < 2) return null;\n return `${parts[0]}/${parts[1]}`;\n }\n return parts[0];\n }\n\n private walk(absDir: string, projectRoot: string, usage: DepUsage): void {\n const entries = fs.readdirSync(absDir, { withFileTypes: true });\n for (const entry of entries) {\n const absPath = path.join(absDir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n this.walk(absPath, projectRoot, usage);\n continue;\n }\n if (!entry.isFile()) continue;\n if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue;\n this.scanFile(absPath, projectRoot, usage);\n }\n }\n\n private scanFile(absPath: string, projectRoot: string, usage: DepUsage): void {\n const source = this.readFile(absPath);\n if (source === null) return;\n const relPath = path.relative(projectRoot, absPath);\n const bucket = this.isDevFile(relPath) ? usage.testPackages : usage.prodPackages;\n for (const packageName of this.extractPackageNames(source, absPath)) {\n bucket.add(packageName);\n }\n }\n\n /** Parse real syntax so prose such as `different from \"no field\"` cannot become a phantom import. */\n private extractPackageNames(source: string, fileName: string): string[] {\n const names: string[] = [];\n const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);\n const add = (specifier: ts.Expression | undefined): void => {\n if (specifier === undefined || !ts.isStringLiteralLike(specifier)) return;\n const packageName = this.toPackageName(specifier.text);\n if (packageName !== null) names.push(packageName);\n };\n const visit = (node: ts.Node): void => {\n if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {\n add(node.moduleSpecifier);\n } else if (\n ts.isImportEqualsDeclaration(node) &&\n ts.isExternalModuleReference(node.moduleReference)\n ) {\n add(node.moduleReference.expression);\n } else if (ts.isCallExpression(node)) {\n const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;\n const isRequire =\n ts.isIdentifier(node.expression) && node.expression.text === 'require';\n if (isDynamicImport || isRequire) add(node.arguments[0]);\n } else if (ts.isImportTypeNode(node)) {\n const argument = node.argument;\n if (ts.isLiteralTypeNode(argument)) add(argument.literal);\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n return names;\n }\n\n private readFile(absPath: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readFileSync(absPath, 'utf-8');\n } catch (err: unknown) {\n const error = toError(err);\n // An unreadable file cannot change the classification of a package; skipping it\n // only ever makes the scan MORE conservative (fewer test-only classifications).\n console.warn(`Could not read ${absPath} while classifying deps: ${error.message}`);\n return null;\n }\n }\n}\n"]}