@supacloud/compiler 0.41.0 → 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/analyze.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { ApplicationGraph, DependencyGraphCache } from "./types";
1
+ import type { ApplicationGraph, DependencyGraphCache, EffectCompilerOptions } from "./types";
2
2
  /**
3
3
  * Analyzes source code under rootDir via the native TypeScript Program API.
4
4
  * Decorators are matched by name only (Module/Injectable/Inject/Command/Query/Controller/Get/...),
5
5
  * without checking import origins, so this package does not need to depend on @supacloud/app.
6
6
  */
7
- export declare function analyzeProject(rootDir: string, include?: string[], cache?: DependencyGraphCache, changedPaths?: string[]): Promise<ApplicationGraph>;
7
+ export declare function analyzeProject(rootDir: string, include?: string[], cache?: DependencyGraphCache, changedPaths?: string[], effectOptions?: EffectCompilerOptions): Promise<ApplicationGraph>;
package/dist/cli.js CHANGED
@@ -1248,6 +1248,13 @@ var init_validate = __esm(() => {
1248
1248
  "effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
1249
1249
  "effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
1250
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" },
1251
1258
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1252
1259
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1253
1260
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -27815,6 +27822,13 @@ function withDiagnosticMetadata(diagnostic) {
27815
27822
  const metadata = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
27816
27823
  return metadata ? { ...diagnostic, errorCode: metadata.code, docsUrl: metadata.docsUrl } : diagnostic;
27817
27824
  }
27825
+ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
27826
+ requireTaggedErrorTypes: true,
27827
+ requireExactDependencyTypes: true,
27828
+ requireTimeoutForDependencies: true,
27829
+ forbidDirectRuntimeExecution: true,
27830
+ forbidDirectThrows: true
27831
+ };
27818
27832
  function nodeText(node) {
27819
27833
  return node.getText(node.getSourceFile());
27820
27834
  }
@@ -27860,7 +27874,7 @@ function descendantsOfKind(root, predicate) {
27860
27874
  visit(root);
27861
27875
  return result;
27862
27876
  }
27863
- async function analyzeProject(rootDir, include, cache, changedPaths) {
27877
+ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptions = {}) {
27864
27878
  const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
27865
27879
  if (cache)
27866
27880
  cache.programSession = session;
@@ -27879,8 +27893,21 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
27879
27893
  moduleHandlerFiles: new Map,
27880
27894
  infraResources: new Map,
27881
27895
  infraResourceClasses: new Map,
27882
- 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
+ }
27883
27905
  };
27906
+ if (ctx.effectOptions.forbidDirectRuntimeExecution) {
27907
+ for (const sourceFile of sourceFiles) {
27908
+ scanEffectRuntimeEscapes(sourceFile, ctx);
27909
+ }
27910
+ }
27884
27911
  const nativeTraitFiles = new Map;
27885
27912
  for (const diagnostic of session.getDiagnostics()) {
27886
27913
  ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
@@ -29056,11 +29083,150 @@ function declaredClassType(declaration, ctx) {
29056
29083
  return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
29057
29084
  }
29058
29085
  function typeArgumentsOf(type, ctx) {
29059
- 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 ?? [];
29060
29096
  }
29061
29097
  function isTypeReference(type) {
29062
29098
  return "target" in type;
29063
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
+ }
29064
29230
  function isUnknownOrAny(type) {
29065
29231
  return (type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0;
29066
29232
  }
@@ -29124,7 +29290,8 @@ function parseController(input, ctx) {
29124
29290
  handler: propertyName(method.name)
29125
29291
  };
29126
29292
  const signature = ctx.checker.getSignatureFromDeclaration(method);
29127
- const resultType = signature && ctx.checker.typeToString(signature.getReturnType());
29293
+ const returnType = signature?.getReturnType();
29294
+ const resultType = returnType && ctx.checker.typeToString(returnType);
29128
29295
  if (resultType && /\bResponse\b/.test(resultType))
29129
29296
  route.nativeResponse = true;
29130
29297
  const pathParams = [];
@@ -29455,6 +29622,18 @@ function parseController(input, ctx) {
29455
29622
  });
29456
29623
  } else {
29457
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
+ }
29458
29637
  }
