@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 +2 -2
- package/dist/cli.js +263 -25
- package/dist/index.js +263 -25
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
29522
|
-
|
|
29523
|
-
|
|
29524
|
-
|
|
29525
|
-
|
|
29526
|
-
|
|
29527
|
-
|
|
29528
|
-
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
|
|
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) => ![
|
|
31811
|
-
|
|
31812
|
-
|
|
31813
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
29481
|
-
|
|
29482
|
-
|
|
29483
|
-
|
|
29484
|
-
|
|
29485
|
-
|
|
29486
|
-
|
|
29487
|
-
|
|
29488
|
-
|
|
29489
|
-
|
|
29490
|
-
|
|
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.
|
|
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) => ![
|
|
34090
|
-
|
|
34091
|
-
|
|
34092
|
-
|
|
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.
|
|
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.
|
|
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",
|