@supacloud/compiler 0.40.1 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1237,6 +1237,24 @@ 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" },
1251
+ "effect-error-type": { code: "SC3041", docsUrl: "https://supacloud.dev/errors/SC3041" },
1252
+ "effect-error-mapping-type": { code: "SC3042", docsUrl: "https://supacloud.dev/errors/SC3042" },
1253
+ "effect-dependency-mismatch": { code: "SC3043", docsUrl: "https://supacloud.dev/errors/SC3043" },
1254
+ "effect-timeout-required": { code: "SC3044", docsUrl: "https://supacloud.dev/errors/SC3044" },
1255
+ "effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
1256
+ "effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
1257
+ "effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
1240
1258
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1241
1259
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1242
1260
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -1775,6 +1793,9 @@ class ModuleGenerator {
1775
1793
  });
1776
1794
  fields.push(`responses: { ${responseFields.join(", ")} }`);
1777
1795
  }
1796
+ if (route.effect !== undefined) {
1797
+ fields.push(`effect: ${JSON.stringify(route.effect)}`);
1798
+ }
1778
1799
  if (route.command)
1779
1800
  fields.push(`command: ${JSON.stringify(route.command)}`);
1780
1801
  if (route.guards && route.guards.length > 0) {
@@ -3586,6 +3607,14 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
3586
3607
  cookie?: unknown;
3587
3608
  response?: unknown;
3588
3609
  responses?: Record<string | number, unknown>;
3610
+ effect?: {
3611
+ required: true;
3612
+ errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
3613
+ dependencies?: string[];
3614
+ retry?: "none" | "explicit";
3615
+ maxAttempts?: number;
3616
+ timeoutMs?: number;
3617
+ };
3589
3618
  /** Compile-time ownership and transport classification for the route boundary. */
3590
3619
  contract?: {
3591
3620
  body?: "framework" | "domain";
@@ -27793,6 +27822,13 @@ function withDiagnosticMetadata(diagnostic) {
27793
27822
  const metadata = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
27794
27823
  return metadata ? { ...diagnostic, errorCode: metadata.code, docsUrl: metadata.docsUrl } : diagnostic;
27795
27824
  }
27825
+ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
27826
+ requireTaggedErrorTypes: true,
27827
+ requireExactDependencyTypes: true,
27828
+ requireTimeoutForDependencies: true,
27829
+ forbidDirectRuntimeExecution: true,
27830
+ forbidDirectThrows: true
27831
+ };
27796
27832
  function nodeText(node) {
27797
27833
  return node.getText(node.getSourceFile());
27798
27834
  }
@@ -27838,7 +27874,7 @@ function descendantsOfKind(root, predicate) {
27838
27874
  visit(root);
27839
27875
  return result;
27840
27876
  }
27841
- async function analyzeProject(rootDir, include, cache, changedPaths) {
27877
+ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptions = {}) {
27842
27878
  const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
27843
27879
  if (cache)
27844
27880
  cache.programSession = session;
@@ -27857,8 +27893,21 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
27857
27893
  moduleHandlerFiles: new Map,
27858
27894
  infraResources: new Map,
27859
27895
  infraResourceClasses: new Map,
27860
- diagnostics: []
27896
+ diagnostics: [],
27897
+ effectOptions: {
27898
+ ...DEFAULT_EFFECT_ANALYSIS_OPTIONS,
27899
+ requireTaggedErrorTypes: effectOptions.requireTaggedErrorTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTaggedErrorTypes,
27900
+ requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
27901
+ requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
27902
+ forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
27903
+ forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
27904
+ }
27861
27905
  };
27906
+ if (ctx.effectOptions.forbidDirectRuntimeExecution) {
27907
+ for (const sourceFile of sourceFiles) {
27908
+ scanEffectRuntimeEscapes(sourceFile, ctx);
27909
+ }
27910
+ }
27862
27911
  const nativeTraitFiles = new Map;
27863
27912
  for (const diagnostic of session.getDiagnostics()) {
27864
27913
  ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
@@ -29034,11 +29083,150 @@ function declaredClassType(declaration, ctx) {
29034
29083
  return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
29035
29084
  }
29036
29085
  function typeArgumentsOf(type, ctx) {
29037
- return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
29086
+ if (!isTypeReference(type))
29087
+ return [];
29088
+ const argumentsFromChecker = ctx.checker.getTypeArguments(type);
29089
+ if (argumentsFromChecker.length > 0)
29090
+ return argumentsFromChecker;
29091
+ const reference = type;
29092
+ if (reference.resolvedTypeArguments && reference.resolvedTypeArguments.length > 0) {
29093
+ return reference.resolvedTypeArguments;
29094
+ }
29095
+ return reference.aliasTypeArguments ?? [];
29038
29096
  }
29039
29097
  function isTypeReference(type) {
29040
29098
  return "target" in type;
29041
29099
  }
29100
+ function effectTypeArgumentsOf(type, ctx) {
29101
+ if (!type)
29102
+ return;
29103
+ const reference = isTypeReference(type) ? type : undefined;
29104
+ const target = reference?.target ?? type;
29105
+ const symbol = target.aliasSymbol ?? target.getSymbol();
29106
+ const text = ctx.checker.typeToString(type, undefined, ts7.TypeFormatFlags.NoTruncation);
29107
+ if (symbol?.getName() !== "Effect" && !/^(?:Effect\.)?Effect</.test(text))
29108
+ return;
29109
+ const args = typeArgumentsOf(type, ctx);
29110
+ return args.length >= 3 ? [args[0], args[1], args[2]] : undefined;
29111
+ }
29112
+ function effectTypeName(type, location, ctx) {
29113
+ return ctx.checker.typeToString(type, location, ts7.TypeFormatFlags.NoTruncation);
29114
+ }
29115
+ function stringLiteralTypeValue(type) {
29116
+ return (type.flags & ts7.TypeFlags.StringLiteral) !== 0 ? type.value : undefined;
29117
+ }
29118
+ function effectFailureTags(type, location, ctx) {
29119
+ if ((type.flags & ts7.TypeFlags.Never) !== 0)
29120
+ return { tags: [], invalid: false };
29121
+ if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
29122
+ return { tags: [], invalid: true };
29123
+ }
29124
+ if (type.isUnion()) {
29125
+ return type.types.reduce((result, part) => {
29126
+ const next = effectFailureTags(part, location, ctx);
29127
+ result.tags.push(...next.tags);
29128
+ result.invalid ||= next.invalid;
29129
+ return result;
29130
+ }, { tags: [], invalid: false });
29131
+ }
29132
+ const apparent = ctx.checker.getApparentType(type);
29133
+ for (const propertyName of ["_tag", "tag"]) {
29134
+ const property = ctx.checker.getPropertyOfType(apparent, propertyName);
29135
+ if (!property)
29136
+ continue;
29137
+ const propertyType = ctx.checker.getTypeOfSymbolAtLocation(property, location);
29138
+ const tag = stringLiteralTypeValue(propertyType);
29139
+ if (tag)
29140
+ return { tags: [tag], invalid: false };
29141
+ }
29142
+ return { tags: [], invalid: true };
29143
+ }
29144
+ function effectEnvironmentNames(type, location, ctx) {
29145
+ if ((type.flags & ts7.TypeFlags.Never) !== 0)
29146
+ return { names: [], invalid: false };
29147
+ if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
29148
+ return { names: [], invalid: true };
29149
+ }
29150
+ if (type.isUnion() || type.isIntersection()) {
29151
+ return type.types.reduce((result, part) => {
29152
+ const next = effectEnvironmentNames(part, location, ctx);
29153
+ result.names.push(...next.names);
29154
+ result.invalid ||= next.invalid;
29155
+ return result;
29156
+ }, { names: [], invalid: false });
29157
+ }
29158
+ const literal = stringLiteralTypeValue(type);
29159
+ if (literal)
29160
+ return { names: [literal], invalid: false };
29161
+ const symbolName = type.getSymbol()?.getName();
29162
+ if (symbolName && symbolName !== "__type" && symbolName !== "__object") {
29163
+ return { names: [symbolName], invalid: false };
29164
+ }
29165
+ const text = effectTypeName(type, location, ctx).trim();
29166
+ if (text && text !== "object")
29167
+ return { names: [text], invalid: false };
29168
+ return { names: [], invalid: true };
29169
+ }
29170
+ function sortedUnique(values) {
29171
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right, "en"));
29172
+ }
29173
+ function pushEffectDiagnostic(ctx, code, file, line, message, suggestion) {
29174
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
29175
+ ctx.diagnostics.push({
29176
+ severity: "error",
29177
+ code,
29178
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {},
29179
+ file,
29180
+ line,
29181
+ message,
29182
+ suggestion
29183
+ });
29184
+ }
29185
+ function scanEffectRuntimeEscapes(sourceFile, ctx) {
29186
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
29187
+ if (/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativeFile))
29188
+ return;
29189
+ const namespaceBindings = new Set;
29190
+ const runtimeBindings = new Set;
29191
+ for (const statement of sourceFile.statements) {
29192
+ if (!ts7.isImportDeclaration(statement) || !ts7.isStringLiteral(statement.moduleSpecifier))
29193
+ continue;
29194
+ const moduleName = statement.moduleSpecifier.text;
29195
+ if (moduleName !== "effect" && moduleName !== "effect/Effect" && moduleName !== "effect/Runtime")
29196
+ continue;
29197
+ const clause = statement.importClause;
29198
+ if (!clause)
29199
+ continue;
29200
+ if (clause.name)
29201
+ namespaceBindings.add(clause.name.text);
29202
+ if (clause.namedBindings && ts7.isNamespaceImport(clause.namedBindings)) {
29203
+ namespaceBindings.add(clause.namedBindings.name.text);
29204
+ } else if (clause.namedBindings && ts7.isNamedImports(clause.namedBindings)) {
29205
+ for (const element of clause.namedBindings.elements) {
29206
+ const imported = element.propertyName?.text ?? element.name.text;
29207
+ if (moduleName === "effect" && (imported === "Effect" || imported === "Runtime")) {
29208
+ namespaceBindings.add(element.name.text);
29209
+ }
29210
+ if (["runPromise", "runPromiseExit", "runSync", "runFork"].includes(imported)) {
29211
+ runtimeBindings.add(element.name.text);
29212
+ }
29213
+ }
29214
+ }
29215
+ }
29216
+ if (namespaceBindings.size === 0 && runtimeBindings.size === 0)
29217
+ return;
29218
+ for (const call of descendantsOfKind(sourceFile, ts7.isCallExpression)) {
29219
+ let runtimeName;
29220
+ if (ts7.isPropertyAccessExpression(call.expression) && namespaceBindings.has(call.expression.expression.getText(sourceFile)) && ["runPromise", "runPromiseExit", "runSync", "runFork"].includes(call.expression.name.text)) {
29221
+ runtimeName = call.expression.name.text;
29222
+ } else if (ts7.isIdentifier(call.expression) && runtimeBindings.has(call.expression.text)) {
29223
+ runtimeName = call.expression.text;
29224
+ }
29225
+ if (!runtimeName)
29226
+ continue;
29227
+ pushEffectDiagnostic(ctx, "effect-direct-runtime-execution", sourcePath(ctx.rootDir, sourceFile.fileName), lineOf(call), `Direct ${runtimeName} execution escapes the Effect boundary.`, "Return the Effect program to the Elysia boundary; only the framework adapter may interpret it.");
29228
+ }
29229
+ }
29042
29230
  function isUnknownOrAny(type) {
29043
29231
  return (type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0;
29044
29232
  }
@@ -29102,7 +29290,8 @@ function parseController(input, ctx) {
29102
29290
  handler: propertyName(method.name)
29103
29291
  };
29104
29292
  const signature = ctx.checker.getSignatureFromDeclaration(method);
29105
- const resultType = signature && ctx.checker.typeToString(signature.getReturnType());
29293
+ const returnType = signature?.getReturnType();
29294
+ const resultType = returnType && ctx.checker.typeToString(returnType);
29106
29295
  if (resultType && /\bResponse\b/.test(resultType))
29107
29296
  route.nativeResponse = true;
29108
29297
  const pathParams = [];
@@ -29330,6 +29519,235 @@ function parseController(input, ctx) {
29330
29519
  Object.assign(route.contract, { [field]: value });
29331
29520
  }
29332
29521
  }
