@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/index.js CHANGED
@@ -1236,6 +1236,24 @@ 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" },
1250
+ "effect-error-type": { code: "SC3041", docsUrl: "https://supacloud.dev/errors/SC3041" },
1251
+ "effect-error-mapping-type": { code: "SC3042", docsUrl: "https://supacloud.dev/errors/SC3042" },
1252
+ "effect-dependency-mismatch": { code: "SC3043", docsUrl: "https://supacloud.dev/errors/SC3043" },
1253
+ "effect-timeout-required": { code: "SC3044", docsUrl: "https://supacloud.dev/errors/SC3044" },
1254
+ "effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
1255
+ "effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
1256
+ "effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
1239
1257
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1240
1258
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1241
1259
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -1774,6 +1792,9 @@ class ModuleGenerator {
1774
1792
  });
1775
1793
  fields.push(`responses: { ${responseFields.join(", ")} }`);
1776
1794
  }
1795
+ if (route.effect !== undefined) {
1796
+ fields.push(`effect: ${JSON.stringify(route.effect)}`);
1797
+ }
1777
1798
  if (route.command)
1778
1799
  fields.push(`command: ${JSON.stringify(route.command)}`);
1779
1800
  if (route.guards && route.guards.length > 0) {
@@ -3585,6 +3606,14 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
3585
3606
  cookie?: unknown;
3586
3607
  response?: unknown;
3587
3608
  responses?: Record<string | number, unknown>;
3609
+ effect?: {
3610
+ required: true;
3611
+ errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
3612
+ dependencies?: string[];
3613
+ retry?: "none" | "explicit";
3614
+ maxAttempts?: number;
3615
+ timeoutMs?: number;
3616
+ };
3588
3617
  /** Compile-time ownership and transport classification for the route boundary. */
3589
3618
  contract?: {
3590
3619
  body?: "framework" | "domain";
@@ -27752,6 +27781,13 @@ function withDiagnosticMetadata(diagnostic) {
27752
27781
  const metadata = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
27753
27782
  return metadata ? { ...diagnostic, errorCode: metadata.code, docsUrl: metadata.docsUrl } : diagnostic;
27754
27783
  }
27784
+ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
27785
+ requireTaggedErrorTypes: true,
27786
+ requireExactDependencyTypes: true,
27787
+ requireTimeoutForDependencies: true,
27788
+ forbidDirectRuntimeExecution: true,
27789
+ forbidDirectThrows: true
27790
+ };
27755
27791
  function nodeText(node) {
27756
27792
  return node.getText(node.getSourceFile());
27757
27793
  }
@@ -27797,7 +27833,7 @@ function descendantsOfKind(root, predicate) {
27797
27833
  visit(root);
27798
27834
  return result;
27799
27835
  }
27800
- async function analyzeProject(rootDir, include, cache, changedPaths) {
27836
+ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptions = {}) {
27801
27837
  const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
27802
27838
  if (cache)
27803
27839
  cache.programSession = session;
@@ -27816,8 +27852,21 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
27816
27852
  moduleHandlerFiles: new Map,
27817
27853
  infraResources: new Map,
27818
27854
  infraResourceClasses: new Map,
27819
- diagnostics: []
27855
+ diagnostics: [],
27856
+ effectOptions: {
27857
+ ...DEFAULT_EFFECT_ANALYSIS_OPTIONS,
27858
+ requireTaggedErrorTypes: effectOptions.requireTaggedErrorTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTaggedErrorTypes,
27859
+ requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
27860
+ requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
27861
+ forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
27862
+ forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
27863
+ }
27820
27864
  };
27865
+ if (ctx.effectOptions.forbidDirectRuntimeExecution) {
27866
+ for (const sourceFile of sourceFiles) {
27867
+ scanEffectRuntimeEscapes(sourceFile, ctx);
27868
+ }
27869
+ }
27821
27870
  const nativeTraitFiles = new Map;
27822
27871
  for (const diagnostic of session.getDiagnostics()) {
27823
27872
  ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
@@ -28993,11 +29042,150 @@ function declaredClassType(declaration, ctx) {
28993
29042
  return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
28994
29043
  }
28995
29044
  function typeArgumentsOf(type, ctx) {
28996
- return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
29045
+ if (!isTypeReference(type))
29046
+ return [];
29047
+ const argumentsFromChecker = ctx.checker.getTypeArguments(type);
29048
+ if (argumentsFromChecker.length > 0)
29049
+ return argumentsFromChecker;
29050
+ const reference = type;
29051
+ if (reference.resolvedTypeArguments && reference.resolvedTypeArguments.length > 0) {
29052
+ return reference.resolvedTypeArguments;
29053
+ }
29054
+ return reference.aliasTypeArguments ?? [];
28997
29055
  }
28998
29056
  function isTypeReference(type) {
28999
29057
  return "target" in type;
29000
29058
  }
29059
+ function effectTypeArgumentsOf(type, ctx) {
29060
+ if (!type)
29061
+ return;
29062
+ const reference = isTypeReference(type) ? type : undefined;
29063
+ const target = reference?.target ?? type;
29064
+ const symbol = target.aliasSymbol ?? target.getSymbol();
29065
+ const text = ctx.checker.typeToString(type, undefined, ts7.TypeFormatFlags.NoTruncation);
29066
+ if (symbol?.getName() !== "Effect" && !/^(?:Effect\.)?Effect</.test(text))
29067
+ return;
29068
+ const args = typeArgumentsOf(type, ctx);
29069
+ return args.length >= 3 ? [args[0], args[1], args[2]] : undefined;
29070
+ }
29071
+ function effectTypeName(type, location, ctx) {
29072
+ return ctx.checker.typeToString(type, location, ts7.TypeFormatFlags.NoTruncation);
29073
+ }
29074
+ function stringLiteralTypeValue(type) {
29075
+ return (type.flags & ts7.TypeFlags.StringLiteral) !== 0 ? type.value : undefined;
29076
+ }
29077
+ function effectFailureTags(type, location, ctx) {
29078
+ if ((type.flags & ts7.TypeFlags.Never) !== 0)
29079
+ return { tags: [], invalid: false };
29080
+ if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
29081
+ return { tags: [], invalid: true };
29082
+ }
29083
+ if (type.isUnion()) {
29084
+ return type.types.reduce((result, part) => {
29085
+ const next = effectFailureTags(part, location, ctx);
29086
+ result.tags.push(...next.tags);
29087
+ result.invalid ||= next.invalid;
29088
+ return result;
29089
+ }, { tags: [], invalid: false });
29090
+ }
29091
+ const apparent = ctx.checker.getApparentType(type);
29092
+ for (const propertyName of ["_tag", "tag"]) {
29093
+ const property = ctx.checker.getPropertyOfType(apparent, propertyName);
29094
+ if (!property)
29095
+ continue;
29096
+ const propertyType = ctx.checker.getTypeOfSymbolAtLocation(property, location);
29097
+ const tag = stringLiteralTypeValue(propertyType);
29098
+ if (tag)
29099
+ return { tags: [tag], invalid: false };
29100
+ }
29101
+ return { tags: [], invalid: true };
29102
+ }
29103
+ function effectEnvironmentNames(type, location, ctx) {
29104
+ if ((type.flags & ts7.TypeFlags.Never) !== 0)
29105
+ return { names: [], invalid: false };
29106
+ if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
29107
+ return { names: [], invalid: true };
29108
+ }
29109
+ if (type.isUnion() || type.isIntersection()) {
29110
+ return type.types.reduce((result, part) => {
29111
+ const next = effectEnvironmentNames(part, location, ctx);
29112
+ result.names.push(...next.names);
29113
+ result.invalid ||= next.invalid;
29114
+ return result;
29115
+ }, { names: [], invalid: false });
29116
+ }
29117
+ const literal = stringLiteralTypeValue(type);
29118
+ if (literal)
29119
+ return { names: [literal], invalid: false };
29120
+ const symbolName = type.getSymbol()?.getName();
29121
+ if (symbolName && symbolName !== "__type" && symbolName !== "__object") {
29122
+ return { names: [symbolName], invalid: false };
29123
+ }
29124
+ const text = effectTypeName(type, location, ctx).trim();
29125
+ if (text && text !== "object")
29126
+ return { names: [text], invalid: false };
29127
+ return { names: [], invalid: true };
29128
+ }
29129
+ function sortedUnique(values) {
29130
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right, "en"));
29131
+ }
29132
+ function pushEffectDiagnostic(ctx, code, file, line, message, suggestion) {
29133
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
29134
+ ctx.diagnostics.push({
29135
+ severity: "error",
29136
+ code,
29137
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {},
29138
+ file,
29139
+ line,
29140
+ message,
29141
+ suggestion
29142
+ });
29143
+ }
29144
+ function scanEffectRuntimeEscapes(sourceFile, ctx) {
29145
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
29146
+ if (/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativeFile))
29147
+ return;
29148
+ const namespaceBindings = new Set;
29149
+ const runtimeBindings = new Set;
29150
+ for (const statement of sourceFile.statements) {
29151
+ if (!ts7.isImportDeclaration(statement) || !ts7.isStringLiteral(statement.moduleSpecifier))
29152
+ continue;
29153
+ const moduleName = statement.moduleSpecifier.text;
29154
+ if (moduleName !== "effect" && moduleName !== "effect/Effect" && moduleName !== "effect/Runtime")
29155
+ continue;
29156
+ const clause = statement.importClause;
29157
+ if (!clause)
29158
+ continue;
29159
+ if (clause.name)
29160
+ namespaceBindings.add(clause.name.text);
29161
+ if (clause.namedBindings && ts7.isNamespaceImport(clause.namedBindings)) {
29162
+ namespaceBindings.add(clause.namedBindings.name.text);
29163
+ } else if (clause.namedBindings && ts7.isNamedImports(clause.namedBindings)) {
29164
+ for (const element of clause.namedBindings.elements) {
29165
+ const imported = element.propertyName?.text ?? element.name.text;
29166
+ if (moduleName === "effect" && (imported === "Effect" || imported === "Runtime")) {
29167
+ namespaceBindings.add(element.name.text);
29168
+ }
29169
+ if (["runPromise", "runPromiseExit", "runSync", "runFork"].includes(imported)) {
29170
+ runtimeBindings.add(element.name.text);
29171
+ }
29172
+ }
29173
+ }
29174
+ }
29175
+ if (namespaceBindings.size === 0 && runtimeBindings.size === 0)
29176
+ return;
29177
+ for (const call of descendantsOfKind(sourceFile, ts7.isCallExpression)) {
29178
+ let runtimeName;
29179
+ if (ts7.isPropertyAccessExpression(call.expression) && namespaceBindings.has(call.expression.expression.getText(sourceFile)) && ["runPromise", "runPromiseExit", "runSync", "runFork"].includes(call.expression.name.text)) {
29180
+ runtimeName = call.expression.name.text;
29181
+ } else if (ts7.isIdentifier(call.expression) && runtimeBindings.has(call.expression.text)) {
29182
+ runtimeName = call.expression.text;
29183
+ }
29184
+ if (!runtimeName)
29185
+ continue;
29186
+ 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.");
29187
+ }
29188
+ }
29001
29189
  function isUnknownOrAny(type) {
29002
29190
  return (type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0;
29003
29191
  }
@@ -29061,7 +29249,8 @@ function parseController(input, ctx) {
29061
29249
  handler: propertyName(method.name)
29062
29250
  };
29063
29251
  const signature = ctx.checker.getSignatureFromDeclaration(method);
29064
- const resultType = signature && ctx.checker.typeToString(signature.getReturnType());
29252
+ const returnType = signature?.getReturnType();
29253
+ const resultType = returnType && ctx.checker.typeToString(returnType);
29065
29254
  if (resultType && /\bResponse\b/.test(resultType))
29066
29255
  route.nativeResponse = true;
29067
29256
  const pathParams = [];
@@ -29289,6 +29478,235 @@ function parseController(input, ctx) {
29289
29478
  Object.assign(route.contract, { [field]: value });
29290
29479
  }
29291
29480
  }