29459
29638
  }
29460
29639
  const errorsExpr = getProp(effectObject, "errors");
@@ -29503,10 +29682,26 @@ function parseController(input, ctx) {
29503
29682
  errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
29504
29683
  }
29505
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
+ }
29506
29697
  }
29507
29698
  }
29508
29699
  route.effect = effect;
29509
- if (!resultType || !/\bEffect(?:\.Effect)?(?:<|\s)/.test(resultType)) {
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) {
29510
29705
  ctx.diagnostics.push({
29511
29706
  severity: "error",
29512
29707
  code: "effect-return-type",
@@ -29518,18 +29713,37 @@ function parseController(input, ctx) {
29518
29713
  suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
29519
29714
  });
29520
29715
  }
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
- });
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.");
29533
29747
  }
29534
29748
  }
29535
29749
  }
@@ -30851,7 +31065,7 @@ async function renderOptionalGraphql(options) {
30851
31065
  }
30852
31066
  async function compileProject(options) {
30853
31067
  options = withDefaultGovernance(options);
30854
- 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);
30855
31069
  const diagnostics = [
30856
31070
  ...graph.diagnostics ?? [],
30857
31071
  ...validateGraph(graph, options),
@@ -30916,7 +31130,7 @@ async function compileProject(options) {
30916
31130
  }
30917
31131
  async function checkProject(options) {
30918
31132
  options = withDefaultGovernance(options);
30919
- 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);
30920
31134
  const diagnostics = [
30921
31135
  ...graph.diagnostics ?? [],
30922
31136
  ...validateGraph(graph, options),
@@ -31404,6 +31618,7 @@ function optionsKeyOf(options) {
31404
31618
  openApi: options.openApi,
31405
31619
  generatePermissions: options.generatePermissions,
31406
31620
  typeSafety: options.typeSafety,
31621
+ effect: options.effect,
31407
31622
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
31408
31623
  graphql: options.graphql
31409
31624
  });
@@ -31735,7 +31950,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
31735
31950
  effect: {
31736
31951
  requireRouteEffects: true,
31737
31952
  requireErrorMappings: true,
31738
- requireDependencies: true
31953
+ requireDependencies: true,
31954
+ requireTaggedErrorTypes: true,
31955
+ requireExactDependencyTypes: true,
31956
+ requireTimeoutForDependencies: true,
31957
+ forbidDirectRuntimeExecution: true,
31958
+ forbidDirectThrows: true
31739
31959
  }
31740
31960
  };
31741
31961
  function defineSupacloudConfig(config = {}) {
@@ -31807,10 +32027,28 @@ function validateGovernanceConfig(config) {
31807
32027
  throw new Error(`${key} must be a boolean.`);
31808
32028
  }
31809
32029
  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"]) {
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
+ ]) {
31814
32052
  if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
31815
32053
  throw new Error(`effect.${key} must be boolean.`);
31816
32054
  }
@@ -33124,7 +33362,7 @@ import { resolve as resolve18 } from "node:path";
33124
33362
  var MIGRATION_APP_VERSION = "0.24.0";
33125
33363
 
33126
33364
  // src/migration-elysia-version.ts
33127
- var MIGRATION_ELYSIA_VERSION = "0.28.0";
33365
+ var MIGRATION_ELYSIA_VERSION = "0.29.0";
33128
33366
 
33129
33367
  // src/migration-policy.ts
33130
33368
  var cachedCompilerVersion;