29522
+ const effectExpr = getProp(optionsObject, "effect");
29523
+ if (effectExpr) {
29524
+ const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
29525
+ if (!effectObject) {
29526
+ ctx.diagnostics.push({
29527
+ severity: "error",
29528
+ code: "invalid-effect-contract",
29529
+ errorCode: "SC3030",
29530
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29531
+ file,
29532
+ line: lineOf(effectExpr),
29533
+ message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
29534
+ suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
29535
+ });
29536
+ } else {
29537
+ const effect = { required: true };
29538
+ const requiredExpr = getProp(effectObject, "required");
29539
+ if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
29540
+ ctx.diagnostics.push({
29541
+ severity: "error",
29542
+ code: "invalid-effect-contract",
29543
+ errorCode: "SC3030",
29544
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29545
+ file,
29546
+ line: lineOf(requiredExpr),
29547
+ message: `Route ${route.handler} effect.required must be the literal true.`
29548
+ });
29549
+ }
29550
+ const retry = stringLiteralProp(effectObject, "retry");
29551
+ if (retry !== undefined) {
29552
+ if (retry !== "none" && retry !== "explicit") {
29553
+ ctx.diagnostics.push({
29554
+ severity: "error",
29555
+ code: "invalid-effect-retry",
29556
+ errorCode: "SC3031",
29557
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29558
+ file,
29559
+ line: lineOf(getProp(effectObject, "retry")),
29560
+ message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
29561
+ });
29562
+ } else {
29563
+ effect.retry = retry;
29564
+ }
29565
+ }
29566
+ const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
29567
+ if (maxAttemptsExpr !== undefined) {
29568
+ const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
29569
+ if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
29570
+ ctx.diagnostics.push({
29571
+ severity: "error",
29572
+ code: "invalid-effect-retry",
29573
+ errorCode: "SC3031",
29574
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29575
+ file,
29576
+ line: lineOf(maxAttemptsExpr),
29577
+ message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
29578
+ });
29579
+ } else {
29580
+ effect.maxAttempts = maxAttempts;
29581
+ }
29582
+ }
29583
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
29584
+ ctx.diagnostics.push({
29585
+ severity: "error",
29586
+ code: "invalid-effect-retry",
29587
+ errorCode: "SC3031",
29588
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29589
+ file,
29590
+ line: lineOf(effectExpr),
29591
+ message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
29592
+ });
29593
+ }
29594
+ const timeoutExpr = getProp(effectObject, "timeoutMs");
29595
+ if (timeoutExpr !== undefined) {
29596
+ const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
29597
+ if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
29598
+ ctx.diagnostics.push({
29599
+ severity: "error",
29600
+ code: "invalid-effect-timeout",
29601
+ errorCode: "SC3032",
29602
+ docsUrl: "https://supacloud.dev/errors/SC3032",
29603
+ file,
29604
+ line: lineOf(timeoutExpr),
29605
+ message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
29606
+ });
29607
+ } else {
29608
+ effect.timeoutMs = timeoutMs;
29609
+ }
29610
+ }
29611
+ const dependenciesExpr = getProp(effectObject, "dependencies");
29612
+ if (dependenciesExpr !== undefined) {
29613
+ if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
29614
+ ctx.diagnostics.push({
29615
+ severity: "error",
29616
+ code: "invalid-effect-dependencies",
29617
+ errorCode: "SC3033",
29618
+ docsUrl: "https://supacloud.dev/errors/SC3033",
29619
+ file,
29620
+ line: lineOf(dependenciesExpr),
29621
+ message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
29622
+ });
29623
+ } else {
29624
+ effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
29625
+ if (new Set(effect.dependencies).size !== effect.dependencies.length) {
29626
+ ctx.diagnostics.push({
29627
+ severity: "error",
29628
+ code: "invalid-effect-dependencies",
29629
+ errorCode: "SC3033",
29630
+ docsUrl: "https://supacloud.dev/errors/SC3033",
29631
+ file,
29632
+ line: lineOf(dependenciesExpr),
29633
+ message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
29634
+ suggestion: "List each logical Effect dependency exactly once."
29635
+ });
29636
+ }
29637
+ }
29638
+ }
29639
+ const errorsExpr = getProp(effectObject, "errors");
29640
+ if (errorsExpr !== undefined) {
29641
+ if (!ts7.isArrayLiteralExpression(errorsExpr)) {
29642
+ ctx.diagnostics.push({
29643
+ severity: "error",
29644
+ code: "invalid-effect-errors",
29645
+ errorCode: "SC3034",
29646
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29647
+ file,
29648
+ line: lineOf(errorsExpr),
29649
+ message: `Route ${route.handler} effect.errors must be an explicit object array.`
29650
+ });
29651
+ } else {
29652
+ const errors = [];
29653
+ for (const errorExpr of errorsExpr.elements) {
29654
+ if (!ts7.isObjectLiteralExpression(errorExpr)) {
29655
+ ctx.diagnostics.push({
29656
+ severity: "error",
29657
+ code: "invalid-effect-errors",
29658
+ errorCode: "SC3034",
29659
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29660
+ file,
29661
+ line: lineOf(errorExpr),
29662
+ message: `Route ${route.handler} effect.errors entries must be object literals.`
29663
+ });
29664
+ continue;
29665
+ }
29666
+ const tag = stringLiteralProp(errorExpr, "tag");
29667
+ const code = stringLiteralProp(errorExpr, "code");
29668
+ const status = numberLiteralProp(errorExpr, "status");
29669
+ const message = stringLiteralProp(errorExpr, "message");
29670
+ if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
29671
+ ctx.diagnostics.push({
29672
+ severity: "error",
29673
+ code: "invalid-effect-errors",
29674
+ errorCode: "SC3034",
29675
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29676
+ file,
29677
+ line: lineOf(errorExpr),
29678
+ message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
29679
+ });
29680
+ continue;
29681
+ }
29682
+ errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
29683
+ }
29684
+ effect.errors = errors;
29685
+ if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
29686
+ ctx.diagnostics.push({
29687
+ severity: "error",
29688
+ code: "invalid-effect-errors",
29689
+ errorCode: "SC3034",
29690
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29691
+ file,
29692
+ line: lineOf(errorsExpr),
29693
+ message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
29694
+ suggestion: "Map each tagged failure exactly once."
29695
+ });
29696
+ }
29697
+ }
29698
+ }
29699
+ route.effect = effect;
29700
+ const effectArguments = effectTypeArgumentsOf(returnType, ctx);
29701
+ const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
29702
+ if (promiseReturn) {
29703
+ pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
29704
+ } else if (!effectArguments) {
29705
+ ctx.diagnostics.push({
29706
+ severity: "error",
29707
+ code: "effect-return-type",
29708
+ errorCode: "SC3035",
29709
+ docsUrl: "https://supacloud.dev/errors/SC3035",
29710
+ file,
29711
+ line: lineOf(method),
29712
+ message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
29713
+ suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
29714
+ });
29715
+ }
29716
+ if (effectArguments) {
29717
+ const [, failureType, environmentType] = effectArguments;
29718
+ const failure = effectFailureTags(failureType, method, ctx);
29719
+ if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
29720
+ pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
29721
+ } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
29722
+ const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
29723
+ const typedTags = sortedUnique(failure.tags);
29724
+ if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
29725
+ pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(method), `Route ${route.handler} maps [${declaredTags.join(", ")}], but its Effect failure type declares [${typedTags.join(", ")}].`, "Make effect.errors and the Effect error type the same tagged union; do not hide unmapped failures.");
29726
+ }
29727
+ }
29728
+ const environment = effectEnvironmentNames(environmentType, method, ctx);
29729
+ if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
29730
+ if (environment.invalid) {
29731
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} uses an Effect environment that cannot be represented by its explicit dependency list.`, "Use named service types or never in the third Effect type parameter.");
29732
+ } else {
29733
+ const declaredDependencies = sortedUnique(effect.dependencies);
29734
+ const typedDependencies = sortedUnique(environment.names);
29735
+ if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
29736
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} declares dependencies [${declaredDependencies.join(", ")}], but its Effect environment is [${typedDependencies.join(", ")}].`, "Make effect.dependencies exactly match the third Effect type parameter.");
29737
+ }
29738
+ }
29739
+ }
29740
+ if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
29741
+ pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf(method), `Route ${route.handler} declares external Effect dependencies without a timeout.`, "Add a positive effect.timeoutMs so external I/O cannot occupy a request indefinitely.");
29742
+ }
29743
+ }
29744
+ if (ctx.effectOptions.forbidDirectThrows && method.body) {
29745
+ for (const throwStatement of descendantsOfKind(method.body, ts7.isThrowStatement)) {
29746
+ pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(throwStatement), `Route ${route.handler} throws directly instead of representing failure in Effect.`, "Use Effect.fail with a tagged error value and map it in effect.errors.");
29747
+ }
29748
+ }
29749
+ }
29750
+ }
29333
29751
  for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