29481
+ const effectExpr = getProp(optionsObject, "effect");
29482
+ if (effectExpr) {
29483
+ const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
29484
+ if (!effectObject) {
29485
+ ctx.diagnostics.push({
29486
+ severity: "error",
29487
+ code: "invalid-effect-contract",
29488
+ errorCode: "SC3030",
29489
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29490
+ file,
29491
+ line: lineOf(effectExpr),
29492
+ message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
29493
+ suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
29494
+ });
29495
+ } else {
29496
+ const effect = { required: true };
29497
+ const requiredExpr = getProp(effectObject, "required");
29498
+ if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
29499
+ ctx.diagnostics.push({
29500
+ severity: "error",
29501
+ code: "invalid-effect-contract",
29502
+ errorCode: "SC3030",
29503
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29504
+ file,
29505
+ line: lineOf(requiredExpr),
29506
+ message: `Route ${route.handler} effect.required must be the literal true.`
29507
+ });
29508
+ }
29509
+ const retry = stringLiteralProp(effectObject, "retry");
29510
+ if (retry !== undefined) {
29511
+ if (retry !== "none" && retry !== "explicit") {
29512
+ ctx.diagnostics.push({
29513
+ severity: "error",
29514
+ code: "invalid-effect-retry",
29515
+ errorCode: "SC3031",
29516
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29517
+ file,
29518
+ line: lineOf(getProp(effectObject, "retry")),
29519
+ message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
29520
+ });
29521
+ } else {
29522
+ effect.retry = retry;
29523
+ }
29524
+ }
29525
+ const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
29526
+ if (maxAttemptsExpr !== undefined) {
29527
+ const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
29528
+ if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
29529
+ ctx.diagnostics.push({
29530
+ severity: "error",
29531
+ code: "invalid-effect-retry",
29532
+ errorCode: "SC3031",
29533
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29534
+ file,
29535
+ line: lineOf(maxAttemptsExpr),
29536
+ message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
29537
+ });
29538
+ } else {
29539
+ effect.maxAttempts = maxAttempts;
29540
+ }
29541
+ }
29542
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
29543
+ ctx.diagnostics.push({
29544
+ severity: "error",
29545
+ code: "invalid-effect-retry",
29546
+ errorCode: "SC3031",
29547
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29548
+ file,
29549
+ line: lineOf(effectExpr),
29550
+ message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
29551
+ });
29552
+ }
29553
+ const timeoutExpr = getProp(effectObject, "timeoutMs");
29554
+ if (timeoutExpr !== undefined) {
29555
+ const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
29556
+ if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
29557
+ ctx.diagnostics.push({
29558
+ severity: "error",
29559
+ code: "invalid-effect-timeout",
29560
+ errorCode: "SC3032",
29561
+ docsUrl: "https://supacloud.dev/errors/SC3032",
29562
+ file,
29563
+ line: lineOf(timeoutExpr),
29564
+ message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
29565
+ });
29566
+ } else {
29567
+ effect.timeoutMs = timeoutMs;
29568
+ }
29569
+ }
29570
+ const dependenciesExpr = getProp(effectObject, "dependencies");
29571
+ if (dependenciesExpr !== undefined) {
29572
+ if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
29573
+ ctx.diagnostics.push({
29574
+ severity: "error",
29575
+ code: "invalid-effect-dependencies",
29576
+ errorCode: "SC3033",
29577
+ docsUrl: "https://supacloud.dev/errors/SC3033",
29578
+ file,
29579
+ line: lineOf(dependenciesExpr),
29580
+ message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
29581
+ });
29582
+ } else {
29583
+ effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
29584
+ if (new Set(effect.dependencies).size !== effect.dependencies.length) {
29585
+ ctx.diagnostics.push({
29586
+ severity: "error",
29587
+ code: "invalid-effect-dependencies",
29588
+ errorCode: "SC3033",
29589
+ docsUrl: "https://supacloud.dev/errors/SC3033",
29590
+ file,
29591
+ line: lineOf(dependenciesExpr),
29592
+ message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
29593
+ suggestion: "List each logical Effect dependency exactly once."
29594
+ });
29595
+ }
29596
+ }
29597
+ }
29598
+ const errorsExpr = getProp(effectObject, "errors");
29599
+ if (errorsExpr !== undefined) {
29600
+ if (!ts7.isArrayLiteralExpression(errorsExpr)) {
29601
+ ctx.diagnostics.push({
29602
+ severity: "error",
29603
+ code: "invalid-effect-errors",
29604
+ errorCode: "SC3034",
29605
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29606
+ file,
29607
+ line: lineOf(errorsExpr),
29608
+ message: `Route ${route.handler} effect.errors must be an explicit object array.`
29609
+ });
29610
+ } else {
29611
+ const errors = [];
29612
+ for (const errorExpr of errorsExpr.elements) {
29613
+ if (!ts7.isObjectLiteralExpression(errorExpr)) {
29614
+ ctx.diagnostics.push({
29615
+ severity: "error",
29616
+ code: "invalid-effect-errors",
29617
+ errorCode: "SC3034",
29618
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29619
+ file,
29620
+ line: lineOf(errorExpr),
29621
+ message: `Route ${route.handler} effect.errors entries must be object literals.`
29622
+ });
29623
+ continue;
29624
+ }
29625
+ const tag = stringLiteralProp(errorExpr, "tag");
29626
+ const code = stringLiteralProp(errorExpr, "code");
29627
+ const status = numberLiteralProp(errorExpr, "status");
29628
+ const message = stringLiteralProp(errorExpr, "message");
29629
+ if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
29630
+ ctx.diagnostics.push({
29631
+ severity: "error",
29632
+ code: "invalid-effect-errors",
29633
+ errorCode: "SC3034",
29634
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29635
+ file,
29636
+ line: lineOf(errorExpr),
29637
+ message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
29638
+ });
29639
+ continue;
29640
+ }
29641
+ errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
29642
+ }
29643
+ effect.errors = errors;
29644
+ if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
29645
+ ctx.diagnostics.push({
29646
+ severity: "error",
29647
+ code: "invalid-effect-errors",
29648
+ errorCode: "SC3034",
29649
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29650
+ file,
29651
+ line: lineOf(errorsExpr),
29652
+ message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
29653
+ suggestion: "Map each tagged failure exactly once."
29654
+ });
29655
+ }
29656
+ }
29657
+ }
29658
+ route.effect = effect;
29659
+ const effectArguments = effectTypeArgumentsOf(returnType, ctx);
29660
+ const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
29661
+ if (promiseReturn) {
29662
+ 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.");
29663
+ } else if (!effectArguments) {
29664
+ ctx.diagnostics.push({
29665
+ severity: "error",
29666
+ code: "effect-return-type",
29667
+ errorCode: "SC3035",
29668
+ docsUrl: "https://supacloud.dev/errors/SC3035",
29669
+ file,
29670
+ line: lineOf(method),
29671
+ message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
29672
+ suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
29673
+ });
29674
+ }
29675
+ if (effectArguments) {
29676
+ const [, failureType, environmentType] = effectArguments;
29677
+ const failure = effectFailureTags(failureType, method, ctx);
29678
+ if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
29679
+ 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.");
29680
+ } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
29681
+ const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
29682
+ const typedTags = sortedUnique(failure.tags);
29683
+ if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
29684
+ 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.");
29685
+ }
29686
+ }
29687
+ const environment = effectEnvironmentNames(environmentType, method, ctx);
29688
+ if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
29689
+ if (environment.invalid) {
29690
+ 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.");
29691
+ } else {
29692
+ const declaredDependencies = sortedUnique(effect.dependencies);
29693
+ const typedDependencies = sortedUnique(environment.names);
29694
+ if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
29695
+ 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.");
29696
+ }
29697
+ }
29698
+ }
29699
+ if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
29700
+ 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.");
29701
+ }
29702
+ }
29703
+ if (ctx.effectOptions.forbidDirectThrows && method.body) {
29704
+ for (const throwStatement of descendantsOfKind(method.body, ts7.isThrowStatement)) {
29705
+ 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.");
29706
+ }
29707
+ }
29708
+ }
29709
+ }
29292
29710
  for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