package/dist/index.js CHANGED
@@ -1247,6 +1247,13 @@ var init_validate = __esm(() => {
1247
1247
  "effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
1248
1248
  "effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
1249
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" },
1250
1257
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1251
1258
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1252
1259
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -27774,6 +27781,13 @@ function withDiagnosticMetadata(diagnostic) {
27774
27781
  const metadata = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
27775
27782
  return metadata ? { ...diagnostic, errorCode: metadata.code, docsUrl: metadata.docsUrl } : diagnostic;
27776
27783
  }
27784
+ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
27785
+ requireTaggedErrorTypes: true,
27786
+ requireExactDependencyTypes: true,
27787
+ requireTimeoutForDependencies: true,
27788
+ forbidDirectRuntimeExecution: true,
27789
+ forbidDirectThrows: true
27790
+ };
27777
27791
  function nodeText(node) {
27778
27792
  return node.getText(node.getSourceFile());
27779
27793
  }
@@ -27819,7 +27833,7 @@ function descendantsOfKind(root, predicate) {
27819
27833
  visit(root);
27820
27834
  return result;
27821
27835
  }
27822
- async function analyzeProject(rootDir, include, cache, changedPaths) {
27836
+ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptions = {}) {
27823
27837
  const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
27824
27838
  if (cache)
27825
27839
  cache.programSession = session;
@@ -27838,8 +27852,21 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
27838
27852
  moduleHandlerFiles: new Map,
27839
27853
  infraResources: new Map,
27840
27854
  infraResourceClasses: new Map,
27841
- 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
+ }
27842
27864
  };
27865
+ if (ctx.effectOptions.forbidDirectRuntimeExecution) {
27866
+ for (const sourceFile of sourceFiles) {
27867
+ scanEffectRuntimeEscapes(sourceFile, ctx);
27868
+ }
27869
+ }
27843
27870
  const nativeTraitFiles = new Map;
27844
27871
  for (const diagnostic of session.getDiagnostics()) {
27845
27872
  ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
@@ -29015,11 +29042,150 @@ function declaredClassType(declaration, ctx) {
29015
29042
  return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
29016
29043
  }
29017
29044
  function typeArgumentsOf(type, ctx) {
29018
- 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 ?? [];
29019
29055
  }
29020
29056
  function isTypeReference(type) {
29021
29057
  return "target" in type;
29022
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
+ }
29023
29189
  function isUnknownOrAny(type) {
29024
29190
  return (type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0;
29025
29191
  }
@@ -29083,7 +29249,8 @@ function parseController(input, ctx) {
29083
29249
  handler: propertyName(method.name)
29084
29250
  };
29085
29251
  const signature = ctx.checker.getSignatureFromDeclaration(method);
29086
- const resultType = signature && ctx.checker.typeToString(signature.getReturnType());
29252
+ const returnType = signature?.getReturnType();
29253
+ const resultType = returnType && ctx.checker.typeToString(returnType);
29087
29254
  if (resultType && /\bResponse\b/.test(resultType))
29088
29255
  route.nativeResponse = true;
29089
29256
  const pathParams = [];
@@ -29414,6 +29581,18 @@ function parseController(input, ctx) {
29414
29581
  });
29415
29582
  } else {
29416
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
+ }
29417
29596
  }
29418
29597
  }
29419
29598
  const errorsExpr = getProp(effectObject, "errors");
@@ -29462,10 +29641,26 @@ function parseController(input, ctx) {
29462
29641
  errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
29463
29642
  }
29464
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
+ }
29465
29656
  }
29466
29657
  }
29467
29658
  route.effect = effect;
29468
- if (!resultType || !/\bEffect(?:\.Effect)?(?:<|\s)/.test(resultType)) {
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) {
29469
29664
  ctx.diagnostics.push({
29470
29665
  severity: "error",
29471
29666
  code: "effect-return-type",
@@ -29477,18 +29672,37 @@ function parseController(input, ctx) {
29477
29672
  suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
29478
29673
  });
29479
29674
  }
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
- });
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.");
29492
29706
  }
29493
29707
  }
29494
29708
  }
