@supacloud/compiler 0.3.1 → 0.4.1
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/dist/analyze.d.ts +3 -3
- package/dist/cli.js +1698 -0
- package/dist/compile.d.ts +8 -3
- package/dist/generate.d.ts +8 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +377 -7
- package/dist/profiles.d.ts +36 -0
- package/dist/types.d.ts +79 -25
- package/dist/util.d.ts +7 -7
- package/dist/validate.d.ts +4 -7
- package/package.json +5 -2
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1698 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/analyze.ts
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { join, relative, sep } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
Node,
|
|
11
|
+
Project,
|
|
12
|
+
SyntaxKind
|
|
13
|
+
} from "ts-morph";
|
|
14
|
+
var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
|
|
15
|
+
var ROUTE_DECORATORS = {
|
|
16
|
+
Get: "GET",
|
|
17
|
+
Post: "POST",
|
|
18
|
+
Put: "PUT",
|
|
19
|
+
Patch: "PATCH",
|
|
20
|
+
Delete: "DELETE",
|
|
21
|
+
Head: "HEAD",
|
|
22
|
+
Options: "OPTIONS"
|
|
23
|
+
};
|
|
24
|
+
var SCOPES = ["application", "request", "job"];
|
|
25
|
+
async function analyzeProject(rootDir, include) {
|
|
26
|
+
const project = createProject(rootDir);
|
|
27
|
+
const patterns = (include ?? DEFAULT_INCLUDE).map((glob) => join(rootDir, glob));
|
|
28
|
+
project.addSourceFilesAtPaths(patterns);
|
|
29
|
+
const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules") && !sf.isDeclarationFile()).sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
|
|
30
|
+
const ctx = {
|
|
31
|
+
rootDir,
|
|
32
|
+
tokensByName: new Map,
|
|
33
|
+
classesByName: new Map,
|
|
34
|
+
diagnostics: []
|
|
35
|
+
};
|
|
36
|
+
for (const sf of sourceFiles) {
|
|
37
|
+
indexFile(sf, ctx);
|
|
38
|
+
}
|
|
39
|
+
const candidates = [];
|
|
40
|
+
for (const sf of sourceFiles) {
|
|
41
|
+
for (const cls of sf.getClasses()) {
|
|
42
|
+
const moduleDec = findDecorator(cls, "Module");
|
|
43
|
+
if (!moduleDec)
|
|
44
|
+
continue;
|
|
45
|
+
const options = decoratorObjectArg(moduleDec);
|
|
46
|
+
if (!options)
|
|
47
|
+
continue;
|
|
48
|
+
candidates.push({
|
|
49
|
+
node: cls,
|
|
50
|
+
options,
|
|
51
|
+
className: cls.getName() ?? "<anonymous>",
|
|
52
|
+
file: sf.getFilePath(),
|
|
53
|
+
line: cls.getStartLineNumber()
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
57
|
+
if (call.getExpression().getText() !== "defineModule")
|
|
58
|
+
continue;
|
|
59
|
+
const parent = call.getParent();
|
|
60
|
+
if (!parent || !Node.isVariableDeclaration(parent))
|
|
61
|
+
continue;
|
|
62
|
+
const arg = call.getArguments()[0];
|
|
63
|
+
if (!arg || !Node.isObjectLiteralExpression(arg))
|
|
64
|
+
continue;
|
|
65
|
+
candidates.push({
|
|
66
|
+
node: parent,
|
|
67
|
+
options: arg,
|
|
68
|
+
className: parent.getName(),
|
|
69
|
+
file: sf.getFilePath(),
|
|
70
|
+
line: parent.getStartLineNumber()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const nameByNode = new Map;
|
|
75
|
+
for (const c of candidates) {
|
|
76
|
+
nameByNode.set(c.node, stringLiteralProp(c.options, "name") ?? c.className);
|
|
77
|
+
}
|
|
78
|
+
const modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
|
|
79
|
+
const providedTokens = new Set(modules.flatMap((m) => m.providers.map((p) => p.token)));
|
|
80
|
+
const referenced = new Set;
|
|
81
|
+
for (const m of modules) {
|
|
82
|
+
for (const p of m.providers)
|
|
83
|
+
p.deps.forEach((d) => referenced.add(d));
|
|
84
|
+
for (const c of m.controllers)
|
|
85
|
+
c.deps.forEach((d) => referenced.add(d));
|
|
86
|
+
}
|
|
87
|
+
const externalTokens = [...referenced].filter((token) => !providedTokens.has(token)).sort();
|
|
88
|
+
const tokenNames = {};
|
|
89
|
+
for (const info of ctx.tokensByName.values()) {
|
|
90
|
+
if (info.stringName)
|
|
91
|
+
tokenNames[info.name] = info.stringName;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
modules,
|
|
95
|
+
externalTokens,
|
|
96
|
+
diagnostics: ctx.diagnostics,
|
|
97
|
+
tokenNames
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function createProject(rootDir) {
|
|
101
|
+
const tsConfigFilePath = join(rootDir, "tsconfig.json");
|
|
102
|
+
if (existsSync(tsConfigFilePath)) {
|
|
103
|
+
return new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true });
|
|
104
|
+
}
|
|
105
|
+
return new Project({
|
|
106
|
+
compilerOptions: { experimentalDecorators: true, allowJs: false }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function indexFile(sf, ctx) {
|
|
110
|
+
for (const cls of sf.getClasses()) {
|
|
111
|
+
const name = cls.getName();
|
|
112
|
+
if (name && !ctx.classesByName.has(name)) {
|
|
113
|
+
ctx.classesByName.set(name, { name, decl: cls, file: sf.getFilePath() });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const statement of sf.getVariableStatements()) {
|
|
117
|
+
for (const decl of statement.getDeclarations()) {
|
|
118
|
+
const info = parseTokenVariable(decl, sf.getFilePath());
|
|
119
|
+
if (info && !ctx.tokensByName.has(info.name)) {
|
|
120
|
+
ctx.tokensByName.set(info.name, info);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function parseTokenVariable(decl, file) {
|
|
126
|
+
const init = decl.getInitializer();
|
|
127
|
+
if (!init || !Node.isNewExpression(init))
|
|
128
|
+
return;
|
|
129
|
+
if (init.getExpression().getText() !== "InjectionToken")
|
|
130
|
+
return;
|
|
131
|
+
const [nameArg, optionsArg] = init.getArguments();
|
|
132
|
+
const info = { name: decl.getName(), file };
|
|
133
|
+
if (nameArg && Node.isStringLiteral(nameArg)) {
|
|
134
|
+
info.stringName = nameArg.getLiteralText();
|
|
135
|
+
}
|
|
136
|
+
if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
|
|
137
|
+
const scope = stringLiteralProp(optionsArg, "scope");
|
|
138
|
+
if (scope && SCOPES.includes(scope)) {
|
|
139
|
+
info.scope = scope;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return info;
|
|
143
|
+
}
|
|
144
|
+
function parseModule(candidate, nameByNode, ctx) {
|
|
145
|
+
const { options, className, file, line } = candidate;
|
|
146
|
+
const name = nameByNode.get(candidate.node) ?? className;
|
|
147
|
+
const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
|
|
148
|
+
const imports = arrayProp(options, "imports").map((el) => {
|
|
149
|
+
const decl = Node.isIdentifier(el) ? resolveDeclaration(el)[0] : undefined;
|
|
150
|
+
if (decl) {
|
|
151
|
+
const known = nameByNode.get(decl);
|
|
152
|
+
if (known)
|
|
153
|
+
return known;
|
|
154
|
+
if (Node.isClassDeclaration(decl)) {
|
|
155
|
+
const dec = findDecorator(decl, "Module");
|
|
156
|
+
const decOptions = dec && decoratorObjectArg(dec);
|
|
157
|
+
const decName = decOptions && stringLiteralProp(decOptions, "name");
|
|
158
|
+
return decName ?? decl.getName() ?? el.getText();
|
|
159
|
+
}
|
|
160
|
+
if (Node.isVariableDeclaration(decl))
|
|
161
|
+
return decl.getName();
|
|
162
|
+
}
|
|
163
|
+
return el.getText();
|
|
164
|
+
}).filter((v, i, arr) => arr.indexOf(v) === i);
|
|
165
|
+
const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
|
|
166
|
+
const exportsSet = new Set(exports);
|
|
167
|
+
const providers = [];
|
|
168
|
+
for (const el of arrayProp(options, "providers")) {
|
|
169
|
+
const provider = parseProvider(el, exportsSet, ctx);
|
|
170
|
+
if (provider)
|
|
171
|
+
providers.push(provider);
|
|
172
|
+
}
|
|
173
|
+
const controllers = [];
|
|
174
|
+
for (const el of arrayProp(options, "controllers")) {
|
|
175
|
+
const controller = parseController(el, ctx);
|
|
176
|
+
if (controller)
|
|
177
|
+
controllers.push(controller);
|
|
178
|
+
}
|
|
179
|
+
const handlerClasses = [];
|
|
180
|
+
const seenHandlers = new Set;
|
|
181
|
+
const collectHandler = (expr) => {
|
|
182
|
+
if (!Node.isIdentifier(expr))
|
|
183
|
+
return;
|
|
184
|
+
const decl = resolveDeclaration(expr)[0];
|
|
185
|
+
if (decl && Node.isClassDeclaration(decl) && !seenHandlers.has(decl.getName() ?? "")) {
|
|
186
|
+
seenHandlers.add(decl.getName() ?? "");
|
|
187
|
+
handlerClasses.push(decl);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
for (const el of arrayProp(options, "providers")) {
|
|
191
|
+
if (Node.isIdentifier(el))
|
|
192
|
+
collectHandler(el);
|
|
193
|
+
if (Node.isObjectLiteralExpression(el)) {
|
|
194
|
+
const useClass = getProp(el, "useClass");
|
|
195
|
+
if (useClass)
|
|
196
|
+
collectHandler(useClass);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
arrayProp(options, "commands").forEach(collectHandler);
|
|
200
|
+
arrayProp(options, "queries").forEach(collectHandler);
|
|
201
|
+
const commands = [];
|
|
202
|
+
const queries = [];
|
|
203
|
+
for (const cls of handlerClasses) {
|
|
204
|
+
const commandDec = findDecorator(cls, "Command");
|
|
205
|
+
if (commandDec) {
|
|
206
|
+
const meta = decoratorObjectArg(commandDec);
|
|
207
|
+
if (meta) {
|
|
208
|
+
commands.push({
|
|
209
|
+
className: cls.getName() ?? "<anonymous>",
|
|
210
|
+
name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
|
|
211
|
+
permission: stringLiteralProp(meta, "permission"),
|
|
212
|
+
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
213
|
+
audit: stringLiteralProp(meta, "audit"),
|
|
214
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none"
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const queryDec = findDecorator(cls, "Query");
|
|
219
|
+
if (queryDec) {
|
|
220
|
+
const meta = decoratorObjectArg(queryDec);
|
|
221
|
+
if (meta) {
|
|
222
|
+
queries.push({
|
|
223
|
+
className: cls.getName() ?? "<anonymous>",
|
|
224
|
+
name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>"
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
name,
|
|
231
|
+
className,
|
|
232
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
233
|
+
file: sourcePath(ctx.rootDir, file),
|
|
234
|
+
line,
|
|
235
|
+
imports,
|
|
236
|
+
providers,
|
|
237
|
+
controllers,
|
|
238
|
+
commands,
|
|
239
|
+
queries,
|
|
240
|
+
exports
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function commandModeProp(object, name) {
|
|
244
|
+
const value = stringLiteralProp(object, name);
|
|
245
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
246
|
+
}
|
|
247
|
+
function parseProvider(el, exportsSet, ctx) {
|
|
248
|
+
const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
|
|
249
|
+
const line = el.getStartLineNumber();
|
|
250
|
+
if (Node.isIdentifier(el)) {
|
|
251
|
+
const decl = resolveDeclaration(el)[0];
|
|
252
|
+
const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
|
|
253
|
+
const className = cls?.getName() ?? el.getText();
|
|
254
|
+
const { deps, missing } = cls ? classDeps(cls, ctx) : { deps: [], missing: false };
|
|
255
|
+
if (missing) {
|
|
256
|
+
warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
token: className,
|
|
260
|
+
tokenKind: "class",
|
|
261
|
+
kind: "class",
|
|
262
|
+
useClass: className,
|
|
263
|
+
scope: resolveScope({ cls, tokenName: className }, ctx),
|
|
264
|
+
deps,
|
|
265
|
+
exported: exportsSet.has(className),
|
|
266
|
+
file,
|
|
267
|
+
line,
|
|
268
|
+
importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (!Node.isObjectLiteralExpression(el))
|
|
272
|
+
return;
|
|
273
|
+
const provideExpr = getProp(el, "provide");
|
|
274
|
+
if (!provideExpr)
|
|
275
|
+
return;
|
|
276
|
+
const { name: token, kind: tokenKind } = tokenNameOf(provideExpr, ctx);
|
|
277
|
+
const explicitScope = parseScopeProp(el);
|
|
278
|
+
const explicitDeps = arrayProp(el, "deps").map((d) => tokenNameOf(d, ctx).name);
|
|
279
|
+
const useClassExpr = getProp(el, "useClass");
|
|
280
|
+
const useValueExpr = getProp(el, "useValue");
|
|
281
|
+
const useFactoryExpr = getProp(el, "useFactory");
|
|
282
|
+
const useExistingExpr = getProp(el, "useExisting");
|
|
283
|
+
if (useClassExpr) {
|
|
284
|
+
const decl = Node.isIdentifier(useClassExpr) ? resolveDeclaration(useClassExpr)[0] : undefined;
|
|
285
|
+
const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
|
|
286
|
+
const useClass = cls?.getName() ?? useClassExpr.getText();
|
|
287
|
+
let deps = explicitDeps;
|
|
288
|
+
if (deps.length === 0 && cls) {
|
|
289
|
+
const result = classDeps(cls, ctx);
|
|
290
|
+
deps = result.deps;
|
|
291
|
+
if (result.missing) {
|
|
292
|
+
warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
token,
|
|
297
|
+
tokenKind,
|
|
298
|
+
kind: "class",
|
|
299
|
+
useClass,
|
|
300
|
+
scope: resolveScope({ explicit: explicitScope, cls, tokenName: token }, ctx),
|
|
301
|
+
deps,
|
|
302
|
+
exported: exportsSet.has(token),
|
|
303
|
+
file,
|
|
304
|
+
line,
|
|
305
|
+
importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (useValueExpr) {
|
|
309
|
+
return {
|
|
310
|
+
token,
|
|
311
|
+
tokenKind,
|
|
312
|
+
kind: "value",
|
|
313
|
+
useValueExpr: useValueExpr.getText(),
|
|
314
|
+
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
315
|
+
deps: [],
|
|
316
|
+
exported: exportsSet.has(token),
|
|
317
|
+
file,
|
|
318
|
+
line,
|
|
319
|
+
importPath: Node.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (useFactoryExpr) {
|
|
323
|
+
const factoryName = Node.isIdentifier(useFactoryExpr) ? (() => {
|
|
324
|
+
const decl = resolveDeclaration(useFactoryExpr)[0];
|
|
325
|
+
return decl && (Node.isFunctionDeclaration(decl) || Node.isVariableDeclaration(decl)) ? decl.getName() ?? useFactoryExpr.getText() : useFactoryExpr.getText();
|
|
326
|
+
})() : useFactoryExpr.getText();
|
|
327
|
+
return {
|
|
328
|
+
token,
|
|
329
|
+
tokenKind,
|
|
330
|
+
kind: "factory",
|
|
331
|
+
useFactoryName: factoryName,
|
|
332
|
+
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
333
|
+
deps: explicitDeps,
|
|
334
|
+
exported: exportsSet.has(token),
|
|
335
|
+
file,
|
|
336
|
+
line,
|
|
337
|
+
importPath: Node.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (useExistingExpr) {
|
|
341
|
+
const target = tokenNameOf(useExistingExpr, ctx).name;
|
|
342
|
+
return {
|
|
343
|
+
token,
|
|
344
|
+
tokenKind,
|
|
345
|
+
kind: "existing",
|
|
346
|
+
useExisting: target,
|
|
347
|
+
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
348
|
+
deps: [target],
|
|
349
|
+
exported: exportsSet.has(token),
|
|
350
|
+
file,
|
|
351
|
+
line
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
function parseController(el, ctx) {
|
|
357
|
+
if (!Node.isIdentifier(el))
|
|
358
|
+
return;
|
|
359
|
+
const decl = resolveDeclaration(el)[0];
|
|
360
|
+
if (!decl || !Node.isClassDeclaration(decl))
|
|
361
|
+
return;
|
|
362
|
+
const controllerDec = findDecorator(decl, "Controller");
|
|
363
|
+
if (!controllerDec)
|
|
364
|
+
return;
|
|
365
|
+
const pathArg = controllerDec.getArguments()[0];
|
|
366
|
+
const path = pathArg && Node.isStringLiteral(pathArg) ? pathArg.getLiteralText() : "/";
|
|
367
|
+
const { deps, missing } = classDeps(decl, ctx);
|
|
368
|
+
const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
|
|
369
|
+
if (missing) {
|
|
370
|
+
warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
|
|
371
|
+
}
|
|
372
|
+
const injectable = parseInjectableOptions(decl, ctx);
|
|
373
|
+
const routes = [];
|
|
374
|
+
const schemaImports = {};
|
|
375
|
+
for (const method of decl.getMethods()) {
|
|
376
|
+
for (const dec of method.getDecorators()) {
|
|
377
|
+
const name = decoratorName(dec);
|
|
378
|
+
const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
|
|
379
|
+
if (!httpMethod)
|
|
380
|
+
continue;
|
|
381
|
+
const args = dec.getArguments();
|
|
382
|
+
const pathArg2 = args[0];
|
|
383
|
+
const route = {
|
|
384
|
+
method: httpMethod,
|
|
385
|
+
path: pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/",
|
|
386
|
+
handler: method.getName()
|
|
387
|
+
};
|
|
388
|
+
const optionsArg = args[1];
|
|
389
|
+
if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
|
|
390
|
+
for (const field of ["body", "params", "query", "response"]) {
|
|
391
|
+
const schemaExpr = getProp(optionsArg, field);
|
|
392
|
+
if (schemaExpr && Node.isIdentifier(schemaExpr)) {
|
|
393
|
+
route[field] = schemaExpr.getText();
|
|
394
|
+
const importPath = importPathOf(schemaExpr, ctx);
|
|
395
|
+
if (importPath)
|
|
396
|
+
schemaImports[schemaExpr.getText()] = importPath;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const commandExpr = getProp(optionsArg, "command");
|
|
400
|
+
if (commandExpr && Node.isIdentifier(commandExpr)) {
|
|
401
|
+
const commandDecl = resolveDeclaration(commandExpr)[0];
|
|
402
|
+
route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
routes.push(route);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
className: decl.getName() ?? "<anonymous>",
|
|
410
|
+
path,
|
|
411
|
+
scope: injectable?.scope ?? "request",
|
|
412
|
+
deps,
|
|
413
|
+
routes,
|
|
414
|
+
file,
|
|
415
|
+
importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
|
|
416
|
+
schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function classDeps(cls, ctx) {
|
|
420
|
+
const injectable = parseInjectableOptions(cls, ctx);
|
|
421
|
+
if (injectable?.deps)
|
|
422
|
+
return { deps: injectable.deps, missing: false };
|
|
423
|
+
const ctor = cls.getConstructors()[0];
|
|
424
|
+
if (!ctor || ctor.getParameters().length === 0)
|
|
425
|
+
return { deps: [], missing: false };
|
|
426
|
+
const injectParams = parseInjectParams(cls);
|
|
427
|
+
const deps = [];
|
|
428
|
+
let missing = false;
|
|
429
|
+
ctor.getParameters().forEach((param, index) => {
|
|
430
|
+
const injected = injectParams.get(index);
|
|
431
|
+
if (injected) {
|
|
432
|
+
deps.push(injected);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const byType = paramTypeTokenName(param, ctx);
|
|
436
|
+
if (byType) {
|
|
437
|
+
deps.push(byType);
|
|
438
|
+
} else {
|
|
439
|
+
missing = true;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
return { deps, missing };
|
|
443
|
+
}
|
|
444
|
+
function paramTypeTokenName(param, ctx) {
|
|
445
|
+
const typeNode = param.getTypeNode();
|
|
446
|
+
if (!typeNode)
|
|
447
|
+
return;
|
|
448
|
+
const text = typeNode.getText().replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
|
|
449
|
+
if (ctx.classesByName.has(text))
|
|
450
|
+
return text;
|
|
451
|
+
if (ctx.tokensByName.has(text))
|
|
452
|
+
return text;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
function parseInjectableOptions(cls, ctx) {
|
|
456
|
+
const dec = findDecorator(cls, "Injectable");
|
|
457
|
+
if (!dec)
|
|
458
|
+
return;
|
|
459
|
+
const obj = decoratorObjectArg(dec);
|
|
460
|
+
if (!obj)
|
|
461
|
+
return {};
|
|
462
|
+
const scope = stringLiteralProp(obj, "scope");
|
|
463
|
+
const depsExpr = getProp(obj, "deps");
|
|
464
|
+
return {
|
|
465
|
+
scope: scope && SCOPES.includes(scope) ? scope : undefined,
|
|
466
|
+
deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function parseInjectParams(cls) {
|
|
470
|
+
const result = new Map;
|
|
471
|
+
const ctor = cls.getConstructors()[0];
|
|
472
|
+
if (!ctor)
|
|
473
|
+
return result;
|
|
474
|
+
ctor.getParameters().forEach((param, index) => {
|
|
475
|
+
for (const dec of param.getDecorators()) {
|
|
476
|
+
if (decoratorName(dec) !== "Inject")
|
|
477
|
+
continue;
|
|
478
|
+
const arg = dec.getArguments()[0];
|
|
479
|
+
if (arg)
|
|
480
|
+
result.set(index, tokenText(arg));
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
function tokenText(expr) {
|
|
486
|
+
if (Node.isIdentifier(expr)) {
|
|
487
|
+
const decl = resolveDeclaration(expr)[0];
|
|
488
|
+
if (decl && Node.isClassDeclaration(decl))
|
|
489
|
+
return decl.getName() ?? expr.getText();
|
|
490
|
+
if (decl && Node.isVariableDeclaration(decl))
|
|
491
|
+
return decl.getName();
|
|
492
|
+
}
|
|
493
|
+
return expr.getText();
|
|
494
|
+
}
|
|
495
|
+
function resolveScope(input, ctx) {
|
|
496
|
+
if (input.explicit)
|
|
497
|
+
return input.explicit;
|
|
498
|
+
if (input.cls) {
|
|
499
|
+
const injectable = parseInjectableOptions(input.cls, ctx);
|
|
500
|
+
if (injectable?.scope)
|
|
501
|
+
return injectable.scope;
|
|
502
|
+
}
|
|
503
|
+
const token = ctx.tokensByName.get(input.tokenName);
|
|
504
|
+
if (token?.scope)
|
|
505
|
+
return token.scope;
|
|
506
|
+
return "application";
|
|
507
|
+
}
|
|
508
|
+
function tokenNameOf(expr, ctx) {
|
|
509
|
+
if (Node.isIdentifier(expr)) {
|
|
510
|
+
const decl = resolveDeclaration(expr)[0];
|
|
511
|
+
if (decl && Node.isClassDeclaration(decl)) {
|
|
512
|
+
return { name: decl.getName() ?? expr.getText(), kind: "class" };
|
|
513
|
+
}
|
|
514
|
+
if (decl && Node.isVariableDeclaration(decl)) {
|
|
515
|
+
const name = decl.getName();
|
|
516
|
+
return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
|
|
517
|
+
}
|
|
518
|
+
if (ctx.tokensByName.has(expr.getText())) {
|
|
519
|
+
return { name: expr.getText(), kind: "injection-token" };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return { name: expr.getText(), kind: "class" };
|
|
523
|
+
}
|
|
524
|
+
function resolveDeclaration(id) {
|
|
525
|
+
let symbol = id.getSymbol();
|
|
526
|
+
if (!symbol)
|
|
527
|
+
return [];
|
|
528
|
+
let declarations = symbol.getDeclarations();
|
|
529
|
+
for (let guard = 0;guard < 4; guard += 1) {
|
|
530
|
+
const isAlias = declarations.some((d) => Node.isImportSpecifier(d) || Node.isImportClause(d) || Node.isNamespaceImport(d));
|
|
531
|
+
if (!isAlias)
|
|
532
|
+
break;
|
|
533
|
+
const aliased = symbol.getAliasedSymbol();
|
|
534
|
+
if (!aliased)
|
|
535
|
+
break;
|
|
536
|
+
symbol = aliased;
|
|
537
|
+
declarations = aliased.getDeclarations();
|
|
538
|
+
}
|
|
539
|
+
return declarations;
|
|
540
|
+
}
|
|
541
|
+
function importPathOf(id, ctx) {
|
|
542
|
+
const symbol = id.getSymbol();
|
|
543
|
+
const first = symbol?.getDeclarations()[0];
|
|
544
|
+
if (first && (Node.isImportSpecifier(first) || Node.isImportClause(first))) {
|
|
545
|
+
const importDecl = first.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
|
|
546
|
+
const target = importDecl?.getModuleSpecifierSourceFile();
|
|
547
|
+
if (target)
|
|
548
|
+
return modulePath(ctx.rootDir, target.getFilePath());
|
|
549
|
+
}
|
|
550
|
+
const decl = resolveDeclaration(id)[0];
|
|
551
|
+
if (decl)
|
|
552
|
+
return modulePath(ctx.rootDir, decl.getSourceFile().getFilePath());
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
function findDecorator(cls, name) {
|
|
556
|
+
return cls.getDecorators().find((dec) => decoratorName(dec) === name);
|
|
557
|
+
}
|
|
558
|
+
function decoratorName(dec) {
|
|
559
|
+
const expr = dec.getExpression();
|
|
560
|
+
if (Node.isCallExpression(expr)) {
|
|
561
|
+
return expr.getExpression().getText().split(".").pop();
|
|
562
|
+
}
|
|
563
|
+
if (Node.isIdentifier(expr))
|
|
564
|
+
return expr.getText();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
function decoratorObjectArg(dec) {
|
|
568
|
+
const expr = dec.getExpression();
|
|
569
|
+
if (!Node.isCallExpression(expr))
|
|
570
|
+
return;
|
|
571
|
+
const arg = expr.getArguments()[0];
|
|
572
|
+
return arg && Node.isObjectLiteralExpression(arg) ? arg : undefined;
|
|
573
|
+
}
|
|
574
|
+
function getProp(obj, name) {
|
|
575
|
+
const prop = obj.getProperty(name);
|
|
576
|
+
if (prop && Node.isPropertyAssignment(prop))
|
|
577
|
+
return prop.getInitializer();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
function stringLiteralProp(obj, name) {
|
|
581
|
+
const expr = getProp(obj, name);
|
|
582
|
+
return expr && Node.isStringLiteral(expr) ? expr.getLiteralText() : undefined;
|
|
583
|
+
}
|
|
584
|
+
function arrayProp(obj, name) {
|
|
585
|
+
const expr = getProp(obj, name);
|
|
586
|
+
return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
|
|
587
|
+
}
|
|
588
|
+
function parseScopeProp(obj) {
|
|
589
|
+
const scope = stringLiteralProp(obj, "scope");
|
|
590
|
+
return scope && SCOPES.includes(scope) ? scope : undefined;
|
|
591
|
+
}
|
|
592
|
+
function modulePath(rootDir, absFile) {
|
|
593
|
+
return sourcePath(rootDir, absFile).replace(/\.(ts|tsx|js|mts|cts)$/, "");
|
|
594
|
+
}
|
|
595
|
+
function sourcePath(rootDir, absFile) {
|
|
596
|
+
return relative(rootDir, absFile).split(sep).join("/");
|
|
597
|
+
}
|
|
598
|
+
function warn(ctx, code, message, file, line) {
|
|
599
|
+
ctx.diagnostics.push({ severity: "warn", code, message, file, line });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/generate.ts
|
|
603
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
604
|
+
import { join as join2 } from "node:path";
|
|
605
|
+
|
|
606
|
+
// src/util.ts
|
|
607
|
+
function camelName(token) {
|
|
608
|
+
const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
|
|
609
|
+
if (isConstantCase) {
|
|
610
|
+
return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
611
|
+
}
|
|
612
|
+
return token.charAt(0).toLowerCase() + token.slice(1);
|
|
613
|
+
}
|
|
614
|
+
function relativeImportPath(fromDir, toFile) {
|
|
615
|
+
const fromParts = fromDir.split("/").filter(Boolean);
|
|
616
|
+
const toParts = toFile.split("/").filter(Boolean);
|
|
617
|
+
let common = 0;
|
|
618
|
+
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
619
|
+
common += 1;
|
|
620
|
+
}
|
|
621
|
+
const ups = fromParts.length - common;
|
|
622
|
+
const downs = toParts.slice(common);
|
|
623
|
+
const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
|
|
624
|
+
const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
|
|
625
|
+
const joined = segments.join("/");
|
|
626
|
+
return joined.startsWith("..") ? joined : `./${joined}`;
|
|
627
|
+
}
|
|
628
|
+
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
|
|
629
|
+
var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
630
|
+
function isRequestContextToken(token, tokenNames) {
|
|
631
|
+
return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
|
|
632
|
+
}
|
|
633
|
+
function isJobContextToken(token, tokenNames) {
|
|
634
|
+
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/generate.ts
|
|
638
|
+
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
639
|
+
var INTERFACES = `export interface CompiledRoute {
|
|
640
|
+
method: string;
|
|
641
|
+
path: string;
|
|
642
|
+
handler: string;
|
|
643
|
+
body?: unknown;
|
|
644
|
+
params?: unknown;
|
|
645
|
+
query?: unknown;
|
|
646
|
+
response?: unknown;
|
|
647
|
+
command?: string;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export interface CompiledCommand {
|
|
651
|
+
className: string;
|
|
652
|
+
name: string;
|
|
653
|
+
permission: string;
|
|
654
|
+
transaction: "required" | "none";
|
|
655
|
+
audit?: string;
|
|
656
|
+
idempotency: "required" | "none";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export interface CompiledController {
|
|
660
|
+
path: string;
|
|
661
|
+
serviceKey: string;
|
|
662
|
+
scope: string;
|
|
663
|
+
routes: CompiledRoute[];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export interface CompiledModule {
|
|
667
|
+
name: string;
|
|
668
|
+
createServices(
|
|
669
|
+
deps: Record<string, unknown>,
|
|
670
|
+
imported: Record<string, Record<string, unknown>>,
|
|
671
|
+
): Record<string, unknown>;
|
|
672
|
+
createRequestScope?(
|
|
673
|
+
services: Record<string, unknown>,
|
|
674
|
+
ctx: unknown,
|
|
675
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
676
|
+
): Record<string, unknown>;
|
|
677
|
+
createJobScope?(
|
|
678
|
+
services: Record<string, unknown>,
|
|
679
|
+
ctx: unknown,
|
|
680
|
+
imported?: Record<string, Record<string, unknown>>,
|
|
681
|
+
): Record<string, unknown>;
|
|
682
|
+
controllers: CompiledController[];
|
|
683
|
+
commands: CompiledCommand[];
|
|
684
|
+
}`;
|
|
685
|
+
function renderApplication(graph, options) {
|
|
686
|
+
const modules = topoSortModules(graph.modules);
|
|
687
|
+
const imports = new ImportManager;
|
|
688
|
+
const factorySections = [];
|
|
689
|
+
const descriptorEntries = [];
|
|
690
|
+
for (const module of modules) {
|
|
691
|
+
const gen = new ModuleGenerator(graph, module, imports);
|
|
692
|
+
factorySections.push(...gen.renderFactories());
|
|
693
|
+
descriptorEntries.push(gen.renderDescriptor());
|
|
694
|
+
}
|
|
695
|
+
const code = [
|
|
696
|
+
HEADER,
|
|
697
|
+
"",
|
|
698
|
+
...imports.render(options.rootDir, options.outDir),
|
|
699
|
+
...imports.size > 0 ? [""] : [],
|
|
700
|
+
INTERFACES,
|
|
701
|
+
"",
|
|
702
|
+
"export function createCompiledModules(): CompiledModule[] {",
|
|
703
|
+
" return [",
|
|
704
|
+
...descriptorEntries.map((entry) => indent(entry, 4) + ","),
|
|
705
|
+
" ];",
|
|
706
|
+
"}",
|
|
707
|
+
"",
|
|
708
|
+
...factorySections,
|
|
709
|
+
""
|
|
710
|
+
].join(`
|
|
711
|
+
`);
|
|
712
|
+
const manifest = {
|
|
713
|
+
version: 1,
|
|
714
|
+
modules: graph.modules,
|
|
715
|
+
externalTokens: graph.externalTokens
|
|
716
|
+
};
|
|
717
|
+
return {
|
|
718
|
+
applicationCode: code,
|
|
719
|
+
manifestJson: JSON.stringify(manifest, null, 2) + `
|
|
720
|
+
`
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function generateApplication(graph, options) {
|
|
724
|
+
const rendered = renderApplication(graph, options);
|
|
725
|
+
await mkdir(options.outDir, { recursive: true });
|
|
726
|
+
const applicationPath = join2(options.outDir, "application.ts");
|
|
727
|
+
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
728
|
+
await writeFile(applicationPath, rendered.applicationCode, "utf8");
|
|
729
|
+
await writeFile(manifestPath, rendered.manifestJson, "utf8");
|
|
730
|
+
return [applicationPath, manifestPath];
|
|
731
|
+
}
|
|
732
|
+
function factoryOfScope(scope) {
|
|
733
|
+
return scope === "application" ? "services" : scope;
|
|
734
|
+
}
|
|
735
|
+
function topoSortModules(modules) {
|
|
736
|
+
const byName = new Map(modules.map((m) => [m.name, m]));
|
|
737
|
+
const visited = new Set;
|
|
738
|
+
const result = [];
|
|
739
|
+
const visit = (module) => {
|
|
740
|
+
if (visited.has(module.name))
|
|
741
|
+
return;
|
|
742
|
+
visited.add(module.name);
|
|
743
|
+
for (const importName of module.imports) {
|
|
744
|
+
const dep = byName.get(importName);
|
|
745
|
+
if (dep && dep !== module)
|
|
746
|
+
visit(dep);
|
|
747
|
+
}
|
|
748
|
+
result.push(module);
|
|
749
|
+
};
|
|
750
|
+
for (const module of modules)
|
|
751
|
+
visit(module);
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
function pascalName(name) {
|
|
755
|
+
const joined = name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
756
|
+
return joined || "App";
|
|
757
|
+
}
|
|
758
|
+
function indent(text, spaces) {
|
|
759
|
+
const pad = " ".repeat(spaces);
|
|
760
|
+
return text.split(`
|
|
761
|
+
`).map((line) => line.length > 0 ? pad + line : line).join(`
|
|
762
|
+
`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
class ImportManager {
|
|
766
|
+
entries = new Map;
|
|
767
|
+
get size() {
|
|
768
|
+
return this.entries.size;
|
|
769
|
+
}
|
|
770
|
+
add(exported, importPath) {
|
|
771
|
+
if (!importPath)
|
|
772
|
+
return exported;
|
|
773
|
+
for (const [local2, entry] of this.entries) {
|
|
774
|
+
if (entry.path === importPath && entry.exported === exported)
|
|
775
|
+
return local2;
|
|
776
|
+
}
|
|
777
|
+
let local = exported;
|
|
778
|
+
let counter = 2;
|
|
779
|
+
while (this.entries.has(local)) {
|
|
780
|
+
local = `${exported}${counter}`;
|
|
781
|
+
counter += 1;
|
|
782
|
+
}
|
|
783
|
+
this.entries.set(local, { path: importPath, exported });
|
|
784
|
+
return local;
|
|
785
|
+
}
|
|
786
|
+
render(rootDir, outDir) {
|
|
787
|
+
const byPath = new Map;
|
|
788
|
+
for (const [local, entry] of this.entries) {
|
|
789
|
+
const list = byPath.get(entry.path) ?? [];
|
|
790
|
+
list.push({ exported: entry.exported, local });
|
|
791
|
+
byPath.set(entry.path, list);
|
|
792
|
+
}
|
|
793
|
+
return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, symbols]) => {
|
|
794
|
+
const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
|
|
795
|
+
const names = symbols.sort((a, b) => a.exported.localeCompare(b.exported)).map((s) => s.local === s.exported ? s.exported : `${s.exported} as ${s.local}`).join(", ");
|
|
796
|
+
return `import { ${names} } from "${spec}";`;
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
class ModuleGenerator {
|
|
802
|
+
graph;
|
|
803
|
+
module;
|
|
804
|
+
imports;
|
|
805
|
+
pascal;
|
|
806
|
+
locals = {
|
|
807
|
+
services: new Map,
|
|
808
|
+
request: new Map,
|
|
809
|
+
job: new Map
|
|
810
|
+
};
|
|
811
|
+
constructor(graph, module, imports) {
|
|
812
|
+
this.graph = graph;
|
|
813
|
+
this.module = module;
|
|
814
|
+
this.imports = imports;
|
|
815
|
+
this.pascal = pascalName(module.name);
|
|
816
|
+
}
|
|
817
|
+
renderFactories() {
|
|
818
|
+
const sections = [this.renderServicesFactory()];
|
|
819
|
+
if (this.hasFactoryContent("request")) {
|
|
820
|
+
sections.push(this.renderScopeFactory("request"));
|
|
821
|
+
}
|
|
822
|
+
if (this.hasFactoryContent("job")) {
|
|
823
|
+
sections.push(this.renderScopeFactory("job"));
|
|
824
|
+
}
|
|
825
|
+
return sections;
|
|
826
|
+
}
|
|
827
|
+
renderDescriptor() {
|
|
828
|
+
const lines = [
|
|
829
|
+
`{`,
|
|
830
|
+
` name: ${JSON.stringify(this.module.name)},`,
|
|
831
|
+
` createServices: create${this.pascal}Services,`
|
|
832
|
+
];
|
|
833
|
+
if (this.hasFactoryContent("request")) {
|
|
834
|
+
lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
|
|
835
|
+
}
|
|
836
|
+
if (this.hasFactoryContent("job")) {
|
|
837
|
+
lines.push(` createJobScope: create${this.pascal}JobScope,`);
|
|
838
|
+
}
|
|
839
|
+
lines.push(` controllers: ${this.renderControllers()},`);
|
|
840
|
+
lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
|
|
841
|
+
lines.push(`}`);
|
|
842
|
+
return lines.join(`
|
|
843
|
+
`);
|
|
844
|
+
}
|
|
845
|
+
hasFactoryContent(kind) {
|
|
846
|
+
return this.module.providers.some((p) => factoryOfScope(p.scope) === kind) || this.module.controllers.some((c) => factoryOfScope(c.scope) === kind);
|
|
847
|
+
}
|
|
848
|
+
renderControllers() {
|
|
849
|
+
if (this.module.controllers.length === 0)
|
|
850
|
+
return "[]";
|
|
851
|
+
const items = this.module.controllers.map((controller) => {
|
|
852
|
+
const routes = controller.routes.map((route) => {
|
|
853
|
+
const fields = [
|
|
854
|
+
`method: ${JSON.stringify(route.method)}`,
|
|
855
|
+
`path: ${JSON.stringify(route.path)}`,
|
|
856
|
+
`handler: ${JSON.stringify(route.handler)}`
|
|
857
|
+
];
|
|
858
|
+
for (const field of ["body", "params", "query", "response"]) {
|
|
859
|
+
const symbol = route[field];
|
|
860
|
+
if (symbol) {
|
|
861
|
+
const local = this.imports.add(symbol, controller.schemaImports?.[symbol]);
|
|
862
|
+
fields.push(`${field}: ${local}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (route.command)
|
|
866
|
+
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
867
|
+
return `{ ${fields.join(", ")} }`;
|
|
868
|
+
});
|
|
869
|
+
return [
|
|
870
|
+
`{`,
|
|
871
|
+
` path: ${JSON.stringify(controller.path)},`,
|
|
872
|
+
` serviceKey: ${JSON.stringify(camelName(controller.className))},`,
|
|
873
|
+
` scope: ${JSON.stringify(controller.scope)},`,
|
|
874
|
+
` routes: [${routes.join(", ")}],`,
|
|
875
|
+
`}`
|
|
876
|
+
].join(`
|
|
877
|
+
`);
|
|
878
|
+
});
|
|
879
|
+
return `[${items.map((item) => `
|
|
880
|
+
${indent(item, 2)}`).join(",")}
|
|
881
|
+
]`;
|
|
882
|
+
}
|
|
883
|
+
renderServicesFactory() {
|
|
884
|
+
return [
|
|
885
|
+
`function create${this.pascal}Services(`,
|
|
886
|
+
` deps: Record<string, unknown>,`,
|
|
887
|
+
` imported: Record<string, Record<string, unknown>>,`,
|
|
888
|
+
`): Record<string, unknown> {`,
|
|
889
|
+
indent(this.renderFactoryBody("services"), 2),
|
|
890
|
+
`}`
|
|
891
|
+
].join(`
|
|
892
|
+
`);
|
|
893
|
+
}
|
|
894
|
+
renderScopeFactory(kind) {
|
|
895
|
+
const suffix = kind === "request" ? "RequestScope" : "JobScope";
|
|
896
|
+
return [
|
|
897
|
+
`function create${this.pascal}${suffix}(`,
|
|
898
|
+
` services: Record<string, unknown>,`,
|
|
899
|
+
` ctx: unknown,`,
|
|
900
|
+
` imported: Record<string, Record<string, unknown>> = {},`,
|
|
901
|
+
`): Record<string, unknown> {`,
|
|
902
|
+
indent(this.renderFactoryBody(kind), 2),
|
|
903
|
+
`}`
|
|
904
|
+
].join(`
|
|
905
|
+
`);
|
|
906
|
+
}
|
|
907
|
+
renderFactoryBody(kind) {
|
|
908
|
+
const providers = orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind));
|
|
909
|
+
const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
|
|
910
|
+
const lines = [];
|
|
911
|
+
const returns = new Map;
|
|
912
|
+
for (const provider of providers) {
|
|
913
|
+
const emitted = this.emitProvider(provider, kind);
|
|
914
|
+
if (emitted.constLine)
|
|
915
|
+
lines.push(emitted.constLine);
|
|
916
|
+
returns.set(emitted.key, emitted.expr);
|
|
917
|
+
}
|
|
918
|
+
for (const controller of controllers) {
|
|
919
|
+
const emitted = this.emitController(controller, kind);
|
|
920
|
+
lines.push(emitted.constLine);
|
|
921
|
+
returns.set(emitted.key, emitted.expr);
|
|
922
|
+
}
|
|
923
|
+
const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
|
|
924
|
+
lines.push(`return { ${entries.join(", ")} };`);
|
|
925
|
+
return lines.join(`
|
|
926
|
+
`);
|
|
927
|
+
}
|
|
928
|
+
emitProvider(provider, kind) {
|
|
929
|
+
const key = camelName(provider.token);
|
|
930
|
+
switch (provider.kind) {
|
|
931
|
+
case "class": {
|
|
932
|
+
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
|
|
933
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
934
|
+
const local = this.localVar(provider.token, kind);
|
|
935
|
+
return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
|
|
936
|
+
}
|
|
937
|
+
case "value": {
|
|
938
|
+
const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
|
|
939
|
+
const local = this.localVar(provider.token, kind);
|
|
940
|
+
return { constLine: `const ${local} = ${expr};`, key, expr: local };
|
|
941
|
+
}
|
|
942
|
+
case "factory": {
|
|
943
|
+
const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
|
|
944
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
945
|
+
const local = this.localVar(provider.token, kind);
|
|
946
|
+
return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
|
|
947
|
+
}
|
|
948
|
+
case "existing": {
|
|
949
|
+
return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind) };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
emitController(controller, kind) {
|
|
954
|
+
const className = this.imports.add(controller.className, controller.importPath);
|
|
955
|
+
const args = controller.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
956
|
+
const key = camelName(controller.className);
|
|
957
|
+
const local = this.localVar(controller.className, kind);
|
|
958
|
+
return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
|
|
959
|
+
}
|
|
960
|
+
localVar(token, kind) {
|
|
961
|
+
const locals = this.locals[kind];
|
|
962
|
+
const existing = locals.get(token);
|
|
963
|
+
if (existing)
|
|
964
|
+
return existing;
|
|
965
|
+
const base = camelName(token);
|
|
966
|
+
let local = base;
|
|
967
|
+
let counter = 2;
|
|
968
|
+
while ([...locals.values()].includes(local)) {
|
|
969
|
+
local = `${base}${counter}`;
|
|
970
|
+
counter += 1;
|
|
971
|
+
}
|
|
972
|
+
locals.set(token, local);
|
|
973
|
+
return local;
|
|
974
|
+
}
|
|
975
|
+
depExpr(token, kind) {
|
|
976
|
+
if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
|
|
977
|
+
return "ctx";
|
|
978
|
+
if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
|
|
979
|
+
return "ctx";
|
|
980
|
+
const own = this.module.providers.find((p) => p.token === token);
|
|
981
|
+
if (own) {
|
|
982
|
+
if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
|
|
983
|
+
return this.locals[kind].get(token) ?? camelName(token);
|
|
984
|
+
}
|
|
985
|
+
if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
|
|
986
|
+
return this.depExpr(own.useExisting ?? token, kind);
|
|
987
|
+
}
|
|
988
|
+
if (kind === "services") {
|
|
989
|
+
return `services.${camelName(token)}`;
|
|
990
|
+
}
|
|
991
|
+
return `services.${camelName(token)}`;
|
|
992
|
+
}
|
|
993
|
+
for (const importName of this.module.imports) {
|
|
994
|
+
const imported = this.graph.modules.find((m) => m.name === importName);
|
|
995
|
+
if (!imported?.exports.includes(token))
|
|
996
|
+
continue;
|
|
997
|
+
if (kind === "services")
|
|
998
|
+
return `imported.${importName}.${camelName(token)}`;
|
|
999
|
+
return `imported.${importName}.${camelName(token)}`;
|
|
1000
|
+
}
|
|
1001
|
+
if (kind === "services")
|
|
1002
|
+
return `deps.${camelName(token)}`;
|
|
1003
|
+
return `services.${camelName(token)}`;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function orderProviders(providers) {
|
|
1007
|
+
const remaining = [...providers];
|
|
1008
|
+
const emitted = new Set;
|
|
1009
|
+
const result = [];
|
|
1010
|
+
while (remaining.length > 0) {
|
|
1011
|
+
const index = remaining.findIndex((p) => p.deps.every((dep) => {
|
|
1012
|
+
const depProvider = providers.find((x) => x.token === dep);
|
|
1013
|
+
return !depProvider || emitted.has(dep);
|
|
1014
|
+
}));
|
|
1015
|
+
if (index === -1) {
|
|
1016
|
+
result.push(...remaining.splice(0));
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
const [provider] = remaining.splice(index, 1);
|
|
1020
|
+
emitted.add(provider.token);
|
|
1021
|
+
result.push(provider);
|
|
1022
|
+
}
|
|
1023
|
+
return result;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/profiles.ts
|
|
1027
|
+
var MODULAR_MONOLITH_RULES = [
|
|
1028
|
+
{
|
|
1029
|
+
sourceTag: "type:feature",
|
|
1030
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
sourceTag: "type:root",
|
|
1034
|
+
onlyDependOnLibsWithTags: ["type:feature", "type:core", "type:shared", "type:domain"]
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
sourceTag: "type:app",
|
|
1038
|
+
onlyDependOnLibsWithTags: ["type:feature", "type:core", "type:shared", "type:domain"]
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
sourceTag: "type:core",
|
|
1042
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
sourceTag: "type:shared",
|
|
1046
|
+
bannedDependenciesWithTags: ["type:feature", "type:core", "type:root", "type:app"]
|
|
1047
|
+
}
|
|
1048
|
+
];
|
|
1049
|
+
var ANGULAR_ENTERPRISE_RULES = [
|
|
1050
|
+
{
|
|
1051
|
+
sourceTag: "type:feature",
|
|
1052
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
sourceTag: "type:ui",
|
|
1056
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
sourceTag: "type:data-access",
|
|
1060
|
+
bannedDependenciesWithTags: ["type:feature", "type:ui", "type:root", "type:app"]
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
sourceTag: "type:util",
|
|
1064
|
+
bannedDependenciesWithTags: ["type:feature", "type:ui", "type:data-access", "type:root", "type:app"]
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
sourceTag: "type:shared",
|
|
1068
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
sourceTag: "type:core",
|
|
1072
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
sourceTag: "type:root",
|
|
1076
|
+
onlyDependOnLibsWithTags: ["type:feature", "type:ui", "type:data-access", "type:core", "type:shared", "type:util"]
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
sourceTag: "type:app",
|
|
1080
|
+
onlyDependOnLibsWithTags: ["type:feature", "type:ui", "type:data-access", "type:core", "type:shared", "type:util"]
|
|
1081
|
+
}
|
|
1082
|
+
];
|
|
1083
|
+
var CLEAN_ARCHITECTURE_RULES = [
|
|
1084
|
+
{
|
|
1085
|
+
sourceTag: "type:api",
|
|
1086
|
+
onlyDependOnLibsWithTags: ["type:application", "type:domain", "type:shared", "type:common"]
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
sourceTag: "type:controller",
|
|
1090
|
+
onlyDependOnLibsWithTags: ["type:application", "type:domain", "type:shared", "type:common"]
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
sourceTag: "type:presentation",
|
|
1094
|
+
onlyDependOnLibsWithTags: ["type:application", "type:domain", "type:shared", "type:common"]
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
sourceTag: "type:application",
|
|
1098
|
+
onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
sourceTag: "type:service",
|
|
1102
|
+
onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
sourceTag: "type:domain",
|
|
1106
|
+
bannedDependenciesWithTags: [
|
|
1107
|
+
"type:api",
|
|
1108
|
+
"type:controller",
|
|
1109
|
+
"type:presentation",
|
|
1110
|
+
"type:application",
|
|
1111
|
+
"type:service",
|
|
1112
|
+
"type:infrastructure",
|
|
1113
|
+
"type:infra",
|
|
1114
|
+
"type:root",
|
|
1115
|
+
"type:app"
|
|
1116
|
+
]
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
sourceTag: "type:infrastructure",
|
|
1120
|
+
onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
sourceTag: "type:infra",
|
|
1124
|
+
onlyDependOnLibsWithTags: ["type:domain", "type:shared", "type:common"]
|
|
1125
|
+
}
|
|
1126
|
+
];
|
|
1127
|
+
var MODULE_BOUNDARY_PROFILES = {
|
|
1128
|
+
"modular-monolith": {
|
|
1129
|
+
name: "modular-monolith",
|
|
1130
|
+
description: "Modular monolith / vertical slice preset (blocks cross-feature dependencies and limits root aggregation to feature/core/shared/domain)",
|
|
1131
|
+
rules: MODULAR_MONOLITH_RULES
|
|
1132
|
+
},
|
|
1133
|
+
"feature-slices": {
|
|
1134
|
+
name: "feature-slices",
|
|
1135
|
+
description: "Vertical slice preset (alias for modular-monolith)",
|
|
1136
|
+
rules: MODULAR_MONOLITH_RULES
|
|
1137
|
+
},
|
|
1138
|
+
"vertical-slices": {
|
|
1139
|
+
name: "vertical-slices",
|
|
1140
|
+
description: "Vertical slice architecture preset (alias for modular-monolith)",
|
|
1141
|
+
rules: MODULAR_MONOLITH_RULES
|
|
1142
|
+
},
|
|
1143
|
+
"angular-enterprise": {
|
|
1144
|
+
name: "angular-enterprise",
|
|
1145
|
+
description: "Angular / Nx enterprise monorepo preset (enforces one-way layering across feature, UI, data-access, util, shared, and root modules)",
|
|
1146
|
+
rules: ANGULAR_ENTERPRISE_RULES
|
|
1147
|
+
},
|
|
1148
|
+
angular: {
|
|
1149
|
+
name: "angular",
|
|
1150
|
+
description: "Angular enterprise layering preset (alias for angular-enterprise)",
|
|
1151
|
+
rules: ANGULAR_ENTERPRISE_RULES
|
|
1152
|
+
},
|
|
1153
|
+
"clean-architecture": {
|
|
1154
|
+
name: "clean-architecture",
|
|
1155
|
+
description: "Clean Architecture / DDD layering preset (API/presentation -> application -> domain <- infrastructure)",
|
|
1156
|
+
rules: CLEAN_ARCHITECTURE_RULES
|
|
1157
|
+
},
|
|
1158
|
+
"domain-driven": {
|
|
1159
|
+
name: "domain-driven",
|
|
1160
|
+
description: "DDD layering preset (alias for clean-architecture)",
|
|
1161
|
+
rules: CLEAN_ARCHITECTURE_RULES
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
function getModuleBoundaryProfile(name) {
|
|
1165
|
+
const profile = MODULE_BOUNDARY_PROFILES[name];
|
|
1166
|
+
if (!profile) {
|
|
1167
|
+
throw new Error(`Unknown module boundary preset: '${name}'. Supported presets: ${Object.keys(MODULE_BOUNDARY_PROFILES).join(", ")}`);
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
...profile,
|
|
1171
|
+
rules: profile.rules.map((rule) => ({
|
|
1172
|
+
...rule,
|
|
1173
|
+
onlyDependOnLibsWithTags: rule.onlyDependOnLibsWithTags ? [...rule.onlyDependOnLibsWithTags] : undefined,
|
|
1174
|
+
bannedDependenciesWithTags: rule.bannedDependenciesWithTags ? [...rule.bannedDependenciesWithTags] : undefined
|
|
1175
|
+
}))
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function getModuleBoundaryPreset(name) {
|
|
1179
|
+
return getModuleBoundaryProfile(name).rules;
|
|
1180
|
+
}
|
|
1181
|
+
function resolveModuleBoundaries(options) {
|
|
1182
|
+
if (!options)
|
|
1183
|
+
return;
|
|
1184
|
+
const { preset, rules } = options;
|
|
1185
|
+
if (!preset && !rules)
|
|
1186
|
+
return;
|
|
1187
|
+
const presetRules = preset ? getModuleBoundaryPreset(preset) : [];
|
|
1188
|
+
const customRules = rules ?? [];
|
|
1189
|
+
const merged = [...presetRules, ...customRules];
|
|
1190
|
+
return merged.length > 0 ? merged : undefined;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/validate.ts
|
|
1194
|
+
var SCOPE_LIFETIME_RANK = {
|
|
1195
|
+
application: 0,
|
|
1196
|
+
request: 1,
|
|
1197
|
+
job: 1
|
|
1198
|
+
};
|
|
1199
|
+
function validateGraph(graph, options = false) {
|
|
1200
|
+
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
1201
|
+
const diagnostics = [];
|
|
1202
|
+
let moduleBoundaries;
|
|
1203
|
+
if (typeof options === "object") {
|
|
1204
|
+
try {
|
|
1205
|
+
moduleBoundaries = resolveModuleBoundaries({
|
|
1206
|
+
preset: options.moduleBoundaryPreset,
|
|
1207
|
+
rules: options.moduleBoundaries
|
|
1208
|
+
});
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
diagnostics.push({
|
|
1211
|
+
severity: "error",
|
|
1212
|
+
code: "invalid-boundary-preset",
|
|
1213
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1214
|
+
file: graph.modules[0]?.file,
|
|
1215
|
+
line: graph.modules[0]?.line
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const globalProviders = new Map;
|
|
1220
|
+
for (const module of graph.modules) {
|
|
1221
|
+
for (const provider of module.providers) {
|
|
1222
|
+
if (!globalProviders.has(provider.token)) {
|
|
1223
|
+
globalProviders.set(provider.token, { module, provider });
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
function resolveDep(module, token) {
|
|
1228
|
+
const own = module.providers.find((p) => p.token === token);
|
|
1229
|
+
if (own)
|
|
1230
|
+
return { module, provider: own };
|
|
1231
|
+
for (const importName of module.imports) {
|
|
1232
|
+
const imported = graph.modules.find((m) => m.name === importName);
|
|
1233
|
+
if (!imported || !imported.exports.includes(token))
|
|
1234
|
+
continue;
|
|
1235
|
+
const provider = imported.providers.find((p) => p.token === token);
|
|
1236
|
+
if (provider)
|
|
1237
|
+
return { module: imported, provider };
|
|
1238
|
+
}
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const error = (code, message, file, line) => {
|
|
1242
|
+
diagnostics.push({ severity: "error", code, message, file, line });
|
|
1243
|
+
};
|
|
1244
|
+
const warn2 = (code, message, file, line) => {
|
|
1245
|
+
diagnostics.push({ severity: strict ? "error" : "warn", code, message, file, line });
|
|
1246
|
+
};
|
|
1247
|
+
const modulesByName = new Map;
|
|
1248
|
+
const commandsByName = new Map;
|
|
1249
|
+
const routesByKey = new Map;
|
|
1250
|
+
for (const module of graph.modules) {
|
|
1251
|
+
const previousModule = modulesByName.get(module.name);
|
|
1252
|
+
if (previousModule) {
|
|
1253
|
+
error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
|
|
1254
|
+
} else {
|
|
1255
|
+
modulesByName.set(module.name, module);
|
|
1256
|
+
}
|
|
1257
|
+
for (const command of module.commands) {
|
|
1258
|
+
const previousName = commandsByName.get(command.name);
|
|
1259
|
+
if (previousName) {
|
|
1260
|
+
error("duplicate-command", `command 名 ${command.name} 重复(首次由模块 ${previousName.module.name} 的 ${previousName.className} 声明)`, module.file, module.line);
|
|
1261
|
+
} else {
|
|
1262
|
+
commandsByName.set(command.name, { module, className: command.className });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
for (const module of graph.modules) {
|
|
1267
|
+
for (const controller of module.controllers) {
|
|
1268
|
+
for (const route of controller.routes) {
|
|
1269
|
+
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
1270
|
+
const key = `${route.method} ${fullPath}`;
|
|
1271
|
+
const previous = routesByKey.get(key);
|
|
1272
|
+
if (previous) {
|
|
1273
|
+
error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
|
|
1274
|
+
} else {
|
|
1275
|
+
routesByKey.set(key, { module, controller });
|
|
1276
|
+
}
|
|
1277
|
+
if (route.command && !module.commands.some((command) => command.className === route.command)) {
|
|
1278
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
1279
|
+
}
|
|
1280
|
+
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
1281
|
+
error("route-command-binding-disallowed", `Route ${key} binds command ${route.command}, but route-level command bindings are disabled by policy. Use an application service (${controller.className}.${route.handler}, ${controller.file}).`, controller.file);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (typeof options === "object" && options.disallowControllerDirectDb) {
|
|
1285
|
+
for (const dep of controller.deps) {
|
|
1286
|
+
const isDbClient = dep === "DB_CLIENT" || dep === "DatabaseClient" || graph.tokenNames?.[dep] === "supacloud.db-client";
|
|
1287
|
+
if (isDbClient) {
|
|
1288
|
+
error("controller-direct-db-access", `Controller ${controller.className} directly injects database client '${dep}', violating presentation layer separation (${controller.file})`, controller.file);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
for (const module of graph.modules) {
|
|
1295
|
+
const seen = new Map;
|
|
1296
|
+
for (const provider of module.providers) {
|
|
1297
|
+
const first = seen.get(provider.token);
|
|
1298
|
+
if (first) {
|
|
1299
|
+
error("duplicate-token", `模块 ${module.name} 重复注册 token ${provider.token}(首次注册于 ${first.file}:${first.line})`, provider.file, provider.line);
|
|
1300
|
+
} else {
|
|
1301
|
+
seen.set(provider.token, provider);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
for (const provider of module.providers) {
|
|
1305
|
+
for (const dep of provider.deps) {
|
|
1306
|
+
const resolved = resolveDep(module, dep);
|
|
1307
|
+
if (!resolved) {
|
|
1308
|
+
if (!graph.externalTokens.includes(dep)) {
|
|
1309
|
+
if (globalProviders.has(dep)) {
|
|
1310
|
+
const owner = globalProviders.get(dep);
|
|
1311
|
+
error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line);
|
|
1312
|
+
} else {
|
|
1313
|
+
error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
if (SCOPE_LIFETIME_RANK[resolved.provider.scope] > SCOPE_LIFETIME_RANK[provider.scope]) {
|
|
1319
|
+
error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
for (const command of module.commands) {
|
|
1324
|
+
if (!command.permission) {
|
|
1325
|
+
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
|
|
1326
|
+
}
|
|
1327
|
+
if (typeof options === "object" && options.commandCapabilities) {
|
|
1328
|
+
const caps = options.commandCapabilities;
|
|
1329
|
+
const location = `${command.className} (${module.file})`;
|
|
1330
|
+
if (command.permission && caps.permission === false) {
|
|
1331
|
+
error("command-permission-unsupported", `Command ${command.name} declares permission, but runtime permission checks are unavailable (${location}).`, module.file, module.line);
|
|
1332
|
+
}
|
|
1333
|
+
if (command.audit && caps.audit === false) {
|
|
1334
|
+
error("command-audit-unsupported", `Command ${command.name} declares audit, but audit persistence is unavailable (${location}).`, module.file, module.line);
|
|
1335
|
+
}
|
|
1336
|
+
if (command.idempotency === "required" && caps.idempotency === false) {
|
|
1337
|
+
error("command-idempotency-unsupported", `Command ${command.name} declares idempotency, but idempotency receipt persistence is unavailable (${location}).`, module.file, module.line);
|
|
1338
|
+
}
|
|
1339
|
+
if (command.transaction === "required") {
|
|
1340
|
+
if (caps.transaction === "rpc-only") {
|
|
1341
|
+
warn2("command-transaction-rpc-only", `Command ${command.name} declares transaction: 'required', but only DB RPC transactions are available; multi-table writes must use one DB RPC (${location}).`, module.file, module.line);
|
|
1342
|
+
} else if (caps.transaction === false) {
|
|
1343
|
+
error("command-transaction-unsupported", `Command ${command.name} declares transaction: 'required', but transaction support is unavailable (${location}).`, module.file, module.line);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (moduleBoundaries && moduleBoundaries.length > 0) {
|
|
1350
|
+
for (const module of graph.modules) {
|
|
1351
|
+
const sourceTags = module.tags ?? [];
|
|
1352
|
+
for (const importName of module.imports) {
|
|
1353
|
+
const targetModule = graph.modules.find((m) => m.name === importName);
|
|
1354
|
+
if (!targetModule)
|
|
1355
|
+
continue;
|
|
1356
|
+
const targetTags = targetModule.tags ?? [];
|
|
1357
|
+
for (const rule of moduleBoundaries) {
|
|
1358
|
+
const matchesSource = rule.sourceTag === "*" || sourceTags.includes(rule.sourceTag);
|
|
1359
|
+
if (!matchesSource)
|
|
1360
|
+
continue;
|
|
1361
|
+
if (rule.bannedDependenciesWithTags) {
|
|
1362
|
+
for (const bannedTag of rule.bannedDependenciesWithTags) {
|
|
1363
|
+
if (targetTags.includes(bannedTag)) {
|
|
1364
|
+
error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 禁止依赖带有标签 '${bannedTag}' 的模块 ${targetModule.name} (tags: [${targetTags.join(", ")}])`, module.file, module.line);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
|
|
1369
|
+
const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
|
|
1370
|
+
if (!hasAllowed && targetTags.length > 0) {
|
|
1371
|
+
error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
diagnostics.push(...detectCycles(graph, resolveDep));
|
|
1379
|
+
diagnostics.push(...detectModuleCycles(graph));
|
|
1380
|
+
if (typeof options === "object" && options.detectOrphanModules) {
|
|
1381
|
+
diagnostics.push(...detectOrphanModules(graph));
|
|
1382
|
+
}
|
|
1383
|
+
return diagnostics;
|
|
1384
|
+
}
|
|
1385
|
+
function joinRoutePaths(prefix, path) {
|
|
1386
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1387
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1388
|
+
return normalized.replace(/:[^/]+/g, ":param");
|
|
1389
|
+
}
|
|
1390
|
+
function detectCycles(graph, resolveDep) {
|
|
1391
|
+
const diagnostics = [];
|
|
1392
|
+
const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
|
|
1393
|
+
const nodes = graph.modules.flatMap((module) => module.providers.map((provider) => ({ module, provider })));
|
|
1394
|
+
const state = new Map;
|
|
1395
|
+
const stack = [];
|
|
1396
|
+
const reported = new Set;
|
|
1397
|
+
const visit = (ref) => {
|
|
1398
|
+
const id = nodeId(ref);
|
|
1399
|
+
if (state.get(id) === "done")
|
|
1400
|
+
return;
|
|
1401
|
+
if (state.get(id) === "visiting") {
|
|
1402
|
+
const cycleStart = stack.findIndex((item) => nodeId(item) === id);
|
|
1403
|
+
const cycle = [...stack.slice(cycleStart), ref];
|
|
1404
|
+
const path = cycle.map((item) => item.provider.token).join(" -> ");
|
|
1405
|
+
const cycleKey = cycle.map((item) => nodeId(item)).sort().join("|");
|
|
1406
|
+
if (!reported.has(cycleKey)) {
|
|
1407
|
+
reported.add(cycleKey);
|
|
1408
|
+
diagnostics.push({
|
|
1409
|
+
severity: "error",
|
|
1410
|
+
code: "circular-dependency",
|
|
1411
|
+
message: `provider 循环依赖: ${path}`,
|
|
1412
|
+
file: ref.provider.file,
|
|
1413
|
+
line: ref.provider.line
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
state.set(id, "visiting");
|
|
1419
|
+
stack.push(ref);
|
|
1420
|
+
for (const dep of ref.provider.deps) {
|
|
1421
|
+
const resolved = resolveDep(ref.module, dep);
|
|
1422
|
+
if (resolved)
|
|
1423
|
+
visit(resolved);
|
|
1424
|
+
}
|
|
1425
|
+
stack.pop();
|
|
1426
|
+
state.set(id, "done");
|
|
1427
|
+
};
|
|
1428
|
+
for (const ref of nodes)
|
|
1429
|
+
visit(ref);
|
|
1430
|
+
return diagnostics;
|
|
1431
|
+
}
|
|
1432
|
+
function detectModuleCycles(graph) {
|
|
1433
|
+
const diagnostics = [];
|
|
1434
|
+
const moduleMap = new Map(graph.modules.map((m) => [m.name, m]));
|
|
1435
|
+
const state = new Map;
|
|
1436
|
+
const stack = [];
|
|
1437
|
+
const reported = new Set;
|
|
1438
|
+
const visit = (name) => {
|
|
1439
|
+
if (state.get(name) === "done")
|
|
1440
|
+
return;
|
|
1441
|
+
if (state.get(name) === "visiting") {
|
|
1442
|
+
const cycleStart = stack.indexOf(name);
|
|
1443
|
+
const cycle = [...stack.slice(cycleStart), name];
|
|
1444
|
+
const cycleKey = [...cycle].sort().join("|");
|
|
1445
|
+
if (!reported.has(cycleKey)) {
|
|
1446
|
+
reported.add(cycleKey);
|
|
1447
|
+
const mod2 = moduleMap.get(name);
|
|
1448
|
+
diagnostics.push({
|
|
1449
|
+
severity: "error",
|
|
1450
|
+
code: "circular-module-import",
|
|
1451
|
+
message: `Module circular import detected: ${cycle.join(" -> ")}`,
|
|
1452
|
+
file: mod2?.file,
|
|
1453
|
+
line: mod2?.line
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
state.set(name, "visiting");
|
|
1459
|
+
stack.push(name);
|
|
1460
|
+
const mod = moduleMap.get(name);
|
|
1461
|
+
if (mod) {
|
|
1462
|
+
for (const importName of mod.imports) {
|
|
1463
|
+
if (moduleMap.has(importName)) {
|
|
1464
|
+
visit(importName);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
stack.pop();
|
|
1469
|
+
state.set(name, "done");
|
|
1470
|
+
};
|
|
1471
|
+
for (const mod of graph.modules) {
|
|
1472
|
+
visit(mod.name);
|
|
1473
|
+
}
|
|
1474
|
+
return diagnostics;
|
|
1475
|
+
}
|
|
1476
|
+
function detectOrphanModules(graph) {
|
|
1477
|
+
const diagnostics = [];
|
|
1478
|
+
const rootModules = graph.modules.filter((m) => m.tags && (m.tags.includes("type:root") || m.tags.includes("type:app")) || m.name === "app" || m.name === "root");
|
|
1479
|
+
if (rootModules.length === 0)
|
|
1480
|
+
return diagnostics;
|
|
1481
|
+
const reachable = new Set;
|
|
1482
|
+
const moduleMap = new Map(graph.modules.map((m) => [m.name, m]));
|
|
1483
|
+
const queue = rootModules.map((m) => m.name);
|
|
1484
|
+
for (const root of rootModules) {
|
|
1485
|
+
reachable.add(root.name);
|
|
1486
|
+
}
|
|
1487
|
+
while (queue.length > 0) {
|
|
1488
|
+
const current = queue.shift();
|
|
1489
|
+
const mod = moduleMap.get(current);
|
|
1490
|
+
if (!mod)
|
|
1491
|
+
continue;
|
|
1492
|
+
for (const imp of mod.imports) {
|
|
1493
|
+
if (!reachable.has(imp)) {
|
|
1494
|
+
reachable.add(imp);
|
|
1495
|
+
queue.push(imp);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
for (const mod of graph.modules) {
|
|
1500
|
+
if (!reachable.has(mod.name)) {
|
|
1501
|
+
diagnostics.push({
|
|
1502
|
+
severity: "warn",
|
|
1503
|
+
code: "orphan-module",
|
|
1504
|
+
message: `Module '${mod.name}' is declared but not reachable from any root module (${rootModules.map((r) => r.name).join(", ")})`,
|
|
1505
|
+
file: mod.file,
|
|
1506
|
+
line: mod.line
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return diagnostics;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/compile.ts
|
|
1514
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
1515
|
+
import { join as join3 } from "node:path";
|
|
1516
|
+
async function compileProject(options) {
|
|
1517
|
+
const graph = await analyzeProject(options.rootDir, options.include);
|
|
1518
|
+
const diagnostics = [
|
|
1519
|
+
...graph.diagnostics ?? [],
|
|
1520
|
+
...validateGraph(graph, {
|
|
1521
|
+
strict: options.strict,
|
|
1522
|
+
moduleBoundaryPreset: options.moduleBoundaryPreset,
|
|
1523
|
+
moduleBoundaries: options.moduleBoundaries,
|
|
1524
|
+
allowRouteCommandBindings: options.allowRouteCommandBindings,
|
|
1525
|
+
commandCapabilities: options.commandCapabilities,
|
|
1526
|
+
disallowControllerDirectDb: options.disallowControllerDirectDb,
|
|
1527
|
+
detectOrphanModules: options.detectOrphanModules
|
|
1528
|
+
})
|
|
1529
|
+
];
|
|
1530
|
+
if (options.strict) {
|
|
1531
|
+
for (const diagnostic of diagnostics) {
|
|
1532
|
+
if (diagnostic.severity === "warn")
|
|
1533
|
+
diagnostic.severity = "error";
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const written = await generateApplication(graph, {
|
|
1537
|
+
rootDir: options.rootDir,
|
|
1538
|
+
outDir: options.outDir
|
|
1539
|
+
});
|
|
1540
|
+
return { diagnostics, graph, written };
|
|
1541
|
+
}
|
|
1542
|
+
async function checkProject(options) {
|
|
1543
|
+
const graph = await analyzeProject(options.rootDir, options.include);
|
|
1544
|
+
const diagnostics = [
|
|
1545
|
+
...graph.diagnostics ?? [],
|
|
1546
|
+
...validateGraph(graph, {
|
|
1547
|
+
strict: options.strict,
|
|
1548
|
+
moduleBoundaryPreset: options.moduleBoundaryPreset,
|
|
1549
|
+
moduleBoundaries: options.moduleBoundaries,
|
|
1550
|
+
allowRouteCommandBindings: options.allowRouteCommandBindings,
|
|
1551
|
+
commandCapabilities: options.commandCapabilities,
|
|
1552
|
+
disallowControllerDirectDb: options.disallowControllerDirectDb,
|
|
1553
|
+
detectOrphanModules: options.detectOrphanModules
|
|
1554
|
+
})
|
|
1555
|
+
];
|
|
1556
|
+
if (options.strict) {
|
|
1557
|
+
for (const diagnostic of diagnostics) {
|
|
1558
|
+
if (diagnostic.severity === "warn")
|
|
1559
|
+
diagnostic.severity = "error";
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const rendered = renderApplication(graph, {
|
|
1563
|
+
rootDir: options.rootDir,
|
|
1564
|
+
outDir: options.outDir
|
|
1565
|
+
});
|
|
1566
|
+
const expectedFiles = {
|
|
1567
|
+
"application.ts": rendered.applicationCode,
|
|
1568
|
+
"app.manifest.json": rendered.manifestJson
|
|
1569
|
+
};
|
|
1570
|
+
const mismatches = [];
|
|
1571
|
+
for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
|
|
1572
|
+
const diskPath = join3(options.outDir, filename);
|
|
1573
|
+
if (!existsSync2(diskPath)) {
|
|
1574
|
+
mismatches.push(`${filename}: generated artifact is missing from disk`);
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
const diskContent = readFileSync(diskPath, "utf8");
|
|
1578
|
+
if (diskContent !== expectedContent) {
|
|
1579
|
+
mismatches.push(`${filename}: disk artifact differs from current compiler output`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
upToDate: mismatches.length === 0,
|
|
1584
|
+
mismatches,
|
|
1585
|
+
diagnostics,
|
|
1586
|
+
graph
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/cli.ts
|
|
1591
|
+
function printUsage() {
|
|
1592
|
+
console.log(`
|
|
1593
|
+
@supacloud/compiler CLI
|
|
1594
|
+
|
|
1595
|
+
Usage:
|
|
1596
|
+
supacloud-compiler compile [rootDir] [options]
|
|
1597
|
+
supacloud-compiler check [rootDir] [options]
|
|
1598
|
+
|
|
1599
|
+
Commands:
|
|
1600
|
+
compile Compile application modules and generate artifacts
|
|
1601
|
+
check Check artifact drift and run governance gates
|
|
1602
|
+
|
|
1603
|
+
Options:
|
|
1604
|
+
--root, -r <dir> Application source root (default: current directory or first positional argument)
|
|
1605
|
+
--out, -o <dir> Artifact output directory (default: <rootDir>/generated)
|
|
1606
|
+
--strict Treat all warnings as errors
|
|
1607
|
+
--preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
|
|
1608
|
+
--help, -h Show this help
|
|
1609
|
+
`);
|
|
1610
|
+
}
|
|
1611
|
+
async function run() {
|
|
1612
|
+
const args = process.argv.slice(2);
|
|
1613
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
1614
|
+
printUsage();
|
|
1615
|
+
process.exit(0);
|
|
1616
|
+
}
|
|
1617
|
+
const command = args[0];
|
|
1618
|
+
if (command !== "compile" && command !== "check") {
|
|
1619
|
+
console.error(`Error: unknown command "${command}"`);
|
|
1620
|
+
printUsage();
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
}
|
|
1623
|
+
let rootDir = ".";
|
|
1624
|
+
let outDir;
|
|
1625
|
+
let strict = false;
|
|
1626
|
+
let preset;
|
|
1627
|
+
for (let i = 1;i < args.length; i++) {
|
|
1628
|
+
const arg = args[i];
|
|
1629
|
+
if (arg === "--root" || arg === "-r") {
|
|
1630
|
+
rootDir = args[++i];
|
|
1631
|
+
} else if (arg === "--out" || arg === "-o") {
|
|
1632
|
+
outDir = args[++i];
|
|
1633
|
+
} else if (arg === "--strict") {
|
|
1634
|
+
strict = true;
|
|
1635
|
+
} else if (arg === "--preset" || arg === "-p") {
|
|
1636
|
+
preset = args[++i];
|
|
1637
|
+
} else if (!arg.startsWith("-") && rootDir === ".") {
|
|
1638
|
+
rootDir = arg;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const resolvedRoot = resolve(process.cwd(), rootDir);
|
|
1642
|
+
const resolvedOut = outDir ? resolve(process.cwd(), outDir) : resolve(resolvedRoot, "generated");
|
|
1643
|
+
if (command === "compile") {
|
|
1644
|
+
const result = await compileProject({
|
|
1645
|
+
rootDir: resolvedRoot,
|
|
1646
|
+
outDir: resolvedOut,
|
|
1647
|
+
strict,
|
|
1648
|
+
moduleBoundaryPreset: preset
|
|
1649
|
+
});
|
|
1650
|
+
for (const diag of result.diagnostics) {
|
|
1651
|
+
const loc = diag.file ? ` ${diag.file}${diag.line ? `:${diag.line}` : ""}` : "";
|
|
1652
|
+
const log = diag.severity === "error" ? console.error : console.warn;
|
|
1653
|
+
log(`[${diag.severity}] ${diag.code}${loc}: ${diag.message}`);
|
|
1654
|
+
}
|
|
1655
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1656
|
+
if (errors.length > 0) {
|
|
1657
|
+
console.error(`
|
|
1658
|
+
Compilation failed with ${errors.length} error(s).`);
|
|
1659
|
+
process.exit(1);
|
|
1660
|
+
}
|
|
1661
|
+
console.log(`
|
|
1662
|
+
Compilation succeeded. Generated artifacts:
|
|
1663
|
+
${result.written.map((f) => ` - ${f}`).join(`
|
|
1664
|
+
`)}`);
|
|
1665
|
+
} else {
|
|
1666
|
+
const result = await checkProject({
|
|
1667
|
+
rootDir: resolvedRoot,
|
|
1668
|
+
outDir: resolvedOut,
|
|
1669
|
+
strict,
|
|
1670
|
+
moduleBoundaryPreset: preset
|
|
1671
|
+
});
|
|
1672
|
+
for (const diag of result.diagnostics) {
|
|
1673
|
+
const loc = diag.file ? ` ${diag.file}${diag.line ? `:${diag.line}` : ""}` : "";
|
|
1674
|
+
const log = diag.severity === "error" ? console.error : console.warn;
|
|
1675
|
+
log(`[${diag.severity}] ${diag.code}${loc}: ${diag.message}`);
|
|
1676
|
+
}
|
|
1677
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1678
|
+
if (errors.length > 0) {
|
|
1679
|
+
console.error(`
|
|
1680
|
+
Governance checks failed with ${errors.length} error(s).`);
|
|
1681
|
+
process.exit(1);
|
|
1682
|
+
}
|
|
1683
|
+
if (!result.upToDate) {
|
|
1684
|
+
console.error(`
|
|
1685
|
+
Artifact drift detected:`);
|
|
1686
|
+
for (const mismatch of result.mismatches) {
|
|
1687
|
+
console.error(` - ${mismatch}`);
|
|
1688
|
+
}
|
|
1689
|
+
console.error("Run the compile command and commit the updated generated artifacts.");
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
console.log("Artifact check passed: disk files match compiler output with no drift.");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
run().catch((err) => {
|
|
1696
|
+
console.error("Unhandled error:", err);
|
|
1697
|
+
process.exit(1);
|
|
1698
|
+
});
|