fix(codegen): preserve equivalent suffixed type references (#48024)

This commit is contained in:
Aiden Cline
2026-09-08 15:21:17 -05:00
committed by GitHub
parent a322d7f98f
commit 11aee1b49f
2 changed files with 55 additions and 3 deletions
+9 -3
View File
@@ -46,7 +46,10 @@ export type EffectOutputType = {
readonly import: string
}
type ResolvedEffectTypeReference = Omit<EffectTypeReference, "schema"> & { readonly ast: SchemaAST.AST }
type ResolvedEffectTypeReference = Omit<EffectTypeReference, "schema"> & {
readonly ast: SchemaAST.AST
readonly type: string | undefined
}
export class GenerationError extends Schema.TaggedError<GenerationError>()("GenerationError", {
reason: Schema.String,
@@ -454,7 +457,6 @@ function effectTypeReferences(input: ReadonlyArray<EffectTypeReference>) {
const asts = new Map<SchemaAST.AST, ResolvedEffectTypeReference>()
const brands = new Map<string, ResolvedEffectTypeReference>()
for (const reference of input) {
const value = { name: reference.name, import: reference.import, ast: reference.schema.ast }
const document = SchemaRepresentation.toCodeDocument(
SchemaRepresentation.toRepresentations([codegenAst(Schema.toType(reference.schema).ast)]),
)
@@ -463,6 +465,7 @@ function effectTypeReferences(input: ReadonlyArray<EffectTypeReference>) {
name === undefined
? undefined
: (document.references.nonRecursives.find((item) => item.$ref === name)?.code.Type ?? name)
const value = { name: reference.name, import: reference.import, ast: reference.schema.ast, type }
if (type?.includes("Brand.Brand<") && !brands.has(type)) brands.set(type, value)
if (SchemaAST.resolveIdentifier(reference.schema.ast) !== undefined || type?.includes("Brand.Brand<")) {
asts.set(reference.schema.ast, value)
@@ -499,7 +502,10 @@ function effectType(schema: Schema.Top, references: ReturnType<typeof effectType
"g",
)
if (!pattern.test(type)) continue
const reference = references.names.get(name)
// Optional/encoded occurrences can acquire a numeric suffix. Reuse the named
// type only when its definition matches; a suffix can also denote a different shape.
const candidate = references.names.get(name.replace(/_\d+$/, ""))
const reference = references.names.get(name) ?? (candidate?.type === value ? candidate : undefined)
if (reference !== undefined) {
imports.add(reference.import)
type = type.replace(pattern, reference.name)
@@ -143,6 +143,52 @@ describe("HttpApiCodegen.generate", () => {
expect(source).not.toContain("@example/api")
})
test("preserves named Effect references across optional schema occurrences", () => {
const State = Schema.Record(Schema.String, Schema.Unknown).annotate({ identifier: "Message.State" })
const output = emitEffectShape(
compileContract(
api(
HttpApiEndpoint.get("get", "/session", {
success: Schema.Struct({
encoded: Schema.toEncoded(Schema.Struct({ state: Schema.optionalKey(State) })),
optional: Schema.optional(State),
}),
}),
),
),
{
typeReferences: [
{ schema: State, name: "Message.State", import: 'import type { Message } from "@example/schema/message"' },
],
},
)
const source = output.files[0]?.content
expect(source).toContain('readonly "state"?: Message.State')
expect(source).toContain('readonly "optional"?: Message.State | undefined')
})
test("does not reuse named Effect references for different suffixed shapes", () => {
const State = Schema.Record(Schema.String, Schema.Unknown).annotate({ identifier: "Message.State" })
const Different = Schema.Record(Schema.String, Schema.Number).annotate({ identifier: "Message.State" })
const output = emitEffectShape(
compileContract(
api(
HttpApiEndpoint.get("get", "/session", {
success: Schema.Struct({ original: State, different: Different }),
}),
),
),
{
typeReferences: [
{ schema: State, name: "Message.State", import: 'import type { Message } from "@example/schema/message"' },
],
},
)
const source = output.files[0]?.content
expect(source).toContain('readonly "original": Message.State')
expect(source).toContain('readonly "different": ({ readonly [x: string]: number })')
})
test("allows composed Effect outputs to use an authoritative named type", () => {
const output = emitEffectShape(
compileContract(api(HttpApiEndpoint.get("events", "/event", { success: Schema.Unknown }))),