@@ -30812,7 +31026,7 @@ async function renderOptionalGraphql(options) {
30812
31026
  }
30813
31027
  async function compileProject(options) {
30814
31028
  options = withDefaultGovernance(options);
30815
- 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);
30816
31030
  const diagnostics = [
30817
31031
  ...graph.diagnostics ?? [],
30818
31032
  ...validateGraph(graph, options),
@@ -30877,7 +31091,7 @@ async function compileProject(options) {
30877
31091
  }
30878
31092
  async function checkProject(options) {
30879
31093
  options = withDefaultGovernance(options);
30880
- 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);
30881
31095
  const diagnostics = [
30882
31096
  ...graph.diagnostics ?? [],
30883
31097
  ...validateGraph(graph, options),
@@ -32528,6 +32742,7 @@ function optionsKeyOf(options) {
32528
32742
  openApi: options.openApi,
32529
32743
  generatePermissions: options.generatePermissions,
32530
32744
  typeSafety: options.typeSafety,
32745
+ effect: options.effect,
32531
32746
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
32532
32747
  graphql: options.graphql
32533
32748
  });
@@ -32840,7 +33055,7 @@ import { resolve as resolve17 } from "node:path";
32840
33055
  var MIGRATION_APP_VERSION = "0.24.0";
32841
33056
 
32842
33057
  // src/migration-elysia-version.ts
32843
- var MIGRATION_ELYSIA_VERSION = "0.28.0";
33058
+ var MIGRATION_ELYSIA_VERSION = "0.29.0";
32844
33059
 
32845
33060
  // src/migration-policy.ts
32846
33061
  var cachedCompilerVersion;
@@ -34014,7 +34229,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
34014
34229
  effect: {
34015
34230
  requireRouteEffects: true,
34016
34231
  requireErrorMappings: true,
34017
- requireDependencies: true
34232
+ requireDependencies: true,
34233
+ requireTaggedErrorTypes: true,
34234
+ requireExactDependencyTypes: true,
34235
+ requireTimeoutForDependencies: true,
34236
+ forbidDirectRuntimeExecution: true,
34237
+ forbidDirectThrows: true
34018
34238
  }
34019
34239
  };
34020
34240
  function defineSupacloudConfig(config = {}) {
@@ -34086,10 +34306,28 @@ function validateGovernanceConfig(config) {
34086
34306
  throw new Error(`${key} must be a boolean.`);
34087
34307
  }
34088
34308
  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"]) {
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
+ ]) {
34093
34331
  if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
34094
34332
  throw new Error(`effect.${key} must be boolean.`);
34095
34333
  }
@@ -1 +1 @@
1
- export declare const MIGRATION_ELYSIA_VERSION = "0.28.0";
1
+ export declare const MIGRATION_ELYSIA_VERSION = "0.29.0";
package/dist/types.d.ts CHANGED
@@ -456,6 +456,16 @@ export interface EffectCompilerOptions {
456
456
  requireErrorMappings?: boolean;
457
457
  /** Require an explicit dependency list for Effect environments. */
458
458
  requireDependencies?: boolean;
459
+ /** Reject unknown/any failures and require a tagged failure union. */
460
+ requireTaggedErrorTypes?: boolean;
461
+ /** Require effect.dependencies to exactly match the Effect environment type. */
462
+ requireExactDependencyTypes?: boolean;
463
+ /** Require a timeout whenever the route declares Effect dependencies. */
464
+ requireTimeoutForDependencies?: boolean;
465
+ /** Reject direct Effect.run* calls inside governed route handlers. */
466
+ forbidDirectRuntimeExecution?: boolean;
467
+ /** Reject direct throw statements inside governed route handlers. */
468
+ forbidDirectThrows?: boolean;
459
469
  }
460
470
  export interface GraphqlOptions {
461
471
  /** Additionally emit standard TypedDocumentNode artifacts for GraphQL ecosystem clients. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.41.0",
3
+ "version": "0.42.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",