@supacloud/compiler 0.40.0 → 0.41.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/README.md +8 -0
- package/dist/cli.js +290 -9
- package/dist/config.d.ts +4 -1
- package/dist/effect-governance.d.ts +3 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +291 -9
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +28 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,6 +8,13 @@ response unions, caller decoders, binary/stream routes and GraphQL stay intact.
|
|
|
8
8
|
These errors do not prove write rollback and do not add automatic retries.
|
|
9
9
|
See [typed clients and recovery](../../docs/framework-composition.md).
|
|
10
10
|
|
|
11
|
+
For Elysia routes, the compiler governs Effect contracts by default:
|
|
12
|
+
`effect.dependencies` documents the runtime environment, `effect.errors`
|
|
13
|
+
documents safe HTTP mappings, and `effect.retry` requires explicit bounded
|
|
14
|
+
attempts. The default policy requires `requireRouteEffects`,
|
|
15
|
+
`requireErrorMappings` and `requireDependencies`. See
|
|
16
|
+
[Effect governance](../../docs/effect-governance.md).
|
|
17
|
+
|
|
11
18
|
## Unified Database Contracts
|
|
12
19
|
|
|
13
20
|
`supacloud-compiler database-contracts database-contracts.json` generates a shared
|
|
@@ -320,6 +327,7 @@ bunx supacloud-compiler dev
|
|
|
320
327
|
| OpenAPI 3.1 module | 开启 |
|
|
321
328
|
| permissions manifest | 开启 |
|
|
322
329
|
| module boundary preset | `modular-monolith` |
|
|
330
|
+
| Effect route governance | 三项严格检查均开启 |
|
|
323
331
|
| provider tree-shaking | 开启 |
|
|
324
332
|
|
|
325
333
|
需要覆盖默认值时,在项目根目录添加 `supacloud.config.ts`:
|
package/dist/cli.js
CHANGED
|
@@ -1237,6 +1237,17 @@ var init_validate = __esm(() => {
|
|
|
1237
1237
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1238
1238
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1239
1239
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
1240
|
+
"invalid-effect-contract": { code: "SC3030", docsUrl: "https://supacloud.dev/errors/SC3030" },
|
|
1241
|
+
"invalid-effect-retry": { code: "SC3031", docsUrl: "https://supacloud.dev/errors/SC3031" },
|
|
1242
|
+
"invalid-effect-timeout": { code: "SC3032", docsUrl: "https://supacloud.dev/errors/SC3032" },
|
|
1243
|
+
"invalid-effect-dependencies": { code: "SC3033", docsUrl: "https://supacloud.dev/errors/SC3033" },
|
|
1244
|
+
"invalid-effect-errors": { code: "SC3034", docsUrl: "https://supacloud.dev/errors/SC3034" },
|
|
1245
|
+
"effect-return-type": { code: "SC3035", docsUrl: "https://supacloud.dev/errors/SC3035" },
|
|
1246
|
+
"effect-contract-required": { code: "SC3036", docsUrl: "https://supacloud.dev/errors/SC3036" },
|
|
1247
|
+
"effect-error-mapping-required": { code: "SC3037", docsUrl: "https://supacloud.dev/errors/SC3037" },
|
|
1248
|
+
"effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
|
|
1249
|
+
"effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
|
|
1250
|
+
"effect-dependency-type": { code: "SC3040", docsUrl: "https://supacloud.dev/errors/SC3040" },
|
|
1240
1251
|
"invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
|
|
1241
1252
|
"invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
|
|
1242
1253
|
"conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
|
|
@@ -1775,6 +1786,9 @@ class ModuleGenerator {
|
|
|
1775
1786
|
});
|
|
1776
1787
|
fields.push(`responses: { ${responseFields.join(", ")} }`);
|
|
1777
1788
|
}
|
|
1789
|
+
if (route.effect !== undefined) {
|
|
1790
|
+
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
1791
|
+
}
|
|
1778
1792
|
if (route.command)
|
|
1779
1793
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
1780
1794
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -3586,6 +3600,14 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3586
3600
|
cookie?: unknown;
|
|
3587
3601
|
response?: unknown;
|
|
3588
3602
|
responses?: Record<string | number, unknown>;
|
|
3603
|
+
effect?: {
|
|
3604
|
+
required: true;
|
|
3605
|
+
errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
|
|
3606
|
+
dependencies?: string[];
|
|
3607
|
+
retry?: "none" | "explicit";
|
|
3608
|
+
maxAttempts?: number;
|
|
3609
|
+
timeoutMs?: number;
|
|
3610
|
+
};
|
|
3589
3611
|
/** Compile-time ownership and transport classification for the route boundary. */
|
|
3590
3612
|
contract?: {
|
|
3591
3613
|
body?: "framework" | "domain";
|
|
@@ -29330,6 +29352,188 @@ function parseController(input, ctx) {
|
|
|
29330
29352
|
Object.assign(route.contract, { [field]: value });
|
|
29331
29353
|
}
|
|
29332
29354
|
}
|
|
29355
|
+
const effectExpr = getProp(optionsObject, "effect");
|
|
29356
|
+
if (effectExpr) {
|
|
29357
|
+
const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
|
|
29358
|
+
if (!effectObject) {
|
|
29359
|
+
ctx.diagnostics.push({
|
|
29360
|
+
severity: "error",
|
|
29361
|
+
code: "invalid-effect-contract",
|
|
29362
|
+
errorCode: "SC3030",
|
|
29363
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
29364
|
+
file,
|
|
29365
|
+
line: lineOf(effectExpr),
|
|
29366
|
+
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
29367
|
+
suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
|
|
29368
|
+
});
|
|
29369
|
+
} else {
|
|
29370
|
+
const effect = { required: true };
|
|
29371
|
+
const requiredExpr = getProp(effectObject, "required");
|
|
29372
|
+
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
29373
|
+
ctx.diagnostics.push({
|
|
29374
|
+
severity: "error",
|
|
29375
|
+
code: "invalid-effect-contract",
|
|
29376
|
+
errorCode: "SC3030",
|
|
29377
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
29378
|
+
file,
|
|
29379
|
+
line: lineOf(requiredExpr),
|
|
29380
|
+
message: `Route ${route.handler} effect.required must be the literal true.`
|
|
29381
|
+
});
|
|
29382
|
+
}
|
|
29383
|
+
const retry = stringLiteralProp(effectObject, "retry");
|
|
29384
|
+
if (retry !== undefined) {
|
|
29385
|
+
if (retry !== "none" && retry !== "explicit") {
|
|
29386
|
+
ctx.diagnostics.push({
|
|
29387
|
+
severity: "error",
|
|
29388
|
+
code: "invalid-effect-retry",
|
|
29389
|
+
errorCode: "SC3031",
|
|
29390
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
29391
|
+
file,
|
|
29392
|
+
line: lineOf(getProp(effectObject, "retry")),
|
|
29393
|
+
message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
|
|
29394
|
+
});
|
|
29395
|
+
} else {
|
|
29396
|
+
effect.retry = retry;
|
|
29397
|
+
}
|
|
29398
|
+
}
|
|
29399
|
+
const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
|
|
29400
|
+
if (maxAttemptsExpr !== undefined) {
|
|
29401
|
+
const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
|
|
29402
|
+
if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
|
|
29403
|
+
ctx.diagnostics.push({
|
|
29404
|
+
severity: "error",
|
|
29405
|
+
code: "invalid-effect-retry",
|
|
29406
|
+
errorCode: "SC3031",
|
|
29407
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
29408
|
+
file,
|
|
29409
|
+
line: lineOf(maxAttemptsExpr),
|
|
29410
|
+
message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
|
|
29411
|
+
});
|
|
29412
|
+
} else {
|
|
29413
|
+
effect.maxAttempts = maxAttempts;
|
|
29414
|
+
}
|
|
29415
|
+
}
|
|
29416
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
29417
|
+
ctx.diagnostics.push({
|
|
29418
|
+
severity: "error",
|
|
29419
|
+
code: "invalid-effect-retry",
|
|
29420
|
+
errorCode: "SC3031",
|
|
29421
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
29422
|
+
file,
|
|
29423
|
+
line: lineOf(effectExpr),
|
|
29424
|
+
message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
|
|
29425
|
+
});
|
|
29426
|
+
}
|
|
29427
|
+
const timeoutExpr = getProp(effectObject, "timeoutMs");
|
|
29428
|
+
if (timeoutExpr !== undefined) {
|
|
29429
|
+
const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
|
|
29430
|
+
if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
|
|
29431
|
+
ctx.diagnostics.push({
|
|
29432
|
+
severity: "error",
|
|
29433
|
+
code: "invalid-effect-timeout",
|
|
29434
|
+
errorCode: "SC3032",
|
|
29435
|
+
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
29436
|
+
file,
|
|
29437
|
+
line: lineOf(timeoutExpr),
|
|
29438
|
+
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
29439
|
+
});
|
|
29440
|
+
} else {
|
|
29441
|
+
effect.timeoutMs = timeoutMs;
|
|
29442
|
+
}
|
|
29443
|
+
}
|
|
29444
|
+
const dependenciesExpr = getProp(effectObject, "dependencies");
|
|
29445
|
+
if (dependenciesExpr !== undefined) {
|
|
29446
|
+
if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
29447
|
+
ctx.diagnostics.push({
|
|
29448
|
+
severity: "error",
|
|
29449
|
+
code: "invalid-effect-dependencies",
|
|
29450
|
+
errorCode: "SC3033",
|
|
29451
|
+
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
29452
|
+
file,
|
|
29453
|
+
line: lineOf(dependenciesExpr),
|
|
29454
|
+
message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
|
|
29455
|
+
});
|
|
29456
|
+
} else {
|
|
29457
|
+
effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
|
|
29458
|
+
}
|
|
29459
|
+
}
|
|
29460
|
+
const errorsExpr = getProp(effectObject, "errors");
|
|
29461
|
+
if (errorsExpr !== undefined) {
|
|
29462
|
+
if (!ts7.isArrayLiteralExpression(errorsExpr)) {
|
|
29463
|
+
ctx.diagnostics.push({
|
|
29464
|
+
severity: "error",
|
|
29465
|
+
code: "invalid-effect-errors",
|
|
29466
|
+
errorCode: "SC3034",
|
|
29467
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
29468
|
+
file,
|
|
29469
|
+
line: lineOf(errorsExpr),
|
|
29470
|
+
message: `Route ${route.handler} effect.errors must be an explicit object array.`
|
|
29471
|
+
});
|
|
29472
|
+
} else {
|
|
29473
|
+
const errors = [];
|
|
29474
|
+
for (const errorExpr of errorsExpr.elements) {
|
|
29475
|
+
if (!ts7.isObjectLiteralExpression(errorExpr)) {
|
|
29476
|
+
ctx.diagnostics.push({
|
|
29477
|
+
severity: "error",
|
|
29478
|
+
code: "invalid-effect-errors",
|
|
29479
|
+
errorCode: "SC3034",
|
|
29480
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
29481
|
+
file,
|
|
29482
|
+
line: lineOf(errorExpr),
|
|
29483
|
+
message: `Route ${route.handler} effect.errors entries must be object literals.`
|
|
29484
|
+
});
|
|
29485
|
+
continue;
|
|
29486
|
+
}
|
|
29487
|
+
const tag = stringLiteralProp(errorExpr, "tag");
|
|
29488
|
+
const code = stringLiteralProp(errorExpr, "code");
|
|
29489
|
+
const status = numberLiteralProp(errorExpr, "status");
|
|
29490
|
+
const message = stringLiteralProp(errorExpr, "message");
|
|
29491
|
+
if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
29492
|
+
ctx.diagnostics.push({
|
|
29493
|
+
severity: "error",
|
|
29494
|
+
code: "invalid-effect-errors",
|
|
29495
|
+
errorCode: "SC3034",
|
|
29496
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
29497
|
+
file,
|
|
29498
|
+
line: lineOf(errorExpr),
|
|
29499
|
+
message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
|
|
29500
|
+
});
|
|
29501
|
+
continue;
|
|
29502
|
+
}
|
|
29503
|
+
errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
|
|
29504
|
+
}
|
|
29505
|
+
effect.errors = errors;
|
|
29506
|
+
}
|
|
29507
|
+
}
|
|
29508
|
+
route.effect = effect;
|
|
29509
|
+
if (!resultType || !/\bEffect(?:\.Effect)?(?:<|\s)/.test(resultType)) {
|
|
29510
|
+
ctx.diagnostics.push({
|
|
29511
|
+
severity: "error",
|
|
29512
|
+
code: "effect-return-type",
|
|
29513
|
+
errorCode: "SC3035",
|
|
29514
|
+
docsUrl: "https://supacloud.dev/errors/SC3035",
|
|
29515
|
+
file,
|
|
29516
|
+
line: lineOf(method),
|
|
29517
|
+
message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
|
|
29518
|
+
suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
|
|
29519
|
+
});
|
|
29520
|
+
}
|
|
29521
|
+
for (const dependency of effect.dependencies ?? []) {
|
|
29522
|
+
if (!resultType?.includes(dependency)) {
|
|
29523
|
+
ctx.diagnostics.push({
|
|
29524
|
+
severity: "error",
|
|
29525
|
+
code: "effect-dependency-type",
|
|
29526
|
+
errorCode: "SC3040",
|
|
29527
|
+
docsUrl: "https://supacloud.dev/errors/SC3040",
|
|
29528
|
+
file,
|
|
29529
|
+
line: lineOf(method),
|
|
29530
|
+
message: `Route ${route.handler} declares Effect dependency "${dependency}", but it is absent from the return type environment ${resultType ?? "unknown"}.`,
|
|
29531
|
+
suggestion: "Keep effect.dependencies aligned with the third Effect type parameter."
|
|
29532
|
+
});
|
|
29533
|
+
}
|
|
29534
|
+
}
|
|
29535
|
+
}
|
|
29536
|
+
}
|
|
29333
29537
|
for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
|
|
29334
29538
|
const schemaExpr = getProp(optionsObject, field);
|
|
29335
29539
|
if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
|
|
@@ -29913,6 +30117,13 @@ function stringLiteralProp(obj, name) {
|
|
|
29913
30117
|
const expr = getProp(obj, name);
|
|
29914
30118
|
return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
|
|
29915
30119
|
}
|
|
30120
|
+
function numberLiteralProp(obj, name) {
|
|
30121
|
+
const expr = getProp(obj, name);
|
|
30122
|
+
if (!expr || !ts7.isNumericLiteral(expr))
|
|
30123
|
+
return;
|
|
30124
|
+
const value = Number(expr.text);
|
|
30125
|
+
return Number.isFinite(value) ? value : undefined;
|
|
30126
|
+
}
|
|
29916
30127
|
function arrayProp(obj, name) {
|
|
29917
30128
|
const expr = getProp(obj, name);
|
|
29918
30129
|
return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
|
|
@@ -30574,6 +30785,57 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
30574
30785
|
return diagnostics;
|
|
30575
30786
|
}
|
|
30576
30787
|
|
|
30788
|
+
// src/effect-governance.ts
|
|
30789
|
+
init_validate();
|
|
30790
|
+
function diagnostic(code, message, file, suggestion) {
|
|
30791
|
+
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
30792
|
+
return {
|
|
30793
|
+
severity: "error",
|
|
30794
|
+
code,
|
|
30795
|
+
message,
|
|
30796
|
+
...file === undefined ? {} : { file },
|
|
30797
|
+
...suggestion === undefined ? {} : { suggestion },
|
|
30798
|
+
...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
|
|
30799
|
+
};
|
|
30800
|
+
}
|
|
30801
|
+
function routeLabel(route, controllerPath) {
|
|
30802
|
+
return `${route.method} ${controllerPath}${route.path}`;
|
|
30803
|
+
}
|
|
30804
|
+
function validateEffectPolicies(graph, options = {}) {
|
|
30805
|
+
const diagnostics = [];
|
|
30806
|
+
for (const module of graph.modules) {
|
|
30807
|
+
const commands = new Map(module.commands.map((command) => [command.className, command]));
|
|
30808
|
+
for (const controller of module.controllers) {
|
|
30809
|
+
for (const route of controller.routes) {
|
|
30810
|
+
const label = routeLabel(route, controller.path);
|
|
30811
|
+
if (options.requireRouteEffects && route.effect === undefined) {
|
|
30812
|
+
diagnostics.push(diagnostic("effect-contract-required", `Route ${label} must declare an Effect contract.`, controller.file, 'Add effect: { dependencies: [...], errors: [...], retry: "none" } to the route options.'));
|
|
30813
|
+
continue;
|
|
30814
|
+
}
|
|
30815
|
+
const effect = route.effect;
|
|
30816
|
+
if (effect === undefined)
|
|
30817
|
+
continue;
|
|
30818
|
+
if (options.requireErrorMappings && effect.errors === undefined) {
|
|
30819
|
+
diagnostics.push(diagnostic("effect-error-mapping-required", `Route ${label} declares an Effect contract without public error mappings.`, controller.file, "Map every expected domain failure to a stable HTTP status and public code."));
|
|
30820
|
+
}
|
|
30821
|
+
if (options.requireDependencies && effect.dependencies === undefined) {
|
|
30822
|
+
diagnostics.push(diagnostic("effect-dependencies-required", `Route ${label} declares an Effect contract without dependencies.`, controller.file, "Declare the logical services required by the Effect environment."));
|
|
30823
|
+
}
|
|
30824
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
30825
|
+
diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
|
|
30826
|
+
}
|
|
30827
|
+
if (route.command && effect.retry === "explicit") {
|
|
30828
|
+
const command = commands.get(route.command);
|
|
30829
|
+
if (command && command.idempotency !== "required") {
|
|
30830
|
+
diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
|
|
30831
|
+
}
|
|
30832
|
+
}
|
|
30833
|
+
}
|
|
30834
|
+
}
|
|
30835
|
+
}
|
|
30836
|
+
return diagnostics;
|
|
30837
|
+
}
|
|
30838
|
+
|
|
30577
30839
|
// src/compile.ts
|
|
30578
30840
|
function withDefaultGovernance(options) {
|
|
30579
30841
|
return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
|
|
@@ -30593,7 +30855,8 @@ async function compileProject(options) {
|
|
|
30593
30855
|
const diagnostics = [
|
|
30594
30856
|
...graph.diagnostics ?? [],
|
|
30595
30857
|
...validateGraph(graph, options),
|
|
30596
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
30858
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
30859
|
+
...validateEffectPolicies(graph, options.effect)
|
|
30597
30860
|
];
|
|
30598
30861
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
30599
30862
|
return { diagnostics, graph, written: [] };
|
|
@@ -30657,7 +30920,8 @@ async function checkProject(options) {
|
|
|
30657
30920
|
const diagnostics = [
|
|
30658
30921
|
...graph.diagnostics ?? [],
|
|
30659
30922
|
...validateGraph(graph, options),
|
|
30660
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
30923
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
30924
|
+
...validateEffectPolicies(graph, options.effect)
|
|
30661
30925
|
];
|
|
30662
30926
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
30663
30927
|
return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
|
|
@@ -31467,7 +31731,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
|
|
|
31467
31731
|
idempotency: true,
|
|
31468
31732
|
transaction: true
|
|
31469
31733
|
},
|
|
31470
|
-
moduleBoundaryPreset: "modular-monolith"
|
|
31734
|
+
moduleBoundaryPreset: "modular-monolith",
|
|
31735
|
+
effect: {
|
|
31736
|
+
requireRouteEffects: true,
|
|
31737
|
+
requireErrorMappings: true,
|
|
31738
|
+
requireDependencies: true
|
|
31739
|
+
}
|
|
31471
31740
|
};
|
|
31472
31741
|
function defineSupacloudConfig(config = {}) {
|
|
31473
31742
|
if (config.delivery !== undefined)
|
|
@@ -31479,7 +31748,8 @@ function defineSupacloudConfig(config = {}) {
|
|
|
31479
31748
|
...DEFAULT_SUPACLOUD_CONFIG,
|
|
31480
31749
|
...config,
|
|
31481
31750
|
include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
31482
|
-
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
|
|
31751
|
+
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
|
|
31752
|
+
effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
|
|
31483
31753
|
};
|
|
31484
31754
|
}
|
|
31485
31755
|
function validateGovernanceConfig(config) {
|
|
@@ -31536,6 +31806,16 @@ function validateGovernanceConfig(config) {
|
|
|
31536
31806
|
if (config[key] !== undefined && typeof config[key] !== "boolean")
|
|
31537
31807
|
throw new Error(`${key} must be a boolean.`);
|
|
31538
31808
|
}
|
|
31809
|
+
if (config.effect !== undefined) {
|
|
31810
|
+
if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => !["requireRouteEffects", "requireErrorMappings", "requireDependencies"].includes(key))) {
|
|
31811
|
+
throw new Error("effect accepts requireRouteEffects, requireErrorMappings and requireDependencies.");
|
|
31812
|
+
}
|
|
31813
|
+
for (const key of ["requireRouteEffects", "requireErrorMappings", "requireDependencies"]) {
|
|
31814
|
+
if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
|
|
31815
|
+
throw new Error(`effect.${key} must be boolean.`);
|
|
31816
|
+
}
|
|
31817
|
+
}
|
|
31818
|
+
}
|
|
31539
31819
|
}
|
|
31540
31820
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
31541
31821
|
const resolved = defineSupacloudConfig(config);
|
|
@@ -31556,6 +31836,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
31556
31836
|
...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
|
|
31557
31837
|
...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
|
|
31558
31838
|
...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
|
|
31839
|
+
effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
|
|
31559
31840
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
31560
31841
|
graphql: resolved.graphql ? {
|
|
31561
31842
|
...resolved.graphql,
|
|
@@ -31644,7 +31925,7 @@ import { createHash as createHash10 } from "node:crypto";
|
|
|
31644
31925
|
init_validate();
|
|
31645
31926
|
var compare3 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
31646
31927
|
var sorted = (values) => [...new Set(values)].sort(compare3);
|
|
31647
|
-
function
|
|
31928
|
+
function diagnostic2(error) {
|
|
31648
31929
|
return {
|
|
31649
31930
|
severity: error.severity,
|
|
31650
31931
|
code: error.code,
|
|
@@ -31669,8 +31950,8 @@ function createDeliveryPlan(graph, input) {
|
|
|
31669
31950
|
}] };
|
|
31670
31951
|
}
|
|
31671
31952
|
const diagnostics = [
|
|
31672
|
-
...(graph.diagnostics ?? []).map(
|
|
31673
|
-
...validateGraph(graph, { strict: true }).map(
|
|
31953
|
+
...(graph.diagnostics ?? []).map(diagnostic2),
|
|
31954
|
+
...validateGraph(graph, { strict: true }).map(diagnostic2)
|
|
31674
31955
|
];
|
|
31675
31956
|
const fail = (code, message, suggestion) => {
|
|
31676
31957
|
diagnostics.push({ severity: "error", code, message, suggestion });
|
|
@@ -32840,10 +33121,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
32840
33121
|
import { resolve as resolve18 } from "node:path";
|
|
32841
33122
|
|
|
32842
33123
|
// src/migration-app-version.ts
|
|
32843
|
-
var MIGRATION_APP_VERSION = "0.
|
|
33124
|
+
var MIGRATION_APP_VERSION = "0.24.0";
|
|
32844
33125
|
|
|
32845
33126
|
// src/migration-elysia-version.ts
|
|
32846
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
33127
|
+
var MIGRATION_ELYSIA_VERSION = "0.28.0";
|
|
32847
33128
|
|
|
32848
33129
|
// src/migration-policy.ts
|
|
32849
33130
|
var cachedCompilerVersion;
|
package/dist/config.d.ts
CHANGED
|
@@ -20,12 +20,14 @@ export interface SupaCloudConfig {
|
|
|
20
20
|
allowRouteCommandBindings?: boolean;
|
|
21
21
|
disallowControllerDirectDb?: boolean;
|
|
22
22
|
detectOrphanModules?: boolean;
|
|
23
|
+
effect?: NonNullable<CompileOptions["effect"]>;
|
|
23
24
|
commandCapabilities?: CommandExecutionCapabilities;
|
|
24
25
|
treeShakeUnusedProviders?: boolean;
|
|
25
26
|
}
|
|
26
|
-
export declare const DEFAULT_SUPACLOUD_CONFIG: Required<Omit<SupaCloudConfig, "include" | "moduleBoundaryPreset" | "moduleBoundaries" | "typeSafety" | "delivery" | "allowRouteCommandBindings" | "disallowControllerDirectDb" | "detectOrphanModules" | "openApi">> & {
|
|
27
|
+
export declare const DEFAULT_SUPACLOUD_CONFIG: Required<Omit<SupaCloudConfig, "include" | "moduleBoundaryPreset" | "moduleBoundaries" | "typeSafety" | "delivery" | "allowRouteCommandBindings" | "disallowControllerDirectDb" | "detectOrphanModules" | "openApi" | "effect">> & {
|
|
27
28
|
include: string[];
|
|
28
29
|
moduleBoundaryPreset: ModuleBoundaryPresetName;
|
|
30
|
+
effect: NonNullable<CompileOptions["effect"]>;
|
|
29
31
|
};
|
|
30
32
|
export declare function defineSupacloudConfig(config?: SupaCloudConfig): SupaCloudConfig;
|
|
31
33
|
export declare function resolveSupacloudConfig(config?: SupaCloudConfig, cwd?: string): {
|
|
@@ -45,6 +47,7 @@ export declare function resolveSupacloudConfig(config?: SupaCloudConfig, cwd?: s
|
|
|
45
47
|
allowRouteCommandBindings?: boolean;
|
|
46
48
|
disallowControllerDirectDb?: boolean;
|
|
47
49
|
detectOrphanModules?: boolean;
|
|
50
|
+
effect?: NonNullable<CompileOptions["effect"]>;
|
|
48
51
|
treeShakeUnusedProviders: boolean;
|
|
49
52
|
graphql?: GraphqlOptions;
|
|
50
53
|
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ApplicationGraph, Diagnostic, EffectCompilerOptions } from "./types";
|
|
2
|
+
/** Validates the static Effect contract without inspecting Effect runtime internals. */
|
|
3
|
+
export declare function validateEffectPolicies(graph: ApplicationGraph, options?: EffectCompilerOptions): Diagnostic[];
|
package/dist/index.d.ts
CHANGED
|
@@ -52,8 +52,9 @@ export { DEFAULT_SUPACLOUD_CONFIG, compileOptionsFromConfig, defineSupacloudConf
|
|
|
52
52
|
export type { SupaCloudConfig } from "./config";
|
|
53
53
|
export { camelName } from "./util";
|
|
54
54
|
export { ANGULAR_ENTERPRISE_RULES, CLEAN_ARCHITECTURE_RULES, MODULAR_MONOLITH_RULES, MODULE_BOUNDARY_PROFILES, getModuleBoundaryPreset, getModuleBoundaryProfile, resolveModuleBoundaries, } from "./profiles";
|
|
55
|
-
export type { ApplicationGraph, AspectRefNode, CachedModuleEntry, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, CompileResult, CompileStats, ControllerNode, DependencyGraphCache, DependencyGraphIndex, Diagnostic, DiagnosticFix, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, JobNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, TypeSafetyOptions, ValidateOptions, WatchEvent, WatchHandle, WatchOptions, FeatureSpecNode, FeatureTransitionNode, GraphqlOptions, GraphqlContractSummary, OpenApiOptions, OpenApiSecurityScheme, OpenApiServer, } from "./types";
|
|
55
|
+
export type { ApplicationGraph, AspectRefNode, CachedModuleEntry, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, CompileResult, CompileStats, ControllerNode, DependencyGraphCache, DependencyGraphIndex, Diagnostic, DiagnosticFix, EffectCompilerOptions, EffectErrorMappingNode, EffectRetryPolicy, EffectRouteNode, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, JobNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, TypeSafetyOptions, ValidateOptions, WatchEvent, WatchHandle, WatchOptions, FeatureSpecNode, FeatureTransitionNode, GraphqlOptions, GraphqlContractSummary, OpenApiOptions, OpenApiSecurityScheme, OpenApiServer, } from "./types";
|
|
56
56
|
export { inspectRouteContracts, validateRouteContracts } from "./route-contracts";
|
|
57
|
+
export { validateEffectPolicies } from "./effect-governance";
|
|
57
58
|
export { pullGraphqlSchema } from "./graphql-schema";
|
|
58
59
|
export type { PullGraphqlSchemaOptions } from "./graphql-schema";
|
|
59
60
|
export { checkGraphqlCompatibility, createRoleSnapshotReport, diffGraphqlSchemas, graphqlFeatureMatrix, introspectionQuery, readGraphqlSchema, schemaFromSource, } from "./graphql-governance";
|
package/dist/index.js
CHANGED
|
@@ -1236,6 +1236,17 @@ var init_validate = __esm(() => {
|
|
|
1236
1236
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1237
1237
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1238
1238
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
1239
|
+
"invalid-effect-contract": { code: "SC3030", docsUrl: "https://supacloud.dev/errors/SC3030" },
|
|
1240
|
+
"invalid-effect-retry": { code: "SC3031", docsUrl: "https://supacloud.dev/errors/SC3031" },
|
|
1241
|
+
"invalid-effect-timeout": { code: "SC3032", docsUrl: "https://supacloud.dev/errors/SC3032" },
|
|
1242
|
+
"invalid-effect-dependencies": { code: "SC3033", docsUrl: "https://supacloud.dev/errors/SC3033" },
|
|
1243
|
+
"invalid-effect-errors": { code: "SC3034", docsUrl: "https://supacloud.dev/errors/SC3034" },
|
|
1244
|
+
"effect-return-type": { code: "SC3035", docsUrl: "https://supacloud.dev/errors/SC3035" },
|
|
1245
|
+
"effect-contract-required": { code: "SC3036", docsUrl: "https://supacloud.dev/errors/SC3036" },
|
|
1246
|
+
"effect-error-mapping-required": { code: "SC3037", docsUrl: "https://supacloud.dev/errors/SC3037" },
|
|
1247
|
+
"effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
|
|
1248
|
+
"effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
|
|
1249
|
+
"effect-dependency-type": { code: "SC3040", docsUrl: "https://supacloud.dev/errors/SC3040" },
|
|
1239
1250
|
"invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
|
|
1240
1251
|
"invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
|
|
1241
1252
|
"conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
|
|
@@ -1774,6 +1785,9 @@ class ModuleGenerator {
|
|
|
1774
1785
|
});
|
|
1775
1786
|
fields.push(`responses: { ${responseFields.join(", ")} }`);
|
|
1776
1787
|
}
|
|
1788
|
+
if (route.effect !== undefined) {
|
|
1789
|
+
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
1790
|
+
}
|
|
1777
1791
|
if (route.command)
|
|
1778
1792
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
1779
1793
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -3585,6 +3599,14 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3585
3599
|
cookie?: unknown;
|
|
3586
3600
|
response?: unknown;
|
|
3587
3601
|
responses?: Record<string | number, unknown>;
|
|
3602
|
+
effect?: {
|
|
3603
|
+
required: true;
|
|
3604
|
+
errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
|
|
3605
|
+
dependencies?: string[];
|
|
3606
|
+
retry?: "none" | "explicit";
|
|
3607
|
+
maxAttempts?: number;
|
|
3608
|
+
timeoutMs?: number;
|
|
3609
|
+
};
|
|
3588
3610
|
/** Compile-time ownership and transport classification for the route boundary. */
|
|
3589
3611
|
contract?: {
|
|
3590
3612
|
body?: "framework" | "domain";
|
|
@@ -29289,6 +29311,188 @@ function parseController(input, ctx) {
|
|
|
29289
29311
|
Object.assign(route.contract, { [field]: value });
|
|
29290
29312
|
}
|
|
29291
29313
|
}
|
|
29314
|
+
const effectExpr = getProp(optionsObject, "effect");
|
|
29315
|
+
if (effectExpr) {
|
|
29316
|
+
const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
|
|
29317
|
+
if (!effectObject) {
|
|
29318
|
+
ctx.diagnostics.push({
|
|
29319
|
+
severity: "error",
|
|
29320
|
+
code: "invalid-effect-contract",
|
|
29321
|
+
errorCode: "SC3030",
|
|
29322
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
29323
|
+
file,
|
|
29324
|
+
line: lineOf(effectExpr),
|
|
29325
|
+
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
29326
|
+
suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
|
|
29327
|
+
});
|
|
29328
|
+
} else {
|
|
29329
|
+
const effect = { required: true };
|
|
29330
|
+
const requiredExpr = getProp(effectObject, "required");
|
|
29331
|
+
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
29332
|
+
ctx.diagnostics.push({
|
|
29333
|
+
severity: "error",
|
|
29334
|
+
code: "invalid-effect-contract",
|
|
29335
|
+
errorCode: "SC3030",
|
|
29336
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
29337
|
+
file,
|
|
29338
|
+
line: lineOf(requiredExpr),
|
|
29339
|
+
message: `Route ${route.handler} effect.required must be the literal true.`
|
|
29340
|
+
});
|
|
29341
|
+
}
|
|
29342
|
+
const retry = stringLiteralProp(effectObject, "retry");
|
|
29343
|
+
if (retry !== undefined) {
|
|
29344
|
+
if (retry !== "none" && retry !== "explicit") {
|
|
29345
|
+
ctx.diagnostics.push({
|
|
29346
|
+
severity: "error",
|
|
29347
|
+
code: "invalid-effect-retry",
|
|
29348
|
+
errorCode: "SC3031",
|
|
29349
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
29350
|
+
file,
|
|
29351
|
+
line: lineOf(getProp(effectObject, "retry")),
|
|
29352
|
+
message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
|
|
29353
|
+
});
|
|
29354
|
+
} else {
|
|
29355
|
+
effect.retry = retry;
|
|
29356
|
+
}
|
|
29357
|
+
}
|
|
29358
|
+
const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
|
|
29359
|
+
if (maxAttemptsExpr !== undefined) {
|
|
29360
|
+
const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
|
|
29361
|
+
if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
|
|
29362
|
+
ctx.diagnostics.push({
|
|
29363
|
+
severity: "error",
|
|
29364
|
+
code: "invalid-effect-retry",
|
|
29365
|
+
errorCode: "SC3031",
|
|
29366
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
29367
|
+
file,
|
|
29368
|
+
line: lineOf(maxAttemptsExpr),
|
|
29369
|
+
message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
|
|
29370
|
+
});
|
|
29371
|
+
} else {
|
|
29372
|
+
effect.maxAttempts = maxAttempts;
|
|
29373
|
+
}
|
|
29374
|
+
}
|
|
29375
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
29376
|
+
ctx.diagnostics.push({
|
|
29377
|
+
severity: "error",
|
|
29378
|
+
code: "invalid-effect-retry",
|
|
29379
|
+
errorCode: "SC3031",
|
|
29380
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
29381
|
+
file,
|
|
29382
|
+
line: lineOf(effectExpr),
|
|
29383
|
+
message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
|
|
29384
|
+
});
|
|
29385
|
+
}
|
|
29386
|
+
const timeoutExpr = getProp(effectObject, "timeoutMs");
|
|
29387
|
+
if (timeoutExpr !== undefined) {
|
|
29388
|
+
const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
|
|
29389
|
+
if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
|
|
29390
|
+
ctx.diagnostics.push({
|
|
29391
|
+
severity: "error",
|
|
29392
|
+
code: "invalid-effect-timeout",
|
|
29393
|
+
errorCode: "SC3032",
|
|
29394
|
+
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
29395
|
+
file,
|
|
29396
|
+
line: lineOf(timeoutExpr),
|
|
29397
|
+
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
29398
|
+
});
|
|
29399
|
+
} else {
|
|
29400
|
+
effect.timeoutMs = timeoutMs;
|
|
29401
|
+
}
|
|
29402
|
+
}
|
|
29403
|
+
const dependenciesExpr = getProp(effectObject, "dependencies");
|
|
29404
|
+
if (dependenciesExpr !== undefined) {
|
|
29405
|
+
if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
29406
|
+
ctx.diagnostics.push({
|
|
29407
|
+
severity: "error",
|
|
29408
|
+
code: "invalid-effect-dependencies",
|
|
29409
|
+
errorCode: "SC3033",
|
|
29410
|
+
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
29411
|
+
file,
|
|
29412
|
+
line: lineOf(dependenciesExpr),
|
|
29413
|
+
message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
|
|
29414
|
+
});
|
|
29415
|
+
} else {
|
|
29416
|
+
effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
|
|
29417
|
+
}
|
|
29418
|
+
}
|
|
29419
|
+
const errorsExpr = getProp(effectObject, "errors");
|
|
29420
|
+
if (errorsExpr !== undefined) {
|
|
29421
|
+
if (!ts7.isArrayLiteralExpression(errorsExpr)) {
|
|
29422
|
+
ctx.diagnostics.push({
|
|
29423
|
+
severity: "error",
|
|
29424
|
+
code: "invalid-effect-errors",
|
|
29425
|
+
errorCode: "SC3034",
|
|
29426
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
29427
|
+
file,
|
|
29428
|
+
line: lineOf(errorsExpr),
|
|
29429
|
+
message: `Route ${route.handler} effect.errors must be an explicit object array.`
|
|
29430
|
+
});
|
|
29431
|
+
} else {
|
|
29432
|
+
const errors = [];
|
|
29433
|
+
for (const errorExpr of errorsExpr.elements) {
|
|
29434
|
+
if (!ts7.isObjectLiteralExpression(errorExpr)) {
|
|
29435
|
+
ctx.diagnostics.push({
|
|
29436
|
+
severity: "error",
|
|
29437
|
+
code: "invalid-effect-errors",
|
|
29438
|
+
errorCode: "SC3034",
|
|
29439
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
29440
|
+
file,
|
|
29441
|
+
line: lineOf(errorExpr),
|
|
29442
|
+
message: `Route ${route.handler} effect.errors entries must be object literals.`
|
|
29443
|
+
});
|
|
29444
|
+
continue;
|
|
29445
|
+
}
|
|
29446
|
+
const tag = stringLiteralProp(errorExpr, "tag");
|
|
29447
|
+
const code = stringLiteralProp(errorExpr, "code");
|
|
29448
|
+
const status = numberLiteralProp(errorExpr, "status");
|
|
29449
|
+
const message = stringLiteralProp(errorExpr, "message");
|
|
29450
|
+
if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
29451
|
+
ctx.diagnostics.push({
|
|
29452
|
+
severity: "error",
|
|
29453
|
+
code: "invalid-effect-errors",
|
|
29454
|
+
errorCode: "SC3034",
|
|
29455
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
29456
|
+
file,
|
|
29457
|
+
line: lineOf(errorExpr),
|
|
29458
|
+
message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
|
|
29459
|
+
});
|
|
29460
|
+
continue;
|
|
29461
|
+
}
|
|
29462
|
+
errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
|
|
29463
|
+
}
|
|
29464
|
+
effect.errors = errors;
|
|
29465
|
+
}
|
|
29466
|
+
}
|
|
29467
|
+
route.effect = effect;
|
|
29468
|
+
if (!resultType || !/\bEffect(?:\.Effect)?(?:<|\s)/.test(resultType)) {
|
|
29469
|
+
ctx.diagnostics.push({
|
|
29470
|
+
severity: "error",
|
|
29471
|
+
code: "effect-return-type",
|
|
29472
|
+
errorCode: "SC3035",
|
|
29473
|
+
docsUrl: "https://supacloud.dev/errors/SC3035",
|
|
29474
|
+
file,
|
|
29475
|
+
line: lineOf(method),
|
|
29476
|
+
message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
|
|
29477
|
+
suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
|
|
29478
|
+
});
|
|
29479
|
+
}
|
|
29480
|
+
for (const dependency of effect.dependencies ?? []) {
|
|
29481
|
+
if (!resultType?.includes(dependency)) {
|
|
29482
|
+
ctx.diagnostics.push({
|
|
29483
|
+
severity: "error",
|
|
29484
|
+
code: "effect-dependency-type",
|
|
29485
|
+
errorCode: "SC3040",
|
|
29486
|
+
docsUrl: "https://supacloud.dev/errors/SC3040",
|
|
29487
|
+
file,
|
|
29488
|
+
line: lineOf(method),
|
|
29489
|
+
message: `Route ${route.handler} declares Effect dependency "${dependency}", but it is absent from the return type environment ${resultType ?? "unknown"}.`,
|
|
29490
|
+
suggestion: "Keep effect.dependencies aligned with the third Effect type parameter."
|
|
29491
|
+
});
|
|
29492
|
+
}
|
|
29493
|
+
}
|
|
29494
|
+
}
|
|
29495
|
+
}
|
|
29292
29496
|
for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
|
|
29293
29497
|
const schemaExpr = getProp(optionsObject, field);
|
|
29294
29498
|
if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
|
|
@@ -29872,6 +30076,13 @@ function stringLiteralProp(obj, name) {
|
|
|
29872
30076
|
const expr = getProp(obj, name);
|
|
29873
30077
|
return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
|
|
29874
30078
|
}
|
|
30079
|
+
function numberLiteralProp(obj, name) {
|
|
30080
|
+
const expr = getProp(obj, name);
|
|
30081
|
+
if (!expr || !ts7.isNumericLiteral(expr))
|
|
30082
|
+
return;
|
|
30083
|
+
const value = Number(expr.text);
|
|
30084
|
+
return Number.isFinite(value) ? value : undefined;
|
|
30085
|
+
}
|
|
29875
30086
|
function arrayProp(obj, name) {
|
|
29876
30087
|
const expr = getProp(obj, name);
|
|
29877
30088
|
return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
|
|
@@ -30535,6 +30746,57 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
30535
30746
|
return diagnostics;
|
|
30536
30747
|
}
|
|
30537
30748
|
|
|
30749
|
+
// src/effect-governance.ts
|
|
30750
|
+
init_validate();
|
|
30751
|
+
function diagnostic(code, message, file, suggestion) {
|
|
30752
|
+
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
30753
|
+
return {
|
|
30754
|
+
severity: "error",
|
|
30755
|
+
code,
|
|
30756
|
+
message,
|
|
30757
|
+
...file === undefined ? {} : { file },
|
|
30758
|
+
...suggestion === undefined ? {} : { suggestion },
|
|
30759
|
+
...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
|
|
30760
|
+
};
|
|
30761
|
+
}
|
|
30762
|
+
function routeLabel(route, controllerPath) {
|
|
30763
|
+
return `${route.method} ${controllerPath}${route.path}`;
|
|
30764
|
+
}
|
|
30765
|
+
function validateEffectPolicies(graph, options = {}) {
|
|
30766
|
+
const diagnostics = [];
|
|
30767
|
+
for (const module of graph.modules) {
|
|
30768
|
+
const commands = new Map(module.commands.map((command) => [command.className, command]));
|
|
30769
|
+
for (const controller of module.controllers) {
|
|
30770
|
+
for (const route of controller.routes) {
|
|
30771
|
+
const label = routeLabel(route, controller.path);
|
|
30772
|
+
if (options.requireRouteEffects && route.effect === undefined) {
|
|
30773
|
+
diagnostics.push(diagnostic("effect-contract-required", `Route ${label} must declare an Effect contract.`, controller.file, 'Add effect: { dependencies: [...], errors: [...], retry: "none" } to the route options.'));
|
|
30774
|
+
continue;
|
|
30775
|
+
}
|
|
30776
|
+
const effect = route.effect;
|
|
30777
|
+
if (effect === undefined)
|
|
30778
|
+
continue;
|
|
30779
|
+
if (options.requireErrorMappings && effect.errors === undefined) {
|
|
30780
|
+
diagnostics.push(diagnostic("effect-error-mapping-required", `Route ${label} declares an Effect contract without public error mappings.`, controller.file, "Map every expected domain failure to a stable HTTP status and public code."));
|
|
30781
|
+
}
|
|
30782
|
+
if (options.requireDependencies && effect.dependencies === undefined) {
|
|
30783
|
+
diagnostics.push(diagnostic("effect-dependencies-required", `Route ${label} declares an Effect contract without dependencies.`, controller.file, "Declare the logical services required by the Effect environment."));
|
|
30784
|
+
}
|
|
30785
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
30786
|
+
diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
|
|
30787
|
+
}
|
|
30788
|
+
if (route.command && effect.retry === "explicit") {
|
|
30789
|
+
const command = commands.get(route.command);
|
|
30790
|
+
if (command && command.idempotency !== "required") {
|
|
30791
|
+
diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
|
|
30792
|
+
}
|
|
30793
|
+
}
|
|
30794
|
+
}
|
|
30795
|
+
}
|
|
30796
|
+
}
|
|
30797
|
+
return diagnostics;
|
|
30798
|
+
}
|
|
30799
|
+
|
|
30538
30800
|
// src/compile.ts
|
|
30539
30801
|
function withDefaultGovernance(options) {
|
|
30540
30802
|
return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
|
|
@@ -30554,7 +30816,8 @@ async function compileProject(options) {
|
|
|
30554
30816
|
const diagnostics = [
|
|
30555
30817
|
...graph.diagnostics ?? [],
|
|
30556
30818
|
...validateGraph(graph, options),
|
|
30557
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
30819
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
30820
|
+
...validateEffectPolicies(graph, options.effect)
|
|
30558
30821
|
];
|
|
30559
30822
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
30560
30823
|
return { diagnostics, graph, written: [] };
|
|
@@ -30618,7 +30881,8 @@ async function checkProject(options) {
|
|
|
30618
30881
|
const diagnostics = [
|
|
30619
30882
|
...graph.diagnostics ?? [],
|
|
30620
30883
|
...validateGraph(graph, options),
|
|
30621
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
30884
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
30885
|
+
...validateEffectPolicies(graph, options.effect)
|
|
30622
30886
|
];
|
|
30623
30887
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
30624
30888
|
return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
|
|
@@ -30704,7 +30968,7 @@ init_delivery_schema();
|
|
|
30704
30968
|
init_validate();
|
|
30705
30969
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
30706
30970
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
30707
|
-
function
|
|
30971
|
+
function diagnostic2(error) {
|
|
30708
30972
|
return {
|
|
30709
30973
|
severity: error.severity,
|
|
30710
30974
|
code: error.code,
|
|
@@ -30729,8 +30993,8 @@ function createDeliveryPlan(graph, input) {
|
|
|
30729
30993
|
}] };
|
|
30730
30994
|
}
|
|
30731
30995
|
const diagnostics = [
|
|
30732
|
-
...(graph.diagnostics ?? []).map(
|
|
30733
|
-
...validateGraph(graph, { strict: true }).map(
|
|
30996
|
+
...(graph.diagnostics ?? []).map(diagnostic2),
|
|
30997
|
+
...validateGraph(graph, { strict: true }).map(diagnostic2)
|
|
30734
30998
|
];
|
|
30735
30999
|
const fail = (code, message, suggestion) => {
|
|
30736
31000
|
diagnostics.push({ severity: "error", code, message, suggestion });
|
|
@@ -32573,10 +32837,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
32573
32837
|
import { resolve as resolve17 } from "node:path";
|
|
32574
32838
|
|
|
32575
32839
|
// src/migration-app-version.ts
|
|
32576
|
-
var MIGRATION_APP_VERSION = "0.
|
|
32840
|
+
var MIGRATION_APP_VERSION = "0.24.0";
|
|
32577
32841
|
|
|
32578
32842
|
// src/migration-elysia-version.ts
|
|
32579
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
32843
|
+
var MIGRATION_ELYSIA_VERSION = "0.28.0";
|
|
32580
32844
|
|
|
32581
32845
|
// src/migration-policy.ts
|
|
32582
32846
|
var cachedCompilerVersion;
|
|
@@ -33746,7 +34010,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
|
|
|
33746
34010
|
idempotency: true,
|
|
33747
34011
|
transaction: true
|
|
33748
34012
|
},
|
|
33749
|
-
moduleBoundaryPreset: "modular-monolith"
|
|
34013
|
+
moduleBoundaryPreset: "modular-monolith",
|
|
34014
|
+
effect: {
|
|
34015
|
+
requireRouteEffects: true,
|
|
34016
|
+
requireErrorMappings: true,
|
|
34017
|
+
requireDependencies: true
|
|
34018
|
+
}
|
|
33750
34019
|
};
|
|
33751
34020
|
function defineSupacloudConfig(config = {}) {
|
|
33752
34021
|
if (config.delivery !== undefined)
|
|
@@ -33758,7 +34027,8 @@ function defineSupacloudConfig(config = {}) {
|
|
|
33758
34027
|
...DEFAULT_SUPACLOUD_CONFIG,
|
|
33759
34028
|
...config,
|
|
33760
34029
|
include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
33761
|
-
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
|
|
34030
|
+
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
|
|
34031
|
+
effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
|
|
33762
34032
|
};
|
|
33763
34033
|
}
|
|
33764
34034
|
function validateGovernanceConfig(config) {
|
|
@@ -33815,6 +34085,16 @@ function validateGovernanceConfig(config) {
|
|
|
33815
34085
|
if (config[key] !== undefined && typeof config[key] !== "boolean")
|
|
33816
34086
|
throw new Error(`${key} must be a boolean.`);
|
|
33817
34087
|
}
|
|
34088
|
+
if (config.effect !== undefined) {
|
|
34089
|
+
if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => !["requireRouteEffects", "requireErrorMappings", "requireDependencies"].includes(key))) {
|
|
34090
|
+
throw new Error("effect accepts requireRouteEffects, requireErrorMappings and requireDependencies.");
|
|
34091
|
+
}
|
|
34092
|
+
for (const key of ["requireRouteEffects", "requireErrorMappings", "requireDependencies"]) {
|
|
34093
|
+
if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
|
|
34094
|
+
throw new Error(`effect.${key} must be boolean.`);
|
|
34095
|
+
}
|
|
34096
|
+
}
|
|
34097
|
+
}
|
|
33818
34098
|
}
|
|
33819
34099
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
33820
34100
|
const resolved = defineSupacloudConfig(config);
|
|
@@ -33835,6 +34115,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
33835
34115
|
...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
|
|
33836
34116
|
...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
|
|
33837
34117
|
...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
|
|
34118
|
+
effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
|
|
33838
34119
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
33839
34120
|
graphql: resolved.graphql ? {
|
|
33840
34121
|
...resolved.graphql,
|
|
@@ -34237,6 +34518,7 @@ export {
|
|
|
34237
34518
|
scanProductionSource,
|
|
34238
34519
|
schemaFromSource,
|
|
34239
34520
|
serializeOpenApiJson,
|
|
34521
|
+
validateEffectPolicies,
|
|
34240
34522
|
validateFeatureSpec,
|
|
34241
34523
|
validateGraph,
|
|
34242
34524
|
validateRouteContracts,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MIGRATION_APP_VERSION = "0.
|
|
1
|
+
export declare const MIGRATION_APP_VERSION = "0.24.0";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MIGRATION_ELYSIA_VERSION = "0.
|
|
1
|
+
export declare const MIGRATION_ELYSIA_VERSION = "0.28.0";
|
package/dist/types.d.ts
CHANGED
|
@@ -185,6 +185,21 @@ export interface HandlerParamNode {
|
|
|
185
185
|
transform?: "number" | "boolean" | "string";
|
|
186
186
|
default?: unknown;
|
|
187
187
|
}
|
|
188
|
+
export type EffectRetryPolicy = "none" | "explicit";
|
|
189
|
+
export interface EffectErrorMappingNode {
|
|
190
|
+
tag: string;
|
|
191
|
+
status: number;
|
|
192
|
+
code: string;
|
|
193
|
+
message?: string;
|
|
194
|
+
}
|
|
195
|
+
export interface EffectRouteNode {
|
|
196
|
+
required: true;
|
|
197
|
+
errors?: EffectErrorMappingNode[];
|
|
198
|
+
dependencies?: string[];
|
|
199
|
+
retry?: EffectRetryPolicy;
|
|
200
|
+
maxAttempts?: number;
|
|
201
|
+
timeoutMs?: number;
|
|
202
|
+
}
|
|
188
203
|
export interface RouteNode {
|
|
189
204
|
parse?: "none";
|
|
190
205
|
allowDeleteBody?: true;
|
|
@@ -205,6 +220,8 @@ export interface RouteNode {
|
|
|
205
220
|
cookie?: string;
|
|
206
221
|
response?: string;
|
|
207
222
|
responses?: Record<string, string>;
|
|
223
|
+
/** Static Effect execution contract for this route. */
|
|
224
|
+
effect?: EffectRouteNode;
|
|
208
225
|
/** @Command-decorated class explicitly bound by the route. */
|
|
209
226
|
command?: string;
|
|
210
227
|
/** Route guards executed before handler (Angular CanActivateFn style). */
|
|
@@ -429,6 +446,16 @@ export interface CompileOptions {
|
|
|
429
446
|
changedPaths?: string[];
|
|
430
447
|
/** Type-safety gates for generated artifacts and production source. */
|
|
431
448
|
typeSafety?: TypeSafetyOptions;
|
|
449
|
+
/** Compile-time Effect governance for routes and command handlers. */
|
|
450
|
+
effect?: EffectCompilerOptions;
|
|
451
|
+
}
|
|
452
|
+
export interface EffectCompilerOptions {
|
|
453
|
+
/** Require every discovered route to declare an Effect contract. */
|
|
454
|
+
requireRouteEffects?: boolean;
|
|
455
|
+
/** Require explicit public mappings for Effect failures. */
|
|
456
|
+
requireErrorMappings?: boolean;
|
|
457
|
+
/** Require an explicit dependency list for Effect environments. */
|
|
458
|
+
requireDependencies?: boolean;
|
|
432
459
|
}
|
|
433
460
|
export interface GraphqlOptions {
|
|
434
461
|
/** Additionally emit standard TypedDocumentNode artifacts for GraphQL ecosystem clients. */
|
|
@@ -527,6 +554,7 @@ export interface ValidateOptions {
|
|
|
527
554
|
disallowControllerDirectDb?: boolean;
|
|
528
555
|
/** Detect modules declared in the project that are unreachable from any root module. */
|
|
529
556
|
detectOrphanModules?: boolean;
|
|
557
|
+
effect?: EffectCompilerOptions;
|
|
530
558
|
}
|
|
531
559
|
/** Runtime capabilities declared by the Command executor. */
|
|
532
560
|
export interface CommandExecutionCapabilities {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
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",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"@whatwg-node/promise-helpers": "1.3.2"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@supabase/supabase-js": "2.
|
|
72
|
-
"drizzle-orm": "1.0.0-rc.
|
|
71
|
+
"@supabase/supabase-js": "2.117.3",
|
|
72
|
+
"drizzle-orm": "1.0.0-rc.5-169397b",
|
|
73
73
|
"@types/bun": "^1.4.2",
|
|
74
74
|
"typescript": "^7.0.2"
|
|
75
75
|
}
|