@webpieces/nx-webpieces-rules 0.4.833 → 0.4.835
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 +8 -8
- package/src/lib/api-usage/api-lib-tag-validator.d.ts +4 -3
- package/src/lib/api-usage/api-lib-tag-validator.js +98 -15
- package/src/lib/api-usage/api-lib-tag-validator.js.map +1 -1
- package/src/lib/dep-usage-scanner.d.ts +6 -2
- package/src/lib/dep-usage-scanner.js +45 -28
- package/src/lib/dep-usage-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.835",
|
|
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.
|
|
22
|
-
"@webpieces/api-doc-model": "0.4.
|
|
23
|
-
"@webpieces/code-rules": "0.4.
|
|
24
|
-
"@webpieces/core-util": "0.4.
|
|
25
|
-
"@webpieces/eslint-rules": "0.4.
|
|
26
|
-
"@webpieces/pr-gate": "0.4.
|
|
27
|
-
"@webpieces/rules-config": "0.4.
|
|
21
|
+
"@webpieces/ai-hook-rules": "0.4.835",
|
|
22
|
+
"@webpieces/api-doc-model": "0.4.835",
|
|
23
|
+
"@webpieces/code-rules": "0.4.835",
|
|
24
|
+
"@webpieces/core-util": "0.4.835",
|
|
25
|
+
"@webpieces/eslint-rules": "0.4.835",
|
|
26
|
+
"@webpieces/pr-gate": "0.4.835",
|
|
27
|
+
"@webpieces/rules-config": "0.4.835",
|
|
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
|
|
12
|
-
* - a project tagged `role:api-client` MUST export a contract
|
|
13
|
-
* `
|
|
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
|
|
13
|
-
* - a project tagged `role:api-client` MUST export a contract
|
|
14
|
-
* `
|
|
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)
|
|
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
|
-
|
|
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) =>
|
|
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 &&
|
|
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
|
|
206
|
-
`
|
|
207
|
-
`
|
|
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":"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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, 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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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 =
|
|
155
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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"]}
|