29293
29711
  const schemaExpr = getProp(optionsObject, field);
29294
29712
  if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
@@ -29872,6 +30290,13 @@ function stringLiteralProp(obj, name) {
29872
30290
  const expr = getProp(obj, name);
29873
30291
  return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
29874
30292
  }
30293
+ function numberLiteralProp(obj, name) {
30294
+ const expr = getProp(obj, name);
30295
+ if (!expr || !ts7.isNumericLiteral(expr))
30296
+ return;
30297
+ const value = Number(expr.text);
30298
+ return Number.isFinite(value) ? value : undefined;
30299
+ }
29875
30300
  function arrayProp(obj, name) {
29876
30301
  const expr = getProp(obj, name);
29877
30302
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -30535,6 +30960,57 @@ function validateClientSchemaBoundaries(graph, options) {
30535
30960
  return diagnostics;
30536
30961
  }
30537
30962
 
30963
+ // src/effect-governance.ts
30964
+ init_validate();
30965
+ function diagnostic(code, message, file, suggestion) {
30966
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
30967
+ return {
30968
+ severity: "error",
30969
+ code,
30970
+ message,
30971
+ ...file === undefined ? {} : { file },
30972
+ ...suggestion === undefined ? {} : { suggestion },
30973
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
30974
+ };
30975
+ }
30976
+ function routeLabel(route, controllerPath) {
30977
+ return `${route.method} ${controllerPath}${route.path}`;
30978
+ }
30979
+ function validateEffectPolicies(graph, options = {}) {
30980
+ const diagnostics = [];
30981
+ for (const module of graph.modules) {
30982
+ const commands = new Map(module.commands.map((command) => [command.className, command]));
30983
+ for (const controller of module.controllers) {
30984
+ for (const route of controller.routes) {
30985
+ const label = routeLabel(route, controller.path);
30986
+ if (options.requireRouteEffects && route.effect === undefined) {
30987
+ 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.'));
30988
+ continue;
30989
+ }
30990
+ const effect = route.effect;
30991
+ if (effect === undefined)
30992
+ continue;
30993
+ if (options.requireErrorMappings && effect.errors === undefined) {
30994
+ 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."));
30995
+ }
30996
+ if (options.requireDependencies && effect.dependencies === undefined) {
30997
+ 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."));
30998
+ }
30999
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
31000
+ 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."));
31001
+ }
31002
+ if (route.command && effect.retry === "explicit") {
31003
+ const command = commands.get(route.command);
31004
+ if (command && command.idempotency !== "required") {
31005
+ 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".'));
31006
+ }
31007
+ }
31008
+ }
31009
+ }
31010
+ }
31011
+ return diagnostics;
31012
+ }
31013
+
30538
31014
  // src/compile.ts
