@supacloud/compiler 0.6.0 → 0.6.2
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/README.md +2 -1
- package/dist/cli.js +145 -79
- package/dist/index.js +145 -79
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ interface ApplicationGraph {
|
|
|
66
66
|
- `makeEnvironmentProviders`、`provideToken`、`provideAppInitializer`、`provideEnvironmentInitializer`、`provideRouter` 和 `provideHttpClient` 的可静态展开部分会在 AST 分析阶段展开为普通 provider;不支持静态安全展开的动态参数会产生诊断,不会被静默丢弃。
|
|
67
67
|
- 每个模块一个 `create<Name>Services(deps, imported)`:实例化 application 级 provider;dep 解析顺序为本模块 services > imports 模块导出的 services(`imported.<module>.<key>`)> 平台注入(`deps.<camelName>`)。
|
|
68
68
|
- 含 request 级 provider/controller 的模块额外生成 `create<Name>RequestScope(services, ctx)`:依赖 `REQUEST_CONTEXT`(或 token name `supacloud.request-context`)的参数传 `ctx`,其余经 `services` 解析(运行期负责把 imports 模块导出的 application 服务合并进 `services`);job 级同理生成 `create<Name>JobScope`。
|
|
69
|
-
- 含 request/job 级 provider 或 controller
|
|
69
|
+
- 含 request/job 级 provider 或 controller 的模块同时生成异步静态 `create<Name>RequestScope` / `create<Name>JobScope` 与 `destroy<Name>RequestScope(scope)` / `destroy<Name>JobScope(scope)`;factory 在构造中途失败时按编译期确定的逆创建顺序回滚已知 `onDestroy` 方法,不会运行时扫描或解析 Token。
|
|
70
70
|
- `@Host()` 在 EnvironmentInjector 作用域中保留元数据但不改变解析,因为 SupaCloud 没有 Angular 元素注入器树;`@Self()` / `@SkipSelf()` 由静态 factory 按当前 scope 与模块可见性执行。
|
|
71
71
|
- AOP 只支持静态边界:`ModuleOptions.aspects`、`RouteOptions.aspects`、`CommandOptions.aspects` 和 `JobOptions.aspects` 必须是显式数组字面量,元素必须是可解析的函数标识符。生成器会直接 import aspect 并生成固定顺序的 onion chain,不使用 Proxy、Reflect 扫描、动态 pointcut 或运行时注册。
|
|
72
72
|
- 执行顺序为 `module -> route -> command -> commandGovernance -> handler`;Job 使用 `module -> job -> executor -> run/execute`,并在 finally 中销毁 job scope。
|
|
@@ -90,6 +90,7 @@ interface ApplicationGraph {
|
|
|
90
90
|
| `duplicate-route` | error | 规范化后 HTTP method + path 冲突 |
|
|
91
91
|
| `route-command-unresolved` | error | 路由绑定了本模块未声明的 command 类 |
|
|
92
92
|
| `command-missing-permission` | error | `@Command` 未声明 permission |
|
|
93
|
+
| `invalid-job-scope` | error | `@Job` 使用了不支持的 `request` scope |
|
|
93
94
|
| `provider-type-mismatch` | error | Provider 的 useClass/useValue/useFactory/useExisting 不满足 InjectionToken 的静态类型契约 |
|
|
94
95
|
| `unsupported-provider-helper` | warn(strict 时 error) | functional provider 的动态参数无法安全展开为静态 factory |
|
|
95
96
|
| `dynamic-aspect-reference` | error | aspects 不是显式数组字面量,或包含 spread/表达式/字符串 pointcut |
|
package/dist/cli.js
CHANGED
|
@@ -329,6 +329,59 @@ function readProjectConfig(rootDir) {
|
|
|
329
329
|
};
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// src/util.ts
|
|
333
|
+
function camelName(token) {
|
|
334
|
+
const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
|
|
335
|
+
if (isConstantCase) {
|
|
336
|
+
return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
337
|
+
}
|
|
338
|
+
return token.charAt(0).toLowerCase() + token.slice(1);
|
|
339
|
+
}
|
|
340
|
+
function relativeImportPath(fromDir, toFile) {
|
|
341
|
+
const fromParts = fromDir.split("/").filter(Boolean);
|
|
342
|
+
const toParts = toFile.split("/").filter(Boolean);
|
|
343
|
+
let common = 0;
|
|
344
|
+
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
345
|
+
common += 1;
|
|
346
|
+
}
|
|
347
|
+
const ups = fromParts.length - common;
|
|
348
|
+
const downs = toParts.slice(common);
|
|
349
|
+
const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
|
|
350
|
+
const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
|
|
351
|
+
const joined = segments.join("/");
|
|
352
|
+
return joined.startsWith("..") ? joined : `./${joined}`;
|
|
353
|
+
}
|
|
354
|
+
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
|
|
355
|
+
var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
356
|
+
function isRequestContextToken(token, tokenNames) {
|
|
357
|
+
return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
|
|
358
|
+
}
|
|
359
|
+
function isJobContextToken(token, tokenNames) {
|
|
360
|
+
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
361
|
+
}
|
|
362
|
+
function joinRoutePaths(prefix, path) {
|
|
363
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
364
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
365
|
+
return normalized;
|
|
366
|
+
}
|
|
367
|
+
function findClosestMatch(target, candidates) {
|
|
368
|
+
if (candidates.length === 0)
|
|
369
|
+
return;
|
|
370
|
+
if (candidates.length === 1)
|
|
371
|
+
return candidates[0];
|
|
372
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
373
|
+
const targetNorm = norm(target);
|
|
374
|
+
for (const c of candidates) {
|
|
375
|
+
if (norm(c) === targetNorm)
|
|
376
|
+
return c;
|
|
377
|
+
}
|
|
378
|
+
for (const c of candidates) {
|
|
379
|
+
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
380
|
+
return c;
|
|
381
|
+
}
|
|
382
|
+
return candidates[0];
|
|
383
|
+
}
|
|
384
|
+
|
|
332
385
|
// src/analyze.ts
|
|
333
386
|
var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
|
|
334
387
|
var ROUTE_DECORATORS = {
|
|
@@ -825,12 +878,12 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
825
878
|
if (!decl || !ts3.isClassDeclaration(decl))
|
|
826
879
|
continue;
|
|
827
880
|
const className2 = decl.name?.text ?? el.text;
|
|
828
|
-
const registeredProvider = providers.find((provider) => provider.token === className2);
|
|
881
|
+
const registeredProvider = providers.find((provider) => provider.token === className2 || provider.useClass === className2);
|
|
829
882
|
if (registeredProvider)
|
|
830
883
|
continue;
|
|
831
884
|
const deps = classDeps(decl, ctx);
|
|
832
885
|
const injectable = parseInjectableOptions(decl, ctx);
|
|
833
|
-
const scope = injectable?.scope
|
|
886
|
+
const scope = injectable?.scope ?? "job";
|
|
834
887
|
providers.push({
|
|
835
888
|
token: className2,
|
|
836
889
|
tokenKind: "class",
|
|
@@ -905,12 +958,27 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
905
958
|
const meta = decoratorObjectArg(jobDec);
|
|
906
959
|
if (meta) {
|
|
907
960
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
908
|
-
const
|
|
909
|
-
const
|
|
961
|
+
const className2 = cls.name?.text ?? "<anonymous>";
|
|
962
|
+
const provider = providers.find((candidate2) => candidate2.token === className2 || candidate2.useClass === className2);
|
|
963
|
+
const scope = provider?.scope ?? injectable?.scope ?? "job";
|
|
964
|
+
if (scope === "request") {
|
|
965
|
+
ctx.diagnostics.push({
|
|
966
|
+
severity: "error",
|
|
967
|
+
code: "invalid-job-scope",
|
|
968
|
+
message: `job ${className2} 不能使用 request scope;Job 只能使用 application 或 job scope`,
|
|
969
|
+
file: sourcePath(ctx.rootDir, cls.getSourceFile().fileName),
|
|
970
|
+
line: lineOf(cls),
|
|
971
|
+
suggestion: "移除 request scope,或改用 application/job scope。",
|
|
972
|
+
errorCode: "SC4007",
|
|
973
|
+
docsUrl: "https://supacloud.dev/errors/SC4007"
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${className2}`);
|
|
910
977
|
jobs.push({
|
|
911
|
-
className:
|
|
912
|
-
name: stringLiteralProp(meta, "name") ??
|
|
913
|
-
|
|
978
|
+
className: className2,
|
|
979
|
+
name: stringLiteralProp(meta, "name") ?? className2,
|
|
980
|
+
serviceKey: camelName(provider?.token ?? className2),
|
|
981
|
+
scope,
|
|
914
982
|
...aspects2.length > 0 ? { aspects: aspects2 } : {}
|
|
915
983
|
});
|
|
916
984
|
}
|
|
@@ -2116,61 +2184,6 @@ function warn(ctx, code, message, file, line) {
|
|
|
2116
2184
|
import { createHash as createHash4 } from "node:crypto";
|
|
2117
2185
|
import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
2118
2186
|
import { join as join2 } from "node:path";
|
|
2119
|
-
|
|
2120
|
-
// src/util.ts
|
|
2121
|
-
function camelName(token) {
|
|
2122
|
-
const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
|
|
2123
|
-
if (isConstantCase) {
|
|
2124
|
-
return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2125
|
-
}
|
|
2126
|
-
return token.charAt(0).toLowerCase() + token.slice(1);
|
|
2127
|
-
}
|
|
2128
|
-
function relativeImportPath(fromDir, toFile) {
|
|
2129
|
-
const fromParts = fromDir.split("/").filter(Boolean);
|
|
2130
|
-
const toParts = toFile.split("/").filter(Boolean);
|
|
2131
|
-
let common = 0;
|
|
2132
|
-
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
2133
|
-
common += 1;
|
|
2134
|
-
}
|
|
2135
|
-
const ups = fromParts.length - common;
|
|
2136
|
-
const downs = toParts.slice(common);
|
|
2137
|
-
const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
|
|
2138
|
-
const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
|
|
2139
|
-
const joined = segments.join("/");
|
|
2140
|
-
return joined.startsWith("..") ? joined : `./${joined}`;
|
|
2141
|
-
}
|
|
2142
|
-
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
|
|
2143
|
-
var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
2144
|
-
function isRequestContextToken(token, tokenNames) {
|
|
2145
|
-
return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
|
|
2146
|
-
}
|
|
2147
|
-
function isJobContextToken(token, tokenNames) {
|
|
2148
|
-
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
2149
|
-
}
|
|
2150
|
-
function joinRoutePaths(prefix, path) {
|
|
2151
|
-
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
2152
|
-
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
2153
|
-
return normalized;
|
|
2154
|
-
}
|
|
2155
|
-
function findClosestMatch(target, candidates) {
|
|
2156
|
-
if (candidates.length === 0)
|
|
2157
|
-
return;
|
|
2158
|
-
if (candidates.length === 1)
|
|
2159
|
-
return candidates[0];
|
|
2160
|
-
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2161
|
-
const targetNorm = norm(target);
|
|
2162
|
-
for (const c of candidates) {
|
|
2163
|
-
if (norm(c) === targetNorm)
|
|
2164
|
-
return c;
|
|
2165
|
-
}
|
|
2166
|
-
for (const c of candidates) {
|
|
2167
|
-
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
2168
|
-
return c;
|
|
2169
|
-
}
|
|
2170
|
-
return candidates[0];
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// src/generate.ts
|
|
2174
2187
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
2175
2188
|
var INTERFACES = `export interface CompiledRoute {
|
|
2176
2189
|
method: string;
|
|
@@ -2258,13 +2271,13 @@ export interface CompiledModule {
|
|
|
2258
2271
|
services: Record<string, unknown>,
|
|
2259
2272
|
ctx: unknown,
|
|
2260
2273
|
imported?: Record<string, Record<string, unknown>>,
|
|
2261
|
-
): Record<string, unknown
|
|
2274
|
+
): Promise<Record<string, unknown>>;
|
|
2262
2275
|
destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
|
|
2263
2276
|
createJobScope?(
|
|
2264
2277
|
services: Record<string, unknown>,
|
|
2265
2278
|
ctx: unknown,
|
|
2266
2279
|
imported?: Record<string, Record<string, unknown>>,
|
|
2267
|
-
): Record<string, unknown
|
|
2280
|
+
): Promise<Record<string, unknown>>;
|
|
2268
2281
|
destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
|
|
2269
2282
|
controllers: CompiledController[];
|
|
2270
2283
|
commands: CompiledCommand[];
|
|
@@ -2742,7 +2755,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2742
2755
|
const jobs = this.module.jobs ?? [];
|
|
2743
2756
|
if (jobs.length === 0)
|
|
2744
2757
|
return "[]";
|
|
2745
|
-
return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(
|
|
2758
|
+
return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(job.serviceKey)}, scope: ${JSON.stringify(job.scope)},${job.aspects && job.aspects.length > 0 ? ` aspects: ${this.renderAspects(job.aspects)},` : ""} }`).join(", ")}]`;
|
|
2746
2759
|
}
|
|
2747
2760
|
renderAspects(aspects) {
|
|
2748
2761
|
return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
|
|
@@ -2761,12 +2774,22 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2761
2774
|
renderScopeFactory(kind) {
|
|
2762
2775
|
const suffix = kind === "request" ? "RequestScope" : "JobScope";
|
|
2763
2776
|
return [
|
|
2764
|
-
`function create${this.pascal}${suffix}(`,
|
|
2777
|
+
`async function create${this.pascal}${suffix}(`,
|
|
2765
2778
|
` services: Record<string, unknown>,`,
|
|
2766
2779
|
` ctx: unknown,`,
|
|
2767
2780
|
` imported: Record<string, Record<string, unknown>> = {},`,
|
|
2768
|
-
`): Record<string, unknown
|
|
2769
|
-
|
|
2781
|
+
`): Promise<Record<string, unknown>> {`,
|
|
2782
|
+
` const scope: Record<string, unknown> = {};`,
|
|
2783
|
+
` try {`,
|
|
2784
|
+
indent(this.renderFactoryBody(kind, true), 4),
|
|
2785
|
+
` } catch (error) {`,
|
|
2786
|
+
` try {`,
|
|
2787
|
+
` await destroy${this.pascal}${suffix}(scope);`,
|
|
2788
|
+
` } catch (cleanupError) {`,
|
|
2789
|
+
` console.error("supacloud: ${kind} scope rollback failed for ${this.module.name}", cleanupError);`,
|
|
2790
|
+
` }`,
|
|
2791
|
+
` throw error;`,
|
|
2792
|
+
` }`,
|
|
2770
2793
|
`}`
|
|
2771
2794
|
].join(`
|
|
2772
2795
|
`);
|
|
@@ -2795,7 +2818,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2795
2818
|
].join(`
|
|
2796
2819
|
`);
|
|
2797
2820
|
}
|
|
2798
|
-
renderFactoryBody(kind) {
|
|
2821
|
+
renderFactoryBody(kind, scoped = false) {
|
|
2799
2822
|
const providers = orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind));
|
|
2800
2823
|
const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
|
|
2801
2824
|
const lines = [];
|
|
@@ -2806,6 +2829,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2806
2829
|
const emitted = this.emitProvider(provider, kind, true);
|
|
2807
2830
|
if (emitted.constLine)
|
|
2808
2831
|
lines.push(emitted.constLine);
|
|
2832
|
+
if (scoped) {
|
|
2833
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = [...(Array.isArray(scope[${JSON.stringify(emitted.key)}]) ? scope[${JSON.stringify(emitted.key)}] : []), ${emitted.expr}];`);
|
|
2834
|
+
}
|
|
2809
2835
|
const list = multiGroups.get(emitted.key) ?? [];
|
|
2810
2836
|
list.push(emitted.expr);
|
|
2811
2837
|
multiGroups.set(emitted.key, list);
|
|
@@ -2813,6 +2839,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2813
2839
|
const emitted = this.emitProvider(provider, kind, false);
|
|
2814
2840
|
if (emitted.constLine)
|
|
2815
2841
|
lines.push(emitted.constLine);
|
|
2842
|
+
if (scoped) {
|
|
2843
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
|
|
2844
|
+
}
|
|
2816
2845
|
returns.set(emitted.key, emitted.expr);
|
|
2817
2846
|
}
|
|
2818
2847
|
}
|
|
@@ -2822,8 +2851,16 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2822
2851
|
for (const controller of controllers) {
|
|
2823
2852
|
const emitted = this.emitController(controller, kind);
|
|
2824
2853
|
lines.push(emitted.constLine);
|
|
2854
|
+
if (scoped) {
|
|
2855
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
|
|
2856
|
+
}
|
|
2825
2857
|
returns.set(emitted.key, emitted.expr);
|
|
2826
2858
|
}
|
|
2859
|
+
if (scoped) {
|
|
2860
|
+
lines.push(`return scope;`);
|
|
2861
|
+
return lines.join(`
|
|
2862
|
+
`);
|
|
2863
|
+
}
|
|
2827
2864
|
const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
|
|
2828
2865
|
lines.push(`return { ${entries.join(", ")} };`);
|
|
2829
2866
|
return lines.join(`
|
|
@@ -3019,13 +3056,9 @@ function renderClient(graph, _options) {
|
|
|
3019
3056
|
title: route.title,
|
|
3020
3057
|
data: route.data
|
|
3021
3058
|
});
|
|
3059
|
+
const routeParams = route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? `{ ${(route.pathParams && route.pathParams.length > 0 ? route.pathParams : (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1))).map((p) => `${p}: string | number`).join("; ")} }` : "Record<string, string | number>";
|
|
3022
3060
|
routeMethods.push(`
|
|
3023
|
-
${route.handler}: (
|
|
3024
|
-
params${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : "?"}: ${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? `{ ${(route.pathParams && route.pathParams.length > 0 ? route.pathParams : (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1))).map((p) => `${p}: string | number`).join("; ")} }` : "Record<string, string | number>"};
|
|
3025
|
-
query?: Record<string, unknown>;
|
|
3026
|
-
body?: unknown;
|
|
3027
|
-
headers?: Record<string, string>;
|
|
3028
|
-
}${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : " = {}"}) => request(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, options),`);
|
|
3061
|
+
${route.handler}: makeRoute<{ params${routeParams.startsWith("{") ? "" : "?"}: ${routeParams}; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, string> }>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}),`);
|
|
3029
3062
|
}
|
|
3030
3063
|
controllerEntries.push(`
|
|
3031
3064
|
${controllerKey}: {${routeMethods.join("")}
|
|
@@ -3042,6 +3075,13 @@ function renderClient(graph, _options) {
|
|
|
3042
3075
|
" headers?: Record<string, string>;",
|
|
3043
3076
|
"}",
|
|
3044
3077
|
"",
|
|
3078
|
+
"export type ResponseDecoder<T> = (value: unknown) => T;",
|
|
3079
|
+
"",
|
|
3080
|
+
"export type RouteMethod<Options extends ClientRequestOptions = ClientRequestOptions> = {",
|
|
3081
|
+
" <T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
|
|
3082
|
+
" (options?: Options): Promise<unknown>;",
|
|
3083
|
+
"};",
|
|
3084
|
+
"",
|
|
3045
3085
|
"export type HttpInterceptorFn = (",
|
|
3046
3086
|
" req: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
|
|
3047
3087
|
" next: (req: { method: string; url: string; headers: Record<string, string>; body?: unknown }) => Promise<Response>,",
|
|
@@ -3087,11 +3127,23 @@ function renderClient(graph, _options) {
|
|
|
3087
3127
|
" const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
|
|
3088
3128
|
' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
|
|
3089
3129
|
"",
|
|
3090
|
-
" async function request<T
|
|
3130
|
+
" async function request<T>(",
|
|
3131
|
+
" method: string,",
|
|
3132
|
+
" path: string,",
|
|
3133
|
+
" options: ClientRequestOptions,",
|
|
3134
|
+
" decode: ResponseDecoder<T>,",
|
|
3135
|
+
" ): Promise<T>;",
|
|
3136
|
+
" async function request(",
|
|
3137
|
+
" method: string,",
|
|
3138
|
+
" path: string,",
|
|
3139
|
+
" options?: ClientRequestOptions,",
|
|
3140
|
+
" ): Promise<unknown>;",
|
|
3141
|
+
" async function request<T>(",
|
|
3091
3142
|
" method: string,",
|
|
3092
3143
|
" path: string,",
|
|
3093
3144
|
" options: ClientRequestOptions = {},",
|
|
3094
|
-
"
|
|
3145
|
+
" decode?: ResponseDecoder<T>,",
|
|
3146
|
+
" ): Promise<T | unknown> {",
|
|
3095
3147
|
" let url = `${baseUrl}${path}`;",
|
|
3096
3148
|
" if (options.params) {",
|
|
3097
3149
|
" for (const [key, value] of Object.entries(options.params)) {",
|
|
@@ -3132,10 +3184,23 @@ function renderClient(graph, _options) {
|
|
|
3132
3184
|
" throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
|
|
3133
3185
|
" }",
|
|
3134
3186
|
' const contentType = response.headers?.get("content-type") ?? "";',
|
|
3187
|
+
" let value: unknown;",
|
|
3135
3188
|
' if (contentType.includes("application/json")) {',
|
|
3136
|
-
"
|
|
3189
|
+
" value = await response.json();",
|
|
3190
|
+
" } else {",
|
|
3191
|
+
" value = await response.text();",
|
|
3192
|
+
" }",
|
|
3193
|
+
" return decode ? decode(value) : value;",
|
|
3194
|
+
" }",
|
|
3195
|
+
"",
|
|
3196
|
+
" function makeRoute<Options extends ClientRequestOptions>(method: string, path: string): RouteMethod<Options> {",
|
|
3197
|
+
" function route<T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
|
|
3198
|
+
" function route(options?: Options): Promise<unknown>;",
|
|
3199
|
+
" function route<T>(options?: Options, decode?: ResponseDecoder<T>): Promise<T | unknown> {",
|
|
3200
|
+
" const requestOptions = options ?? {};",
|
|
3201
|
+
" return decode ? request(method, path, requestOptions, decode) : request(method, path, requestOptions);",
|
|
3137
3202
|
" }",
|
|
3138
|
-
" return
|
|
3203
|
+
" return route;",
|
|
3139
3204
|
" }",
|
|
3140
3205
|
"",
|
|
3141
3206
|
" return {",
|
|
@@ -3432,6 +3497,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3432
3497
|
"command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
|
|
3433
3498
|
"route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
|
|
3434
3499
|
"command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
|
|
3500
|
+
"invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
|
|
3435
3501
|
"dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
|
|
3436
3502
|
"invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
|
|
3437
3503
|
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
package/dist/index.js
CHANGED
|
@@ -324,6 +324,59 @@ function readProjectConfig(rootDir) {
|
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// src/util.ts
|
|
328
|
+
function camelName(token) {
|
|
329
|
+
const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
|
|
330
|
+
if (isConstantCase) {
|
|
331
|
+
return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
332
|
+
}
|
|
333
|
+
return token.charAt(0).toLowerCase() + token.slice(1);
|
|
334
|
+
}
|
|
335
|
+
function relativeImportPath(fromDir, toFile) {
|
|
336
|
+
const fromParts = fromDir.split("/").filter(Boolean);
|
|
337
|
+
const toParts = toFile.split("/").filter(Boolean);
|
|
338
|
+
let common = 0;
|
|
339
|
+
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
340
|
+
common += 1;
|
|
341
|
+
}
|
|
342
|
+
const ups = fromParts.length - common;
|
|
343
|
+
const downs = toParts.slice(common);
|
|
344
|
+
const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
|
|
345
|
+
const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
|
|
346
|
+
const joined = segments.join("/");
|
|
347
|
+
return joined.startsWith("..") ? joined : `./${joined}`;
|
|
348
|
+
}
|
|
349
|
+
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
|
|
350
|
+
var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
351
|
+
function isRequestContextToken(token, tokenNames) {
|
|
352
|
+
return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
|
|
353
|
+
}
|
|
354
|
+
function isJobContextToken(token, tokenNames) {
|
|
355
|
+
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
356
|
+
}
|
|
357
|
+
function joinRoutePaths(prefix, path) {
|
|
358
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
359
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
360
|
+
return normalized;
|
|
361
|
+
}
|
|
362
|
+
function findClosestMatch(target, candidates) {
|
|
363
|
+
if (candidates.length === 0)
|
|
364
|
+
return;
|
|
365
|
+
if (candidates.length === 1)
|
|
366
|
+
return candidates[0];
|
|
367
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
368
|
+
const targetNorm = norm(target);
|
|
369
|
+
for (const c of candidates) {
|
|
370
|
+
if (norm(c) === targetNorm)
|
|
371
|
+
return c;
|
|
372
|
+
}
|
|
373
|
+
for (const c of candidates) {
|
|
374
|
+
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
375
|
+
return c;
|
|
376
|
+
}
|
|
377
|
+
return candidates[0];
|
|
378
|
+
}
|
|
379
|
+
|
|
327
380
|
// src/analyze.ts
|
|
328
381
|
var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
|
|
329
382
|
var ROUTE_DECORATORS = {
|
|
@@ -820,12 +873,12 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
820
873
|
if (!decl || !ts3.isClassDeclaration(decl))
|
|
821
874
|
continue;
|
|
822
875
|
const className2 = decl.name?.text ?? el.text;
|
|
823
|
-
const registeredProvider = providers.find((provider) => provider.token === className2);
|
|
876
|
+
const registeredProvider = providers.find((provider) => provider.token === className2 || provider.useClass === className2);
|
|
824
877
|
if (registeredProvider)
|
|
825
878
|
continue;
|
|
826
879
|
const deps = classDeps(decl, ctx);
|
|
827
880
|
const injectable = parseInjectableOptions(decl, ctx);
|
|
828
|
-
const scope = injectable?.scope
|
|
881
|
+
const scope = injectable?.scope ?? "job";
|
|
829
882
|
providers.push({
|
|
830
883
|
token: className2,
|
|
831
884
|
tokenKind: "class",
|
|
@@ -900,12 +953,27 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
900
953
|
const meta = decoratorObjectArg(jobDec);
|
|
901
954
|
if (meta) {
|
|
902
955
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
903
|
-
const
|
|
904
|
-
const
|
|
956
|
+
const className2 = cls.name?.text ?? "<anonymous>";
|
|
957
|
+
const provider = providers.find((candidate2) => candidate2.token === className2 || candidate2.useClass === className2);
|
|
958
|
+
const scope = provider?.scope ?? injectable?.scope ?? "job";
|
|
959
|
+
if (scope === "request") {
|
|
960
|
+
ctx.diagnostics.push({
|
|
961
|
+
severity: "error",
|
|
962
|
+
code: "invalid-job-scope",
|
|
963
|
+
message: `job ${className2} 不能使用 request scope;Job 只能使用 application 或 job scope`,
|
|
964
|
+
file: sourcePath(ctx.rootDir, cls.getSourceFile().fileName),
|
|
965
|
+
line: lineOf(cls),
|
|
966
|
+
suggestion: "移除 request scope,或改用 application/job scope。",
|
|
967
|
+
errorCode: "SC4007",
|
|
968
|
+
docsUrl: "https://supacloud.dev/errors/SC4007"
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${className2}`);
|
|
905
972
|
jobs.push({
|
|
906
|
-
className:
|
|
907
|
-
name: stringLiteralProp(meta, "name") ??
|
|
908
|
-
|
|
973
|
+
className: className2,
|
|
974
|
+
name: stringLiteralProp(meta, "name") ?? className2,
|
|
975
|
+
serviceKey: camelName(provider?.token ?? className2),
|
|
976
|
+
scope,
|
|
909
977
|
...aspects2.length > 0 ? { aspects: aspects2 } : {}
|
|
910
978
|
});
|
|
911
979
|
}
|
|
@@ -2110,61 +2178,6 @@ function warn(ctx, code, message, file, line) {
|
|
|
2110
2178
|
import { createHash as createHash4 } from "node:crypto";
|
|
2111
2179
|
import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
2112
2180
|
import { join as join2 } from "node:path";
|
|
2113
|
-
|
|
2114
|
-
// src/util.ts
|
|
2115
|
-
function camelName(token) {
|
|
2116
|
-
const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
|
|
2117
|
-
if (isConstantCase) {
|
|
2118
|
-
return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2119
|
-
}
|
|
2120
|
-
return token.charAt(0).toLowerCase() + token.slice(1);
|
|
2121
|
-
}
|
|
2122
|
-
function relativeImportPath(fromDir, toFile) {
|
|
2123
|
-
const fromParts = fromDir.split("/").filter(Boolean);
|
|
2124
|
-
const toParts = toFile.split("/").filter(Boolean);
|
|
2125
|
-
let common = 0;
|
|
2126
|
-
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
2127
|
-
common += 1;
|
|
2128
|
-
}
|
|
2129
|
-
const ups = fromParts.length - common;
|
|
2130
|
-
const downs = toParts.slice(common);
|
|
2131
|
-
const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
|
|
2132
|
-
const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
|
|
2133
|
-
const joined = segments.join("/");
|
|
2134
|
-
return joined.startsWith("..") ? joined : `./${joined}`;
|
|
2135
|
-
}
|
|
2136
|
-
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
|
|
2137
|
-
var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
2138
|
-
function isRequestContextToken(token, tokenNames) {
|
|
2139
|
-
return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
|
|
2140
|
-
}
|
|
2141
|
-
function isJobContextToken(token, tokenNames) {
|
|
2142
|
-
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
2143
|
-
}
|
|
2144
|
-
function joinRoutePaths(prefix, path) {
|
|
2145
|
-
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
2146
|
-
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
2147
|
-
return normalized;
|
|
2148
|
-
}
|
|
2149
|
-
function findClosestMatch(target, candidates) {
|
|
2150
|
-
if (candidates.length === 0)
|
|
2151
|
-
return;
|
|
2152
|
-
if (candidates.length === 1)
|
|
2153
|
-
return candidates[0];
|
|
2154
|
-
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2155
|
-
const targetNorm = norm(target);
|
|
2156
|
-
for (const c of candidates) {
|
|
2157
|
-
if (norm(c) === targetNorm)
|
|
2158
|
-
return c;
|
|
2159
|
-
}
|
|
2160
|
-
for (const c of candidates) {
|
|
2161
|
-
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
2162
|
-
return c;
|
|
2163
|
-
}
|
|
2164
|
-
return candidates[0];
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
// src/generate.ts
|
|
2168
2181
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
2169
2182
|
var INTERFACES = `export interface CompiledRoute {
|
|
2170
2183
|
method: string;
|
|
@@ -2252,13 +2265,13 @@ export interface CompiledModule {
|
|
|
2252
2265
|
services: Record<string, unknown>,
|
|
2253
2266
|
ctx: unknown,
|
|
2254
2267
|
imported?: Record<string, Record<string, unknown>>,
|
|
2255
|
-
): Record<string, unknown
|
|
2268
|
+
): Promise<Record<string, unknown>>;
|
|
2256
2269
|
destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
|
|
2257
2270
|
createJobScope?(
|
|
2258
2271
|
services: Record<string, unknown>,
|
|
2259
2272
|
ctx: unknown,
|
|
2260
2273
|
imported?: Record<string, Record<string, unknown>>,
|
|
2261
|
-
): Record<string, unknown
|
|
2274
|
+
): Promise<Record<string, unknown>>;
|
|
2262
2275
|
destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
|
|
2263
2276
|
controllers: CompiledController[];
|
|
2264
2277
|
commands: CompiledCommand[];
|
|
@@ -2736,7 +2749,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2736
2749
|
const jobs = this.module.jobs ?? [];
|
|
2737
2750
|
if (jobs.length === 0)
|
|
2738
2751
|
return "[]";
|
|
2739
|
-
return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(
|
|
2752
|
+
return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(job.serviceKey)}, scope: ${JSON.stringify(job.scope)},${job.aspects && job.aspects.length > 0 ? ` aspects: ${this.renderAspects(job.aspects)},` : ""} }`).join(", ")}]`;
|
|
2740
2753
|
}
|
|
2741
2754
|
renderAspects(aspects) {
|
|
2742
2755
|
return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
|
|
@@ -2755,12 +2768,22 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2755
2768
|
renderScopeFactory(kind) {
|
|
2756
2769
|
const suffix = kind === "request" ? "RequestScope" : "JobScope";
|
|
2757
2770
|
return [
|
|
2758
|
-
`function create${this.pascal}${suffix}(`,
|
|
2771
|
+
`async function create${this.pascal}${suffix}(`,
|
|
2759
2772
|
` services: Record<string, unknown>,`,
|
|
2760
2773
|
` ctx: unknown,`,
|
|
2761
2774
|
` imported: Record<string, Record<string, unknown>> = {},`,
|
|
2762
|
-
`): Record<string, unknown
|
|
2763
|
-
|
|
2775
|
+
`): Promise<Record<string, unknown>> {`,
|
|
2776
|
+
` const scope: Record<string, unknown> = {};`,
|
|
2777
|
+
` try {`,
|
|
2778
|
+
indent(this.renderFactoryBody(kind, true), 4),
|
|
2779
|
+
` } catch (error) {`,
|
|
2780
|
+
` try {`,
|
|
2781
|
+
` await destroy${this.pascal}${suffix}(scope);`,
|
|
2782
|
+
` } catch (cleanupError) {`,
|
|
2783
|
+
` console.error("supacloud: ${kind} scope rollback failed for ${this.module.name}", cleanupError);`,
|
|
2784
|
+
` }`,
|
|
2785
|
+
` throw error;`,
|
|
2786
|
+
` }`,
|
|
2764
2787
|
`}`
|
|
2765
2788
|
].join(`
|
|
2766
2789
|
`);
|
|
@@ -2789,7 +2812,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2789
2812
|
].join(`
|
|
2790
2813
|
`);
|
|
2791
2814
|
}
|
|
2792
|
-
renderFactoryBody(kind) {
|
|
2815
|
+
renderFactoryBody(kind, scoped = false) {
|
|
2793
2816
|
const providers = orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind));
|
|
2794
2817
|
const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
|
|
2795
2818
|
const lines = [];
|
|
@@ -2800,6 +2823,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2800
2823
|
const emitted = this.emitProvider(provider, kind, true);
|
|
2801
2824
|
if (emitted.constLine)
|
|
2802
2825
|
lines.push(emitted.constLine);
|
|
2826
|
+
if (scoped) {
|
|
2827
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = [...(Array.isArray(scope[${JSON.stringify(emitted.key)}]) ? scope[${JSON.stringify(emitted.key)}] : []), ${emitted.expr}];`);
|
|
2828
|
+
}
|
|
2803
2829
|
const list = multiGroups.get(emitted.key) ?? [];
|
|
2804
2830
|
list.push(emitted.expr);
|
|
2805
2831
|
multiGroups.set(emitted.key, list);
|
|
@@ -2807,6 +2833,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2807
2833
|
const emitted = this.emitProvider(provider, kind, false);
|
|
2808
2834
|
if (emitted.constLine)
|
|
2809
2835
|
lines.push(emitted.constLine);
|
|
2836
|
+
if (scoped) {
|
|
2837
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
|
|
2838
|
+
}
|
|
2810
2839
|
returns.set(emitted.key, emitted.expr);
|
|
2811
2840
|
}
|
|
2812
2841
|
}
|
|
@@ -2816,8 +2845,16 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2816
2845
|
for (const controller of controllers) {
|
|
2817
2846
|
const emitted = this.emitController(controller, kind);
|
|
2818
2847
|
lines.push(emitted.constLine);
|
|
2848
|
+
if (scoped) {
|
|
2849
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
|
|
2850
|
+
}
|
|
2819
2851
|
returns.set(emitted.key, emitted.expr);
|
|
2820
2852
|
}
|
|
2853
|
+
if (scoped) {
|
|
2854
|
+
lines.push(`return scope;`);
|
|
2855
|
+
return lines.join(`
|
|
2856
|
+
`);
|
|
2857
|
+
}
|
|
2821
2858
|
const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
|
|
2822
2859
|
lines.push(`return { ${entries.join(", ")} };`);
|
|
2823
2860
|
return lines.join(`
|
|
@@ -3013,13 +3050,9 @@ function renderClient(graph, _options) {
|
|
|
3013
3050
|
title: route.title,
|
|
3014
3051
|
data: route.data
|
|
3015
3052
|
});
|
|
3053
|
+
const routeParams = route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? `{ ${(route.pathParams && route.pathParams.length > 0 ? route.pathParams : (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1))).map((p) => `${p}: string | number`).join("; ")} }` : "Record<string, string | number>";
|
|
3016
3054
|
routeMethods.push(`
|
|
3017
|
-
${route.handler}: (
|
|
3018
|
-
params${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : "?"}: ${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? `{ ${(route.pathParams && route.pathParams.length > 0 ? route.pathParams : (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1))).map((p) => `${p}: string | number`).join("; ")} }` : "Record<string, string | number>"};
|
|
3019
|
-
query?: Record<string, unknown>;
|
|
3020
|
-
body?: unknown;
|
|
3021
|
-
headers?: Record<string, string>;
|
|
3022
|
-
}${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : " = {}"}) => request(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, options),`);
|
|
3055
|
+
${route.handler}: makeRoute<{ params${routeParams.startsWith("{") ? "" : "?"}: ${routeParams}; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, string> }>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}),`);
|
|
3023
3056
|
}
|
|
3024
3057
|
controllerEntries.push(`
|
|
3025
3058
|
${controllerKey}: {${routeMethods.join("")}
|
|
@@ -3036,6 +3069,13 @@ function renderClient(graph, _options) {
|
|
|
3036
3069
|
" headers?: Record<string, string>;",
|
|
3037
3070
|
"}",
|
|
3038
3071
|
"",
|
|
3072
|
+
"export type ResponseDecoder<T> = (value: unknown) => T;",
|
|
3073
|
+
"",
|
|
3074
|
+
"export type RouteMethod<Options extends ClientRequestOptions = ClientRequestOptions> = {",
|
|
3075
|
+
" <T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
|
|
3076
|
+
" (options?: Options): Promise<unknown>;",
|
|
3077
|
+
"};",
|
|
3078
|
+
"",
|
|
3039
3079
|
"export type HttpInterceptorFn = (",
|
|
3040
3080
|
" req: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
|
|
3041
3081
|
" next: (req: { method: string; url: string; headers: Record<string, string>; body?: unknown }) => Promise<Response>,",
|
|
@@ -3081,11 +3121,23 @@ function renderClient(graph, _options) {
|
|
|
3081
3121
|
" const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
|
|
3082
3122
|
' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
|
|
3083
3123
|
"",
|
|
3084
|
-
" async function request<T
|
|
3124
|
+
" async function request<T>(",
|
|
3125
|
+
" method: string,",
|
|
3126
|
+
" path: string,",
|
|
3127
|
+
" options: ClientRequestOptions,",
|
|
3128
|
+
" decode: ResponseDecoder<T>,",
|
|
3129
|
+
" ): Promise<T>;",
|
|
3130
|
+
" async function request(",
|
|
3131
|
+
" method: string,",
|
|
3132
|
+
" path: string,",
|
|
3133
|
+
" options?: ClientRequestOptions,",
|
|
3134
|
+
" ): Promise<unknown>;",
|
|
3135
|
+
" async function request<T>(",
|
|
3085
3136
|
" method: string,",
|
|
3086
3137
|
" path: string,",
|
|
3087
3138
|
" options: ClientRequestOptions = {},",
|
|
3088
|
-
"
|
|
3139
|
+
" decode?: ResponseDecoder<T>,",
|
|
3140
|
+
" ): Promise<T | unknown> {",
|
|
3089
3141
|
" let url = `${baseUrl}${path}`;",
|
|
3090
3142
|
" if (options.params) {",
|
|
3091
3143
|
" for (const [key, value] of Object.entries(options.params)) {",
|
|
@@ -3126,10 +3178,23 @@ function renderClient(graph, _options) {
|
|
|
3126
3178
|
" throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
|
|
3127
3179
|
" }",
|
|
3128
3180
|
' const contentType = response.headers?.get("content-type") ?? "";',
|
|
3181
|
+
" let value: unknown;",
|
|
3129
3182
|
' if (contentType.includes("application/json")) {',
|
|
3130
|
-
"
|
|
3183
|
+
" value = await response.json();",
|
|
3184
|
+
" } else {",
|
|
3185
|
+
" value = await response.text();",
|
|
3186
|
+
" }",
|
|
3187
|
+
" return decode ? decode(value) : value;",
|
|
3188
|
+
" }",
|
|
3189
|
+
"",
|
|
3190
|
+
" function makeRoute<Options extends ClientRequestOptions>(method: string, path: string): RouteMethod<Options> {",
|
|
3191
|
+
" function route<T>(options: Options, decode: ResponseDecoder<T>): Promise<T>;",
|
|
3192
|
+
" function route(options?: Options): Promise<unknown>;",
|
|
3193
|
+
" function route<T>(options?: Options, decode?: ResponseDecoder<T>): Promise<T | unknown> {",
|
|
3194
|
+
" const requestOptions = options ?? {};",
|
|
3195
|
+
" return decode ? request(method, path, requestOptions, decode) : request(method, path, requestOptions);",
|
|
3131
3196
|
" }",
|
|
3132
|
-
" return
|
|
3197
|
+
" return route;",
|
|
3133
3198
|
" }",
|
|
3134
3199
|
"",
|
|
3135
3200
|
" return {",
|
|
@@ -3426,6 +3491,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3426
3491
|
"command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
|
|
3427
3492
|
"route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
|
|
3428
3493
|
"command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
|
|
3494
|
+
"invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
|
|
3429
3495
|
"dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
|
|
3430
3496
|
"invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
|
|
3431
3497
|
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
package/dist/types.d.ts
CHANGED
|
@@ -171,6 +171,8 @@ export interface CommandNode {
|
|
|
171
171
|
export interface JobNode {
|
|
172
172
|
className: string;
|
|
173
173
|
name: string;
|
|
174
|
+
/** Generated services key; follows a custom useClass provider token when present. */
|
|
175
|
+
serviceKey: string;
|
|
174
176
|
scope: Scope;
|
|
175
177
|
aspects?: AspectRefNode[];
|
|
176
178
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/compiler",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Static compiler for @supacloud/app metadata: builds the application graph from AST, validates it, and generates reflection-free factory code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|