29334
29752
  const schemaExpr = getProp(optionsObject, field);
29335
29753
  if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
@@ -29913,6 +30331,13 @@ function stringLiteralProp(obj, name) {
29913
30331
  const expr = getProp(obj, name);
29914
30332
  return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
29915
30333
  }
30334
+ function numberLiteralProp(obj, name) {
30335
+ const expr = getProp(obj, name);
30336
+ if (!expr || !ts7.isNumericLiteral(expr))
30337
+ return;
30338
+ const value = Number(expr.text);
30339
+ return Number.isFinite(value) ? value : undefined;
30340
+ }
29916
30341
  function arrayProp(obj, name) {
29917
30342
  const expr = getProp(obj, name);
29918
30343
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -30574,6 +30999,57 @@ function validateClientSchemaBoundaries(graph, options) {
30574
30999
  return diagnostics;
30575
31000
  }
30576
31001
 
31002
+ // src/effect-governance.ts
31003
+ init_validate();
31004
+ function diagnostic(code, message, file, suggestion) {
31005
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
31006
+ return {
31007
+ severity: "error",
31008
+ code,
31009
+ message,
31010
+ ...file === undefined ? {} : { file },
31011
+ ...suggestion === undefined ? {} : { suggestion },
31012
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
31013
+ };
31014
+ }
31015
+ function routeLabel(route, controllerPath) {
31016
+ return `${route.method} ${controllerPath}${route.path}`;
31017
+ }
31018
+ function validateEffectPolicies(graph, options = {}) {
31019
+ const diagnostics = [];
31020
+ for (const module of graph.modules) {
31021
+ const commands = new Map(module.commands.map((command) => [command.className, command]));
31022
+ for (const controller of module.controllers) {
31023
+ for (const route of controller.routes) {
31024
+ const label = routeLabel(route, controller.path);
31025
+ if (options.requireRouteEffects && route.effect === undefined) {
31026
+ 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.'));
31027
+ continue;
31028
+ }
31029
+ const effect = route.effect;
31030
+ if (effect === undefined)
31031
+ continue;
31032
+ if (options.requireErrorMappings && effect.errors === undefined) {
31033
+ 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."));
31034
+ }
31035
+ if (options.requireDependencies && effect.dependencies === undefined) {
31036
+ 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."));
31037
+ }
31038
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
31039
+ 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."));
31040
+ }
31041
+ if (route.command && effect.retry === "explicit") {
31042
+ const command = commands.get(route.command);
31043
+ if (command && command.idempotency !== "required") {
31044
+ 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".'));
31045
+ }
31046
+ }
31047
+ }
31048
+ }
31049
+ }
31050
+ return diagnostics;
31051
+ }
31052
+
30577
31053
  // src/compile.ts
