Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/blueprint-codegen-recursive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@evolution-sdk/evolution": patch
---

Blueprint codegen now supports recursive type schemas — Plutus types that reference themselves
directly or through an intermediate type (e.g. `MultiSig` containing a `List<MultiSig>` field).
Cyclic references are emitted as typed `Schema.suspend` thunks where the encoded type `I` is
inferred by recursively walking the blueprint definition graph rather than hardcoded to `Data.Constr`:
`list` → `readonly ItemEncoded[]`, `map` → `globalThis.Map<Data.Data, Data.Data>`,
`bytes` → `Uint8Array`, `integer` → `bigint`, `constructor` and union → `Data.Constr`,
`$ref` followed transitively up to depth 10. The previous hardcoded `Data.Constr` caused a
TypeScript invariance error for any recursive field referencing a list type.

Several other codegen correctness and API improvements ship in the same release:

- **Namespace emission ordering** — the group-by-namespace emitter is replaced by a streaming emitter
that walks a global topological sort and opens/closes namespace blocks on demand. TypeScript namespace
merging handles split declarations transparently. This fixes cases where a type was emitted before
its cross-namespace dependency (e.g. `Option.OfStakeCredential` appearing before `Cardano.Address.StakeCredential`).

- **Cyclic type emit pattern** — cyclic types now emit a `export type X = ...` / `export const X = ...`
pair with no outer `Schema.suspend` wrapper and no `as` cast. Only the inner field references that
close the cycle use typed thunks: `Schema.suspend((): Schema.Schema<T, I> => T)`.

- **`unionStyle` config** — `CodegenConfig` gains `unionStyle: "Variant" | "Struct" | "TaggedStruct"`
in place of the removed `forceVariant` and `useSuspend` fields. `Struct` emits
`TSchema.Struct({ Tag: TSchema.Struct({...}, { flatFields: true }) }, { flatInUnion: true })`,
`TaggedStruct` emits `TSchema.TaggedStruct("Tag", {...}, { flatInUnion: true })`,
and `Variant` emits `TSchema.Variant({ Tag: {...} })`.

- **Import hygiene** — generated files emit `import { Schema } from "@evolution-sdk/evolution"`
only when cyclic types are present, rather than always importing from `effect` directly.
`CodegenConfig.imports.effect` is replaced by `imports.schema`.

34 changes: 16 additions & 18 deletions docs/app/tools/blueprint-codegen/blueprint-codegen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function BlueprintCodegen() {
const [error, setError] = useState<string | null>(null)
const [optionStyle, setOptionStyle] = useState<"NullOr" | "UndefinedOr" | "Union">("UndefinedOr")
const [moduleStrategy, setModuleStrategy] = useState<"flat" | "namespaced">("namespaced")
const [forceVariant, setForceVariant] = useState(true)
const [unionStyle, setUnionStyle] = useState<"Variant" | "Struct" | "TaggedStruct">("Variant")

const generateTypes = async () => {
setError(null)
Expand All @@ -33,8 +33,7 @@ export function BlueprintCodegen() {
const config = createCodegenConfig({
optionStyle,
moduleStrategy,
forceVariant,
useSuspend: false,
unionStyle,
useRelativeRefs: true,
emptyConstructorStyle: "Literal"
})
Expand Down Expand Up @@ -75,7 +74,8 @@ export function BlueprintCodegen() {

const loadSample = async () => {
try {
const response = await fetch("/evolution-sdk/sample-blueprint.json")
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""
const response = await fetch(`${basePath}/sample-blueprint.json`)
if (!response.ok) {
throw new Error("Failed to load sample blueprint")
}
Expand Down Expand Up @@ -139,21 +139,19 @@ export function BlueprintCodegen() {
</div>

<div className="space-y-2">
<label htmlFor="force-variant" className="text-sm font-medium leading-none">
Force Variant
<label htmlFor="union-style" className="text-sm font-medium leading-none">
Union Style
</label>
<div className="flex items-center h-10">
<input
id="force-variant"
type="checkbox"
checked={forceVariant}
onChange={(e) => setForceVariant(e.target.checked)}
className="h-4 w-4 rounded border-input"
/>
<label htmlFor="force-variant" className="ml-2 text-sm text-muted-foreground">
Use Variant for all unions
</label>
</div>
<select
id="union-style"
value={unionStyle}
onChange={(e) => setUnionStyle(e.target.value as any)}
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
<option value="Variant">Variant</option>
<option value="Struct">Struct (verbose)</option>
<option value="TaggedStruct">TaggedStruct</option>
</select>
</div>
</div>

Expand Down
Loading
Loading