30539
31015
  function withDefaultGovernance(options) {
30540
31016
  return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
@@ -30550,11 +31026,12 @@ async function renderOptionalGraphql(options) {
30550
31026
  }
30551
31027
  async function compileProject(options) {
30552
31028
  options = withDefaultGovernance(options);
30553
- const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
31029
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
30554
31030
  const diagnostics = [
30555
31031
  ...graph.diagnostics ?? [],
30556
31032
  ...validateGraph(graph, options),
30557
- ...validateClientSchemaBoundaries(graph, options)
31033
+ ...validateClientSchemaBoundaries(graph, options),
31034
+ ...validateEffectPolicies(graph, options.effect)
30558
31035
  ];
30559
31036
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30560
31037
  return { diagnostics, graph, written: [] };
@@ -30614,11 +31091,12 @@ async function compileProject(options) {
30614
31091
  }
30615
31092
  async function checkProject(options) {
30616
31093
  options = withDefaultGovernance(options);
30617
- const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
31094
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
30618
31095
  const diagnostics = [
30619
31096
  ...graph.diagnostics ?? [],
30620
31097
  ...validateGraph(graph, options),
30621
- ...validateClientSchemaBoundaries(graph, options)
31098
+ ...validateClientSchemaBoundaries(graph, options),
31099
+ ...validateEffectPolicies(graph, options.effect)
30622
31100
  ];
30623
31101
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30624
31102
  return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
@@ -30704,7 +31182,7 @@ init_delivery_schema();
30704
31182
  init_validate();
30705
31183
  var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
30706
31184
  var sorted = (values) => [...new Set(values)].sort(compare2);
30707
- function diagnostic(error) {
31185
+ function diagnostic2(error) {
30708
31186
  return {
30709
31187
  severity: error.severity,
30710
31188
  code: error.code,
@@ -30729,8 +31207,8 @@ function createDeliveryPlan(graph, input) {
30729
31207
  }] };
30730
31208
  }
30731
31209
  const diagnostics = [
30732
- ...(graph.diagnostics ?? []).map(diagnostic),
30733
- ...validateGraph(graph, { strict: true }).map(diagnostic)
31210
+ ...(graph.diagnostics ?? []).map(diagnostic2),
31211
+ ...validateGraph(graph, { strict: true }).map(diagnostic2)
30734
31212
  ];
30735
31213
  const fail = (code, message, suggestion) => {
30736
31214
  diagnostics.push({ severity: "error", code, message, suggestion });
@@ -32264,6 +32742,7 @@ function optionsKeyOf(options) {
32264
32742
  openApi: options.openApi,
32265
32743
  generatePermissions: options.generatePermissions,
32266
32744
  typeSafety: options.typeSafety,
32745
+ effect: options.effect,
32267
32746
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
32268
32747
  graphql: options.graphql
32269
32748
  });
@@ -32573,10 +33052,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
32573
33052
  import { resolve as resolve17 } from "node:path";
32574
33053
 
32575
33054
  // src/migration-app-version.ts
32576
- var MIGRATION_APP_VERSION = "0.23.0";
33055
+ var MIGRATION_APP_VERSION = "0.24.0";
32577
33056
 
32578
33057
  // src/migration-elysia-version.ts
32579
- var MIGRATION_ELYSIA_VERSION = "0.27.1";
33058
+ var MIGRATION_ELYSIA_VERSION = "0.29.0";
32580
33059
 
32581
33060
  // src/migration-policy.ts
32582
33061
  var cachedCompilerVersion;
@@ -33746,7 +34225,17 @@ var DEFAULT_SUPACLOUD_CONFIG = {
33746
34225
  idempotency: true,
33747
34226
  transaction: true
33748
34227
  },
33749
- moduleBoundaryPreset: "modular-monolith"
34228
+ moduleBoundaryPreset: "modular-monolith",
34229
+ effect: {
34230
+ requireRouteEffects: true,
34231
+ requireErrorMappings: true,
34232
+ requireDependencies: true,
34233
+ requireTaggedErrorTypes: true,
34234
+ requireExactDependencyTypes: true,
34235
+ requireTimeoutForDependencies: true,
34236
+ forbidDirectRuntimeExecution: true,
34237
+ forbidDirectThrows: true
34238
+ }
33750
34239
  };
33751
34240
  function defineSupacloudConfig(config = {}) {
33752
34241
  if (config.delivery !== undefined)
@@ -33758,7 +34247,8 @@ function defineSupacloudConfig(config = {}) {
33758
34247
  ...DEFAULT_SUPACLOUD_CONFIG,
33759
34248
  ...config,
33760
34249
  include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
33761
- graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
34250
+ graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
34251
+ effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
33762
34252
  };
33763
34253
  }
33764
34254
  function validateGovernanceConfig(config) {
@@ -33815,6 +34305,34 @@ function validateGovernanceConfig(config) {
33815
34305
  if (config[key] !== undefined && typeof config[key] !== "boolean")
33816
34306
  throw new Error(`${key} must be a boolean.`);
33817
34307
  }
34308
+ if (config.effect !== undefined) {
34309
+ if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => ![
34310
+ "requireRouteEffects",
34311
+ "requireErrorMappings",
34312
+ "requireDependencies",
34313
+ "requireTaggedErrorTypes",
34314
+ "requireExactDependencyTypes",
34315
+ "requireTimeoutForDependencies",
34316
+ "forbidDirectRuntimeExecution",
34317
+ "forbidDirectThrows"
34318
+ ].includes(key))) {
34319
+ throw new Error("effect contains an unsupported governance option.");
34320
+ }
34321
+ for (const key of [
34322
+ "requireRouteEffects",
34323
+ "requireErrorMappings",
34324
+ "requireDependencies",
34325
+ "requireTaggedErrorTypes",
34326
+ "requireExactDependencyTypes",
34327
+ "requireTimeoutForDependencies",
34328
+ "forbidDirectRuntimeExecution",
34329
+ "forbidDirectThrows"
34330
+ ]) {
34331
+ if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
34332
+ throw new Error(`effect.${key} must be boolean.`);
34333
+ }
34334
+ }
34335
+ }
33818
34336
  }
33819
34337
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
33820
34338
  const resolved = defineSupacloudConfig(config);
@@ -33835,6 +34353,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
33835
34353
  ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
33836
34354
  ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
33837
34355
  ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
34356
+ effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
33838
34357
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
33839
34358
  graphql: resolved.graphql ? {
33840
34359
  ...resolved.graphql,
@@ -34237,6 +34756,7 @@ export {
34237
34756
  scanProductionSource,
34238
34757
  schemaFromSource,
34239
34758
  serializeOpenApiJson,
34759
+ validateEffectPolicies,
34240
34760
  validateFeatureSpec,
34241
34761
  validateGraph,
34242
34762
  validateRouteContracts,