30578
31054
  function withDefaultGovernance(options) {
30579
31055
  return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
@@ -30589,11 +31065,12 @@ async function renderOptionalGraphql(options) {
30589
31065
  }
30590
31066
  async function compileProject(options) {
30591
31067
  options = withDefaultGovernance(options);
30592
- const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
31068
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
30593
31069
  const diagnostics = [
30594
31070
  ...graph.diagnostics ?? [],
30595
31071
  ...validateGraph(graph, options),
30596
- ...validateClientSchemaBoundaries(graph, options)
31072
+ ...validateClientSchemaBoundaries(graph, options),
31073
+ ...validateEffectPolicies(graph, options.effect)
30597
31074
  ];
30598
31075
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30599
31076
  return { diagnostics, graph, written: [] };
@@ -30653,11 +31130,12 @@ async function compileProject(options) {
30653
31130
  }
30654
31131
  async function checkProject(options) {
30655
31132
  options = withDefaultGovernance(options);
30656
- const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
31133
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
30657
31134
  const diagnostics = [
30658
31135
  ...graph.diagnostics ?? [],
30659
31136
  ...validateGraph(graph, options),
30660
- ...validateClientSchemaBoundaries(graph, options)
31137
+ ...validateClientSchemaBoundaries(graph, options),
31138
+ ...validateEffectPolicies(graph, options.effect)
30661
31139
  ];
30662
31140
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30663
31141
  return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
@@ -31140,6 +31618,7 @@ function optionsKeyOf(options) {
31140
31618
  openApi: options.openApi,
31141
31619
  generatePermissions: options.generatePermissions,
31142
31620
  typeSafety: options.typeSafety,
31621
+ effect: options.effect,
31143
31622
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
31144
31623
  graphql: options.graphql
31145
31624
  });
@@ -31467,7 +31946,17 @@ var DEFAULT_SUPACLOUD_CONFIG = {
31467
31946
  idempotency: true,
31468
31947
  transaction: true
31469
31948
  },
31470
- moduleBoundaryPreset: "modular-monolith"
31949
+ moduleBoundaryPreset: "modular-monolith",
31950
+ effect: {
31951
+ requireRouteEffects: true,
31952
+ requireErrorMappings: true,
31953
+ requireDependencies: true,
31954
+ requireTaggedErrorTypes: true,
31955
+ requireExactDependencyTypes: true,
31956
+ requireTimeoutForDependencies: true,
31957
+ forbidDirectRuntimeExecution: true,
31958
+ forbidDirectThrows: true
31959
+ }
31471
31960
  };
31472
31961
  function defineSupacloudConfig(config = {}) {
31473
31962
  if (config.delivery !== undefined)
@@ -31479,7 +31968,8 @@ function defineSupacloudConfig(config = {}) {
31479
31968
  ...DEFAULT_SUPACLOUD_CONFIG,
31480
31969
  ...config,
31481
31970
  include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
31482
- graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
31971
+ graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
31972
+ effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
31483
31973
  };
31484
31974
  }
31485
31975
  function validateGovernanceConfig(config) {
@@ -31536,6 +32026,34 @@ function validateGovernanceConfig(config) {
31536
32026
  if (config[key] !== undefined && typeof config[key] !== "boolean")
31537
32027
  throw new Error(`${key} must be a boolean.`);
31538
32028
  }
32029
+ if (config.effect !== undefined) {
32030
+ if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => ![
32031
+ "requireRouteEffects",
32032
+ "requireErrorMappings",
32033
+ "requireDependencies",
32034
+ "requireTaggedErrorTypes",
32035
+ "requireExactDependencyTypes",
32036
+ "requireTimeoutForDependencies",
32037
+ "forbidDirectRuntimeExecution",
32038
+ "forbidDirectThrows"
32039
+ ].includes(key))) {
32040
+ throw new Error("effect contains an unsupported governance option.");
32041
+ }
32042
+ for (const key of [
32043
+ "requireRouteEffects",
32044
+ "requireErrorMappings",
32045
+ "requireDependencies",
32046
+ "requireTaggedErrorTypes",
32047
+ "requireExactDependencyTypes",
32048
+ "requireTimeoutForDependencies",
32049
+ "forbidDirectRuntimeExecution",
32050
+ "forbidDirectThrows"
32051
+ ]) {
32052
+ if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
32053
+ throw new Error(`effect.${key} must be boolean.`);
32054
+ }
32055
+ }
32056
+ }
31539
32057
  }
