@supacloud/compiler 0.1.0

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/index.js ADDED
@@ -0,0 +1,1125 @@
1
+ // src/analyze.ts
2
+ import { existsSync } from "node:fs";
3
+ import { join, relative, sep } from "node:path";
4
+ import {
5
+ Node,
6
+ Project,
7
+ SyntaxKind
8
+ } from "ts-morph";
9
+ var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
10
+ var ROUTE_DECORATORS = {
11
+ Get: "GET",
12
+ Post: "POST",
13
+ Put: "PUT",
14
+ Patch: "PATCH",
15
+ Delete: "DELETE"
16
+ };
17
+ var SCOPES = ["application", "request", "job"];
18
+ async function analyzeProject(rootDir, include) {
19
+ const project = createProject(rootDir);
20
+ const patterns = (include ?? DEFAULT_INCLUDE).map((glob) => join(rootDir, glob));
21
+ project.addSourceFilesAtPaths(patterns);
22
+ const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules") && !sf.isDeclarationFile()).sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
23
+ const ctx = {
24
+ rootDir,
25
+ tokensByName: new Map,
26
+ classesByName: new Map,
27
+ diagnostics: []
28
+ };
29
+ for (const sf of sourceFiles) {
30
+ indexFile(sf, ctx);
31
+ }
32
+ const candidates = [];
33
+ for (const sf of sourceFiles) {
34
+ for (const cls of sf.getClasses()) {
35
+ const moduleDec = findDecorator(cls, "Module");
36
+ if (!moduleDec)
37
+ continue;
38
+ const options = decoratorObjectArg(moduleDec);
39
+ if (!options)
40
+ continue;
41
+ candidates.push({
42
+ node: cls,
43
+ options,
44
+ className: cls.getName() ?? "<anonymous>",
45
+ file: sf.getFilePath(),
46
+ line: cls.getStartLineNumber()
47
+ });
48
+ }
49
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
50
+ if (call.getExpression().getText() !== "defineModule")
51
+ continue;
52
+ const parent = call.getParent();
53
+ if (!parent || !Node.isVariableDeclaration(parent))
54
+ continue;
55
+ const arg = call.getArguments()[0];
56
+ if (!arg || !Node.isObjectLiteralExpression(arg))
57
+ continue;
58
+ candidates.push({
59
+ node: parent,
60
+ options: arg,
61
+ className: parent.getName(),
62
+ file: sf.getFilePath(),
63
+ line: parent.getStartLineNumber()
64
+ });
65
+ }
66
+ }
67
+ const nameByNode = new Map;
68
+ for (const c of candidates) {
69
+ nameByNode.set(c.node, stringLiteralProp(c.options, "name") ?? c.className);
70
+ }
71
+ const modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
72
+ const providedTokens = new Set(modules.flatMap((m) => m.providers.map((p) => p.token)));
73
+ const referenced = new Set;
74
+ for (const m of modules) {
75
+ for (const p of m.providers)
76
+ p.deps.forEach((d) => referenced.add(d));
77
+ for (const c of m.controllers)
78
+ c.deps.forEach((d) => referenced.add(d));
79
+ }
80
+ const externalTokens = [...referenced].filter((token) => !providedTokens.has(token)).sort();
81
+ const tokenNames = {};
82
+ for (const info of ctx.tokensByName.values()) {
83
+ if (info.stringName)
84
+ tokenNames[info.name] = info.stringName;
85
+ }
86
+ return {
87
+ modules,
88
+ externalTokens,
89
+ diagnostics: ctx.diagnostics,
90
+ tokenNames
91
+ };
92
+ }
93
+ function createProject(rootDir) {
94
+ const tsConfigFilePath = join(rootDir, "tsconfig.json");
95
+ if (existsSync(tsConfigFilePath)) {
96
+ return new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true });
97
+ }
98
+ return new Project({
99
+ compilerOptions: { experimentalDecorators: true, allowJs: false }
100
+ });
101
+ }
102
+ function indexFile(sf, ctx) {
103
+ for (const cls of sf.getClasses()) {
104
+ const name = cls.getName();
105
+ if (name && !ctx.classesByName.has(name)) {
106
+ ctx.classesByName.set(name, { name, decl: cls, file: sf.getFilePath() });
107
+ }
108
+ }
109
+ for (const statement of sf.getVariableStatements()) {
110
+ for (const decl of statement.getDeclarations()) {
111
+ const info = parseTokenVariable(decl, sf.getFilePath());
112
+ if (info && !ctx.tokensByName.has(info.name)) {
113
+ ctx.tokensByName.set(info.name, info);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ function parseTokenVariable(decl, file) {
119
+ const init = decl.getInitializer();
120
+ if (!init || !Node.isNewExpression(init))
121
+ return;
122
+ if (init.getExpression().getText() !== "InjectionToken")
123
+ return;
124
+ const [nameArg, optionsArg] = init.getArguments();
125
+ const info = { name: decl.getName(), file };
126
+ if (nameArg && Node.isStringLiteral(nameArg)) {
127
+ info.stringName = nameArg.getLiteralText();
128
+ }
129
+ if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
130
+ const scope = stringLiteralProp(optionsArg, "scope");
131
+ if (scope && SCOPES.includes(scope)) {
132
+ info.scope = scope;
133
+ }
134
+ }
135
+ return info;
136
+ }
137
+ function parseModule(candidate, nameByNode, ctx) {
138
+ const { options, className, file, line } = candidate;
139
+ const name = nameByNode.get(candidate.node) ?? className;
140
+ const imports = arrayProp(options, "imports").map((el) => {
141
+ const decl = Node.isIdentifier(el) ? resolveDeclaration(el)[0] : undefined;
142
+ if (decl) {
143
+ const known = nameByNode.get(decl);
144
+ if (known)
145
+ return known;
146
+ if (Node.isClassDeclaration(decl)) {
147
+ const dec = findDecorator(decl, "Module");
148
+ const decOptions = dec && decoratorObjectArg(dec);
149
+ const decName = decOptions && stringLiteralProp(decOptions, "name");
150
+ return decName ?? decl.getName() ?? el.getText();
151
+ }
152
+ if (Node.isVariableDeclaration(decl))
153
+ return decl.getName();
154
+ }
155
+ return el.getText();
156
+ }).filter((v, i, arr) => arr.indexOf(v) === i);
157
+ const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
158
+ const exportsSet = new Set(exports);
159
+ const providers = [];
160
+ for (const el of arrayProp(options, "providers")) {
161
+ const provider = parseProvider(el, exportsSet, ctx);
162
+ if (provider)
163
+ providers.push(provider);
164
+ }
165
+ const controllers = [];
166
+ for (const el of arrayProp(options, "controllers")) {
167
+ const controller = parseController(el, ctx);
168
+ if (controller)
169
+ controllers.push(controller);
170
+ }
171
+ const handlerClasses = [];
172
+ const seenHandlers = new Set;
173
+ const collectHandler = (expr) => {
174
+ if (!Node.isIdentifier(expr))
175
+ return;
176
+ const decl = resolveDeclaration(expr)[0];
177
+ if (decl && Node.isClassDeclaration(decl) && !seenHandlers.has(decl.getName() ?? "")) {
178
+ seenHandlers.add(decl.getName() ?? "");
179
+ handlerClasses.push(decl);
180
+ }
181
+ };
182
+ for (const el of arrayProp(options, "providers")) {
183
+ if (Node.isIdentifier(el))
184
+ collectHandler(el);
185
+ if (Node.isObjectLiteralExpression(el)) {
186
+ const useClass = getProp(el, "useClass");
187
+ if (useClass)
188
+ collectHandler(useClass);
189
+ }
190
+ }
191
+ arrayProp(options, "commands").forEach(collectHandler);
192
+ arrayProp(options, "queries").forEach(collectHandler);
193
+ const commands = [];
194
+ const queries = [];
195
+ for (const cls of handlerClasses) {
196
+ const commandDec = findDecorator(cls, "Command");
197
+ if (commandDec) {
198
+ const meta = decoratorObjectArg(commandDec);
199
+ if (meta) {
200
+ commands.push({
201
+ className: cls.getName() ?? "<anonymous>",
202
+ name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
203
+ permission: stringLiteralProp(meta, "permission"),
204
+ transaction: stringLiteralProp(meta, "transaction"),
205
+ audit: stringLiteralProp(meta, "audit"),
206
+ idempotency: stringLiteralProp(meta, "idempotency")
207
+ });
208
+ }
209
+ }
210
+ const queryDec = findDecorator(cls, "Query");
211
+ if (queryDec) {
212
+ const meta = decoratorObjectArg(queryDec);
213
+ if (meta) {
214
+ queries.push({
215
+ className: cls.getName() ?? "<anonymous>",
216
+ name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>"
217
+ });
218
+ }
219
+ }
220
+ }
221
+ return {
222
+ name,
223
+ className,
224
+ file: sourcePath(ctx.rootDir, file),
225
+ line,
226
+ imports,
227
+ providers,
228
+ controllers,
229
+ commands,
230
+ queries,
231
+ exports
232
+ };
233
+ }
234
+ function parseProvider(el, exportsSet, ctx) {
235
+ const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
236
+ const line = el.getStartLineNumber();
237
+ if (Node.isIdentifier(el)) {
238
+ const decl = resolveDeclaration(el)[0];
239
+ const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
240
+ const className = cls?.getName() ?? el.getText();
241
+ const { deps, missing } = cls ? classDeps(cls, ctx) : { deps: [], missing: false };
242
+ if (missing) {
243
+ warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
244
+ }
245
+ return {
246
+ token: className,
247
+ tokenKind: "class",
248
+ kind: "class",
249
+ useClass: className,
250
+ scope: resolveScope({ cls, tokenName: className }, ctx),
251
+ deps,
252
+ exported: exportsSet.has(className),
253
+ file,
254
+ line,
255
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
256
+ };
257
+ }
258
+ if (!Node.isObjectLiteralExpression(el))
259
+ return;
260
+ const provideExpr = getProp(el, "provide");
261
+ if (!provideExpr)
262
+ return;
263
+ const { name: token, kind: tokenKind } = tokenNameOf(provideExpr, ctx);
264
+ const explicitScope = parseScopeProp(el);
265
+ const explicitDeps = arrayProp(el, "deps").map((d) => tokenNameOf(d, ctx).name);
266
+ const useClassExpr = getProp(el, "useClass");
267
+ const useValueExpr = getProp(el, "useValue");
268
+ const useFactoryExpr = getProp(el, "useFactory");
269
+ const useExistingExpr = getProp(el, "useExisting");
270
+ if (useClassExpr) {
271
+ const decl = Node.isIdentifier(useClassExpr) ? resolveDeclaration(useClassExpr)[0] : undefined;
272
+ const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
273
+ const useClass = cls?.getName() ?? useClassExpr.getText();
274
+ let deps = explicitDeps;
275
+ if (deps.length === 0 && cls) {
276
+ const result = classDeps(cls, ctx);
277
+ deps = result.deps;
278
+ if (result.missing) {
279
+ warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
280
+ }
281
+ }
282
+ return {
283
+ token,
284
+ tokenKind,
285
+ kind: "class",
286
+ useClass,
287
+ scope: resolveScope({ explicit: explicitScope, cls, tokenName: token }, ctx),
288
+ deps,
289
+ exported: exportsSet.has(token),
290
+ file,
291
+ line,
292
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
293
+ };
294
+ }
295
+ if (useValueExpr) {
296
+ return {
297
+ token,
298
+ tokenKind,
299
+ kind: "value",
300
+ useValueExpr: useValueExpr.getText(),
301
+ scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
302
+ deps: [],
303
+ exported: exportsSet.has(token),
304
+ file,
305
+ line,
306
+ importPath: Node.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
307
+ };
308
+ }
309
+ if (useFactoryExpr) {
310
+ const factoryName = Node.isIdentifier(useFactoryExpr) ? (() => {
311
+ const decl = resolveDeclaration(useFactoryExpr)[0];
312
+ return decl && (Node.isFunctionDeclaration(decl) || Node.isVariableDeclaration(decl)) ? decl.getName() ?? useFactoryExpr.getText() : useFactoryExpr.getText();
313
+ })() : useFactoryExpr.getText();
314
+ return {
315
+ token,
316
+ tokenKind,
317
+ kind: "factory",
318
+ useFactoryName: factoryName,
319
+ scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
320
+ deps: explicitDeps,
321
+ exported: exportsSet.has(token),
322
+ file,
323
+ line,
324
+ importPath: Node.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
325
+ };
326
+ }
327
+ if (useExistingExpr) {
328
+ const target = tokenNameOf(useExistingExpr, ctx).name;
329
+ return {
330
+ token,
331
+ tokenKind,
332
+ kind: "existing",
333
+ useExisting: target,
334
+ scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
335
+ deps: [target],
336
+ exported: exportsSet.has(token),
337
+ file,
338
+ line
339
+ };
340
+ }
341
+ return;
342
+ }
343
+ function parseController(el, ctx) {
344
+ if (!Node.isIdentifier(el))
345
+ return;
346
+ const decl = resolveDeclaration(el)[0];
347
+ if (!decl || !Node.isClassDeclaration(decl))
348
+ return;
349
+ const controllerDec = findDecorator(decl, "Controller");
350
+ if (!controllerDec)
351
+ return;
352
+ const pathArg = controllerDec.getArguments()[0];
353
+ const path = pathArg && Node.isStringLiteral(pathArg) ? pathArg.getLiteralText() : "/";
354
+ const { deps, missing } = classDeps(decl, ctx);
355
+ const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
356
+ if (missing) {
357
+ warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
358
+ }
359
+ const injectable = parseInjectableOptions(decl, ctx);
360
+ const routes = [];
361
+ const schemaImports = {};
362
+ for (const method of decl.getMethods()) {
363
+ for (const dec of method.getDecorators()) {
364
+ const name = decoratorName(dec);
365
+ const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
366
+ if (!httpMethod)
367
+ continue;
368
+ const args = dec.getArguments();
369
+ const pathArg2 = args[0];
370
+ const route = {
371
+ method: httpMethod,
372
+ path: pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/",
373
+ handler: method.getName()
374
+ };
375
+ const optionsArg = args[1];
376
+ if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
377
+ for (const field of ["body", "params", "query", "response"]) {
378
+ const schemaExpr = getProp(optionsArg, field);
379
+ if (schemaExpr && Node.isIdentifier(schemaExpr)) {
380
+ route[field] = schemaExpr.getText();
381
+ const importPath = importPathOf(schemaExpr, ctx);
382
+ if (importPath)
383
+ schemaImports[schemaExpr.getText()] = importPath;
384
+ }
385
+ }
386
+ }
387
+ routes.push(route);
388
+ }
389
+ }
390
+ return {
391
+ className: decl.getName() ?? "<anonymous>",
392
+ path,
393
+ scope: injectable?.scope ?? "request",
394
+ deps,
395
+ routes,
396
+ file,
397
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
398
+ schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
399
+ };
400
+ }
401
+ function classDeps(cls, ctx) {
402
+ const injectable = parseInjectableOptions(cls, ctx);
403
+ if (injectable?.deps)
404
+ return { deps: injectable.deps, missing: false };
405
+ const ctor = cls.getConstructors()[0];
406
+ if (!ctor || ctor.getParameters().length === 0)
407
+ return { deps: [], missing: false };
408
+ const injectParams = parseInjectParams(cls);
409
+ const deps = [];
410
+ let missing = false;
411
+ ctor.getParameters().forEach((param, index) => {
412
+ const injected = injectParams.get(index);
413
+ if (injected) {
414
+ deps.push(injected);
415
+ return;
416
+ }
417
+ const byType = paramTypeTokenName(param, ctx);
418
+ if (byType) {
419
+ deps.push(byType);
420
+ } else {
421
+ missing = true;
422
+ }
423
+ });
424
+ return { deps, missing };
425
+ }
426
+ function paramTypeTokenName(param, ctx) {
427
+ const typeNode = param.getTypeNode();
428
+ if (!typeNode)
429
+ return;
430
+ const text = typeNode.getText().replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
431
+ if (ctx.classesByName.has(text))
432
+ return text;
433
+ if (ctx.tokensByName.has(text))
434
+ return text;
435
+ return;
436
+ }
437
+ function parseInjectableOptions(cls, ctx) {
438
+ const dec = findDecorator(cls, "Injectable");
439
+ if (!dec)
440
+ return;
441
+ const obj = decoratorObjectArg(dec);
442
+ if (!obj)
443
+ return {};
444
+ const scope = stringLiteralProp(obj, "scope");
445
+ const depsExpr = getProp(obj, "deps");
446
+ return {
447
+ scope: scope && SCOPES.includes(scope) ? scope : undefined,
448
+ deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
449
+ };
450
+ }
451
+ function parseInjectParams(cls) {
452
+ const result = new Map;
453
+ const ctor = cls.getConstructors()[0];
454
+ if (!ctor)
455
+ return result;
456
+ ctor.getParameters().forEach((param, index) => {
457
+ for (const dec of param.getDecorators()) {
458
+ if (decoratorName(dec) !== "Inject")
459
+ continue;
460
+ const arg = dec.getArguments()[0];
461
+ if (arg)
462
+ result.set(index, tokenText(arg));
463
+ }
464
+ });
465
+ return result;
466
+ }
467
+ function tokenText(expr) {
468
+ if (Node.isIdentifier(expr)) {
469
+ const decl = resolveDeclaration(expr)[0];
470
+ if (decl && Node.isClassDeclaration(decl))
471
+ return decl.getName() ?? expr.getText();
472
+ if (decl && Node.isVariableDeclaration(decl))
473
+ return decl.getName();
474
+ }
475
+ return expr.getText();
476
+ }
477
+ function resolveScope(input, ctx) {
478
+ if (input.explicit)
479
+ return input.explicit;
480
+ if (input.cls) {
481
+ const injectable = parseInjectableOptions(input.cls, ctx);
482
+ if (injectable?.scope)
483
+ return injectable.scope;
484
+ }
485
+ const token = ctx.tokensByName.get(input.tokenName);
486
+ if (token?.scope)
487
+ return token.scope;
488
+ return "application";
489
+ }
490
+ function tokenNameOf(expr, ctx) {
491
+ if (Node.isIdentifier(expr)) {
492
+ const decl = resolveDeclaration(expr)[0];
493
+ if (decl && Node.isClassDeclaration(decl)) {
494
+ return { name: decl.getName() ?? expr.getText(), kind: "class" };
495
+ }
496
+ if (decl && Node.isVariableDeclaration(decl)) {
497
+ const name = decl.getName();
498
+ return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
499
+ }
500
+ if (ctx.tokensByName.has(expr.getText())) {
501
+ return { name: expr.getText(), kind: "injection-token" };
502
+ }
503
+ }
504
+ return { name: expr.getText(), kind: "class" };
505
+ }
506
+ function resolveDeclaration(id) {
507
+ let symbol = id.getSymbol();
508
+ if (!symbol)
509
+ return [];
510
+ let declarations = symbol.getDeclarations();
511
+ for (let guard = 0;guard < 4; guard += 1) {
512
+ const isAlias = declarations.some((d) => Node.isImportSpecifier(d) || Node.isImportClause(d) || Node.isNamespaceImport(d));
513
+ if (!isAlias)
514
+ break;
515
+ const aliased = symbol.getAliasedSymbol();
516
+ if (!aliased)
517
+ break;
518
+ symbol = aliased;
519
+ declarations = aliased.getDeclarations();
520
+ }
521
+ return declarations;
522
+ }
523
+ function importPathOf(id, ctx) {
524
+ const symbol = id.getSymbol();
525
+ const first = symbol?.getDeclarations()[0];
526
+ if (first && (Node.isImportSpecifier(first) || Node.isImportClause(first))) {
527
+ const importDecl = first.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
528
+ const target = importDecl?.getModuleSpecifierSourceFile();
529
+ if (target)
530
+ return modulePath(ctx.rootDir, target.getFilePath());
531
+ }
532
+ const decl = resolveDeclaration(id)[0];
533
+ if (decl)
534
+ return modulePath(ctx.rootDir, decl.getSourceFile().getFilePath());
535
+ return;
536
+ }
537
+ function findDecorator(cls, name) {
538
+ return cls.getDecorators().find((dec) => decoratorName(dec) === name);
539
+ }
540
+ function decoratorName(dec) {
541
+ const expr = dec.getExpression();
542
+ if (Node.isCallExpression(expr)) {
543
+ return expr.getExpression().getText().split(".").pop();
544
+ }
545
+ if (Node.isIdentifier(expr))
546
+ return expr.getText();
547
+ return;
548
+ }
549
+ function decoratorObjectArg(dec) {
550
+ const expr = dec.getExpression();
551
+ if (!Node.isCallExpression(expr))
552
+ return;
553
+ const arg = expr.getArguments()[0];
554
+ return arg && Node.isObjectLiteralExpression(arg) ? arg : undefined;
555
+ }
556
+ function getProp(obj, name) {
557
+ const prop = obj.getProperty(name);
558
+ if (prop && Node.isPropertyAssignment(prop))
559
+ return prop.getInitializer();
560
+ return;
561
+ }
562
+ function stringLiteralProp(obj, name) {
563
+ const expr = getProp(obj, name);
564
+ return expr && Node.isStringLiteral(expr) ? expr.getLiteralText() : undefined;
565
+ }
566
+ function arrayProp(obj, name) {
567
+ const expr = getProp(obj, name);
568
+ return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
569
+ }
570
+ function parseScopeProp(obj) {
571
+ const scope = stringLiteralProp(obj, "scope");
572
+ return scope && SCOPES.includes(scope) ? scope : undefined;
573
+ }
574
+ function modulePath(rootDir, absFile) {
575
+ return sourcePath(rootDir, absFile).replace(/\.(ts|tsx|js|mts|cts)$/, "");
576
+ }
577
+ function sourcePath(rootDir, absFile) {
578
+ return relative(rootDir, absFile).split(sep).join("/");
579
+ }
580
+ function warn(ctx, code, message, file, line) {
581
+ ctx.diagnostics.push({ severity: "warn", code, message, file, line });
582
+ }
583
+ // src/generate.ts
584
+ import { mkdir, writeFile } from "node:fs/promises";
585
+ import { join as join2 } from "node:path";
586
+
587
+ // src/util.ts
588
+ function camelName(token) {
589
+ const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
590
+ if (isConstantCase) {
591
+ return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
592
+ }
593
+ return token.charAt(0).toLowerCase() + token.slice(1);
594
+ }
595
+ function relativeImportPath(fromDir, toFile) {
596
+ const fromParts = fromDir.split("/").filter(Boolean);
597
+ const toParts = toFile.split("/").filter(Boolean);
598
+ let common = 0;
599
+ while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
600
+ common += 1;
601
+ }
602
+ const ups = fromParts.length - common;
603
+ const downs = toParts.slice(common);
604
+ const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
605
+ const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
606
+ const joined = segments.join("/");
607
+ return joined.startsWith("..") ? joined : `./${joined}`;
608
+ }
609
+ var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
610
+ var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
611
+ function isRequestContextToken(token, tokenNames) {
612
+ return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
613
+ }
614
+ function isJobContextToken(token, tokenNames) {
615
+ return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
616
+ }
617
+
618
+ // src/generate.ts
619
+ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
620
+ var INTERFACES = `export interface CompiledRoute {
621
+ method: string;
622
+ path: string;
623
+ handler: string;
624
+ body?: unknown;
625
+ params?: unknown;
626
+ query?: unknown;
627
+ response?: unknown;
628
+ }
629
+
630
+ export interface CompiledController {
631
+ path: string;
632
+ serviceKey: string;
633
+ scope: string;
634
+ routes: CompiledRoute[];
635
+ }
636
+
637
+ export interface CompiledModule {
638
+ name: string;
639
+ createServices(
640
+ deps: Record<string, unknown>,
641
+ imported: Record<string, Record<string, unknown>>,
642
+ ): Record<string, unknown>;
643
+ createRequestScope?(
644
+ services: Record<string, unknown>,
645
+ ctx: unknown,
646
+ ): Record<string, unknown>;
647
+ createJobScope?(
648
+ services: Record<string, unknown>,
649
+ ctx: unknown,
650
+ ): Record<string, unknown>;
651
+ controllers: CompiledController[];
652
+ }`;
653
+ async function generateApplication(graph, options) {
654
+ const modules = topoSortModules(graph.modules);
655
+ const imports = new ImportManager;
656
+ const factorySections = [];
657
+ const descriptorEntries = [];
658
+ for (const module of modules) {
659
+ const gen = new ModuleGenerator(graph, module, imports);
660
+ factorySections.push(...gen.renderFactories());
661
+ descriptorEntries.push(gen.renderDescriptor());
662
+ }
663
+ const code = [
664
+ HEADER,
665
+ "",
666
+ ...imports.render(options.rootDir, options.outDir),
667
+ ...imports.size > 0 ? [""] : [],
668
+ INTERFACES,
669
+ "",
670
+ "export function createCompiledModules(deps: Record<string, unknown>): CompiledModule[] {",
671
+ " return [",
672
+ ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
673
+ " ];",
674
+ "}",
675
+ "",
676
+ ...factorySections,
677
+ ""
678
+ ].join(`
679
+ `);
680
+ const manifest = {
681
+ version: 1,
682
+ modules: graph.modules,
683
+ externalTokens: graph.externalTokens
684
+ };
685
+ await mkdir(options.outDir, { recursive: true });
686
+ const applicationPath = join2(options.outDir, "application.ts");
687
+ const manifestPath = join2(options.outDir, "app.manifest.json");
688
+ await writeFile(applicationPath, code, "utf8");
689
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + `
690
+ `, "utf8");
691
+ return [applicationPath, manifestPath];
692
+ }
693
+ function factoryOfScope(scope) {
694
+ return scope === "application" ? "services" : scope;
695
+ }
696
+ function topoSortModules(modules) {
697
+ const byName = new Map(modules.map((m) => [m.name, m]));
698
+ const visited = new Set;
699
+ const result = [];
700
+ const visit = (module) => {
701
+ if (visited.has(module.name))
702
+ return;
703
+ visited.add(module.name);
704
+ for (const importName of module.imports) {
705
+ const dep = byName.get(importName);
706
+ if (dep && dep !== module)
707
+ visit(dep);
708
+ }
709
+ result.push(module);
710
+ };
711
+ for (const module of modules)
712
+ visit(module);
713
+ return result;
714
+ }
715
+ function pascalName(name) {
716
+ const joined = name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
717
+ return joined || "App";
718
+ }
719
+ function indent(text, spaces) {
720
+ const pad = " ".repeat(spaces);
721
+ return text.split(`
722
+ `).map((line) => line.length > 0 ? pad + line : line).join(`
723
+ `);
724
+ }
725
+
726
+ class ImportManager {
727
+ entries = new Map;
728
+ get size() {
729
+ return this.entries.size;
730
+ }
731
+ add(exported, importPath) {
732
+ if (!importPath)
733
+ return exported;
734
+ for (const [local2, entry] of this.entries) {
735
+ if (entry.path === importPath && entry.exported === exported)
736
+ return local2;
737
+ }
738
+ let local = exported;
739
+ let counter = 2;
740
+ while (this.entries.has(local)) {
741
+ local = `${exported}${counter}`;
742
+ counter += 1;
743
+ }
744
+ this.entries.set(local, { path: importPath, exported });
745
+ return local;
746
+ }
747
+ render(rootDir, outDir) {
748
+ const byPath = new Map;
749
+ for (const [local, entry] of this.entries) {
750
+ const list = byPath.get(entry.path) ?? [];
751
+ list.push({ exported: entry.exported, local });
752
+ byPath.set(entry.path, list);
753
+ }
754
+ return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, symbols]) => {
755
+ const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
756
+ 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(", ");
757
+ return `import { ${names} } from "${spec}";`;
758
+ });
759
+ }
760
+ }
761
+
762
+ class ModuleGenerator {
763
+ graph;
764
+ module;
765
+ imports;
766
+ pascal;
767
+ locals = {
768
+ services: new Map,
769
+ request: new Map,
770
+ job: new Map
771
+ };
772
+ constructor(graph, module, imports) {
773
+ this.graph = graph;
774
+ this.module = module;
775
+ this.imports = imports;
776
+ this.pascal = pascalName(module.name);
777
+ }
778
+ renderFactories() {
779
+ const sections = [this.renderServicesFactory()];
780
+ if (this.hasFactoryContent("request")) {
781
+ sections.push(this.renderScopeFactory("request"));
782
+ }
783
+ if (this.hasFactoryContent("job")) {
784
+ sections.push(this.renderScopeFactory("job"));
785
+ }
786
+ return sections;
787
+ }
788
+ renderDescriptor() {
789
+ const lines = [
790
+ `{`,
791
+ ` name: ${JSON.stringify(this.module.name)},`,
792
+ ` createServices: create${this.pascal}Services,`
793
+ ];
794
+ if (this.hasFactoryContent("request")) {
795
+ lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
796
+ }
797
+ if (this.hasFactoryContent("job")) {
798
+ lines.push(` createJobScope: create${this.pascal}JobScope,`);
799
+ }
800
+ lines.push(` controllers: ${this.renderControllers()},`);
801
+ lines.push(`}`);
802
+ return lines.join(`
803
+ `);
804
+ }
805
+ hasFactoryContent(kind) {
806
+ return this.module.providers.some((p) => factoryOfScope(p.scope) === kind) || this.module.controllers.some((c) => factoryOfScope(c.scope) === kind);
807
+ }
808
+ renderControllers() {
809
+ if (this.module.controllers.length === 0)
810
+ return "[]";
811
+ const items = this.module.controllers.map((controller) => {
812
+ const routes = controller.routes.map((route) => {
813
+ const fields = [
814
+ `method: ${JSON.stringify(route.method)}`,
815
+ `path: ${JSON.stringify(route.path)}`,
816
+ `handler: ${JSON.stringify(route.handler)}`
817
+ ];
818
+ for (const field of ["body", "params", "query", "response"]) {
819
+ const symbol = route[field];
820
+ if (symbol) {
821
+ const local = this.imports.add(symbol, controller.schemaImports?.[symbol]);
822
+ fields.push(`${field}: ${local}`);
823
+ }
824
+ }
825
+ return `{ ${fields.join(", ")} }`;
826
+ });
827
+ return [
828
+ `{`,
829
+ ` path: ${JSON.stringify(controller.path)},`,
830
+ ` serviceKey: ${JSON.stringify(camelName(controller.className))},`,
831
+ ` scope: ${JSON.stringify(controller.scope)},`,
832
+ ` routes: [${routes.join(", ")}],`,
833
+ `}`
834
+ ].join(`
835
+ `);
836
+ });
837
+ return `[${items.map((item) => `
838
+ ${indent(item, 2)}`).join(",")}
839
+ ]`;
840
+ }
841
+ renderServicesFactory() {
842
+ return [
843
+ `function create${this.pascal}Services(`,
844
+ ` deps: Record<string, unknown>,`,
845
+ ` imported: Record<string, Record<string, unknown>>,`,
846
+ `): Record<string, unknown> {`,
847
+ indent(this.renderFactoryBody("services"), 2),
848
+ `}`
849
+ ].join(`
850
+ `);
851
+ }
852
+ renderScopeFactory(kind) {
853
+ const suffix = kind === "request" ? "RequestScope" : "JobScope";
854
+ return [
855
+ `function create${this.pascal}${suffix}(`,
856
+ ` services: Record<string, unknown>,`,
857
+ ` ctx: unknown,`,
858
+ `): Record<string, unknown> {`,
859
+ indent(this.renderFactoryBody(kind), 2),
860
+ `}`
861
+ ].join(`
862
+ `);
863
+ }
864
+ renderFactoryBody(kind) {
865
+ const providers = orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind));
866
+ const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
867
+ const lines = [];
868
+ const returns = new Map;
869
+ for (const provider of providers) {
870
+ const emitted = this.emitProvider(provider, kind);
871
+ if (emitted.constLine)
872
+ lines.push(emitted.constLine);
873
+ returns.set(emitted.key, emitted.expr);
874
+ }
875
+ for (const controller of controllers) {
876
+ const emitted = this.emitController(controller, kind);
877
+ lines.push(emitted.constLine);
878
+ returns.set(emitted.key, emitted.expr);
879
+ }
880
+ const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
881
+ lines.push(`return { ${entries.join(", ")} };`);
882
+ return lines.join(`
883
+ `);
884
+ }
885
+ emitProvider(provider, kind) {
886
+ const key = camelName(provider.token);
887
+ switch (provider.kind) {
888
+ case "class": {
889
+ const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
890
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
891
+ const local = this.localVar(provider.token, kind);
892
+ return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
893
+ }
894
+ case "value": {
895
+ const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
896
+ const local = this.localVar(provider.token, kind);
897
+ return { constLine: `const ${local} = ${expr};`, key, expr: local };
898
+ }
899
+ case "factory": {
900
+ const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
901
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
902
+ const local = this.localVar(provider.token, kind);
903
+ return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
904
+ }
905
+ case "existing": {
906
+ return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind) };
907
+ }
908
+ }
909
+ }
910
+ emitController(controller, kind) {
911
+ const className = this.imports.add(controller.className, controller.importPath);
912
+ const args = controller.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
913
+ const key = camelName(controller.className);
914
+ const local = this.localVar(controller.className, kind);
915
+ return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
916
+ }
917
+ localVar(token, kind) {
918
+ const locals = this.locals[kind];
919
+ const existing = locals.get(token);
920
+ if (existing)
921
+ return existing;
922
+ const base = camelName(token);
923
+ let local = base;
924
+ let counter = 2;
925
+ while ([...locals.values()].includes(local)) {
926
+ local = `${base}${counter}`;
927
+ counter += 1;
928
+ }
929
+ locals.set(token, local);
930
+ return local;
931
+ }
932
+ depExpr(token, kind) {
933
+ if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
934
+ return "ctx";
935
+ if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
936
+ return "ctx";
937
+ const own = this.module.providers.find((p) => p.token === token);
938
+ if (own) {
939
+ if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
940
+ return this.locals[kind].get(token) ?? camelName(token);
941
+ }
942
+ if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
943
+ return this.depExpr(own.useExisting ?? token, kind);
944
+ }
945
+ if (kind === "services") {
946
+ return `services.${camelName(token)}`;
947
+ }
948
+ return `services.${camelName(token)}`;
949
+ }
950
+ for (const importName of this.module.imports) {
951
+ const imported = this.graph.modules.find((m) => m.name === importName);
952
+ if (!imported?.exports.includes(token))
953
+ continue;
954
+ if (kind === "services")
955
+ return `imported.${importName}.${camelName(token)}`;
956
+ return `services.${camelName(token)}`;
957
+ }
958
+ if (kind === "services")
959
+ return `deps.${camelName(token)}`;
960
+ return `services.${camelName(token)}`;
961
+ }
962
+ }
963
+ function orderProviders(providers) {
964
+ const remaining = [...providers];
965
+ const emitted = new Set;
966
+ const result = [];
967
+ while (remaining.length > 0) {
968
+ const index = remaining.findIndex((p) => p.deps.every((dep) => {
969
+ const depProvider = providers.find((x) => x.token === dep);
970
+ return !depProvider || emitted.has(dep);
971
+ }));
972
+ if (index === -1) {
973
+ result.push(...remaining.splice(0));
974
+ break;
975
+ }
976
+ const [provider] = remaining.splice(index, 1);
977
+ emitted.add(provider.token);
978
+ result.push(provider);
979
+ }
980
+ return result;
981
+ }
982
+
983
+ // src/validate.ts
984
+ var SCOPE_LIFETIME_RANK = {
985
+ application: 0,
986
+ request: 1,
987
+ job: 1
988
+ };
989
+ function validateGraph(graph, strict = false) {
990
+ const diagnostics = [];
991
+ const globalProviders = new Map;
992
+ for (const module of graph.modules) {
993
+ for (const provider of module.providers) {
994
+ if (!globalProviders.has(provider.token)) {
995
+ globalProviders.set(provider.token, { module, provider });
996
+ }
997
+ }
998
+ }
999
+ function resolveDep(module, token) {
1000
+ const own = module.providers.find((p) => p.token === token);
1001
+ if (own)
1002
+ return { module, provider: own };
1003
+ for (const importName of module.imports) {
1004
+ const imported = graph.modules.find((m) => m.name === importName);
1005
+ if (!imported || !imported.exports.includes(token))
1006
+ continue;
1007
+ const provider = imported.providers.find((p) => p.token === token);
1008
+ if (provider)
1009
+ return { module: imported, provider };
1010
+ }
1011
+ return;
1012
+ }
1013
+ const error = (code, message, file, line) => {
1014
+ diagnostics.push({ severity: "error", code, message, file, line });
1015
+ };
1016
+ const warn2 = (code, message, file, line) => {
1017
+ diagnostics.push({ severity: strict ? "error" : "warn", code, message, file, line });
1018
+ };
1019
+ for (const module of graph.modules) {
1020
+ const seen = new Map;
1021
+ for (const provider of module.providers) {
1022
+ const first = seen.get(provider.token);
1023
+ if (first) {
1024
+ error("duplicate-token", `模块 ${module.name} 重复注册 token ${provider.token}(首次注册于 ${first.file}:${first.line})`, provider.file, provider.line);
1025
+ } else {
1026
+ seen.set(provider.token, provider);
1027
+ }
1028
+ }
1029
+ for (const provider of module.providers) {
1030
+ for (const dep of provider.deps) {
1031
+ const resolved = resolveDep(module, dep);
1032
+ if (!resolved) {
1033
+ if (!graph.externalTokens.includes(dep)) {
1034
+ if (globalProviders.has(dep)) {
1035
+ const owner = globalProviders.get(dep);
1036
+ error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line);
1037
+ } else {
1038
+ error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line);
1039
+ }
1040
+ }
1041
+ continue;
1042
+ }
1043
+ if (SCOPE_LIFETIME_RANK[resolved.provider.scope] > SCOPE_LIFETIME_RANK[provider.scope]) {
1044
+ error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line);
1045
+ }
1046
+ }
1047
+ }
1048
+ for (const command of module.commands) {
1049
+ if (!command.permission) {
1050
+ warn2("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
1051
+ }
1052
+ }
1053
+ }
1054
+ diagnostics.push(...detectCycles(graph, resolveDep));
1055
+ return diagnostics;
1056
+ }
1057
+ function detectCycles(graph, resolveDep) {
1058
+ const diagnostics = [];
1059
+ const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
1060
+ const nodes = graph.modules.flatMap((module) => module.providers.map((provider) => ({ module, provider })));
1061
+ const state = new Map;
1062
+ const stack = [];
1063
+ const reported = new Set;
1064
+ const visit = (ref) => {
1065
+ const id = nodeId(ref);
1066
+ if (state.get(id) === "done")
1067
+ return;
1068
+ if (state.get(id) === "visiting") {
1069
+ const cycleStart = stack.findIndex((item) => nodeId(item) === id);
1070
+ const cycle = [...stack.slice(cycleStart), ref];
1071
+ const path = cycle.map((item) => item.provider.token).join(" -> ");
1072
+ const cycleKey = cycle.map((item) => nodeId(item)).sort().join("|");
1073
+ if (!reported.has(cycleKey)) {
1074
+ reported.add(cycleKey);
1075
+ diagnostics.push({
1076
+ severity: "error",
1077
+ code: "circular-dependency",
1078
+ message: `provider 循环依赖: ${path}`,
1079
+ file: ref.provider.file,
1080
+ line: ref.provider.line
1081
+ });
1082
+ }
1083
+ return;
1084
+ }
1085
+ state.set(id, "visiting");
1086
+ stack.push(ref);
1087
+ for (const dep of ref.provider.deps) {
1088
+ const resolved = resolveDep(ref.module, dep);
1089
+ if (resolved)
1090
+ visit(resolved);
1091
+ }
1092
+ stack.pop();
1093
+ state.set(id, "done");
1094
+ };
1095
+ for (const ref of nodes)
1096
+ visit(ref);
1097
+ return diagnostics;
1098
+ }
1099
+
1100
+ // src/compile.ts
1101
+ async function compileProject(options) {
1102
+ const graph = await analyzeProject(options.rootDir, options.include);
1103
+ const diagnostics = [
1104
+ ...graph.diagnostics ?? [],
1105
+ ...validateGraph(graph, options.strict)
1106
+ ];
1107
+ if (options.strict) {
1108
+ for (const diagnostic of diagnostics) {
1109
+ if (diagnostic.severity === "warn")
1110
+ diagnostic.severity = "error";
1111
+ }
1112
+ }
1113
+ const written = await generateApplication(graph, {
1114
+ rootDir: options.rootDir,
1115
+ outDir: options.outDir
1116
+ });
1117
+ return { diagnostics, graph, written };
1118
+ }
1119
+ export {
1120
+ analyzeProject,
1121
+ camelName,
1122
+ compileProject,
1123
+ generateApplication,
1124
+ validateGraph
1125
+ };