31540
32058
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
31541
32059
  const resolved = defineSupacloudConfig(config);
@@ -31556,6 +32074,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
31556
32074
  ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
31557
32075
  ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
31558
32076
  ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
32077
+ effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
31559
32078
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
31560
32079
  graphql: resolved.graphql ? {
31561
32080
  ...resolved.graphql,
@@ -31644,7 +32163,7 @@ import { createHash as createHash10 } from "node:crypto";
31644
32163
  init_validate();
31645
32164
  var compare3 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
31646
32165
  var sorted = (values) => [...new Set(values)].sort(compare3);
31647
- function diagnostic(error) {
32166
+ function diagnostic2(error) {
31648
32167
  return {
31649
32168
  severity: error.severity,
31650
32169
  code: error.code,
@@ -31669,8 +32188,8 @@ function createDeliveryPlan(graph, input) {
31669
32188
  }] };
31670
32189
  }
31671
32190
  const diagnostics = [
31672
- ...(graph.diagnostics ?? []).map(diagnostic),
31673
- ...validateGraph(graph, { strict: true }).map(diagnostic)
32191
+ ...(graph.diagnostics ?? []).map(diagnostic2),
32192
+ ...validateGraph(graph, { strict: true }).map(diagnostic2)
31674
32193
  ];
31675
32194
  const fail = (code, message, suggestion) => {
31676
32195
  diagnostics.push({ severity: "error", code, message, suggestion });
@@ -32840,10 +33359,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
32840
33359
  import { resolve as resolve18 } from "node:path";
32841
33360
 
32842
33361
  // src/migration-app-version.ts
32843
- var MIGRATION_APP_VERSION = "0.23.0";
33362
+ var MIGRATION_APP_VERSION = "0.24.0";
32844
33363
 
32845
33364
  // src/migration-elysia-version.ts
32846
- var MIGRATION_ELYSIA_VERSION = "0.27.1";
33365
+ var MIGRATION_ELYSIA_VERSION = "0.29.0";
32847
33366
 
32848
33367
  // src/migration-policy.ts
32849
33368
  var cachedCompilerVersion;