1import { assert, describe, it } from "@effect/vitest"
2import { Schema, SchemaAST, SchemaParser } from "effect"
3import * as CompilerRegistry from "effect/internal/schema/compilerRegistry"
4import * as SchemaAOTCompiler from "effect/schema/SchemaAOTCompiler"
5import * as SchemaTransformation from "effect/SchemaTransformation"
6import { execFileSync } from "node:child_process"
7import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
8import { join } from "node:path"
9import { fileURLToPath, pathToFileURL } from "node:url"
10import { roots, schemas, suspendEvaluations } from "./fixtures/aot.ts"
11
12describe("SchemaAOTCompiler", { concurrent: false }, () => {
13 it("emits deterministic modules without installing a decoder", () => {
14 let checks = 0
15 const schema = Schema.Struct({
16 value: Schema.String.check(Schema.makeFilter(() => {
17 checks++
18 return true
19 }))
20 })
21 const before = CompilerRegistry.resolve(schema.ast)
22 const targets = [{ ast: schema.ast, operations: ["decode"] }] as const
23 const source = SchemaAOTCompiler.compile(targets)
24 assert.strictEqual(SchemaAOTCompiler.compile(targets), source)
25 assert.strictEqual(CompilerRegistry.resolve(schema.ast), before)
26 assert.strictEqual(checks, 0)
27 assert.include(source, "effect/schema/SchemaCompiler/runtime")
28 assert.notInclude(source, "new Function")
29 assert.notInclude(source, "SchemaJITCompiler")
30 })
31
32 it("emits only the requested operation family", () => {
33 const schema = Schema.Struct({ value: Schema.String })
34 const decode = SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode"] }])
35 assert.include(decode, "case \"decode\":{")
36 assert.include(decode, "case \"decodeEffect\":{")
37 assert.notInclude(decode, "case \"is\":{")
38 assert.notInclude(decode, "case \"make\":{")
39 assert.notInclude(decode, "case \"makeEffect\":{")
40
41 const make = SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["make"] }])
42 assert.include(make, "case \"make\":{")
43 assert.include(make, "case \"makeEffect\":{")
44 assert.notInclude(make, "case \"is\":{")
45 assert.notInclude(make, "case \"decode\":{")
46 })
47
48 it("allows a target without requested operations", () => {
49 const schema = Schema.Struct({ value: Schema.String })
50 const source = SchemaAOTCompiler.compile([{ ast: schema.ast, operations: [] }])
51 assert.notInclude(source, "runtime.setCompiler(")
52 })
53
54 it("omits fast decode operations from diagnostic-only dependencies", () => {
55 const child = Schema.Struct({ value: Schema.String })
56 const schema = Schema.Array(child)
57 const source = SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode"] }])
58 assert.strictEqual(source.match(/case "decode":\{/g)?.length, 1)
59 assert.strictEqual(source.match(/case "decodeEffect":\{/g)?.length, 2)
60
61 const targeted = SchemaAOTCompiler.compile([
62 { ast: schema.ast, operations: ["decode"] },
63 { ast: child.ast, operations: ["decode"] }
64 ])
65 assert.strictEqual(targeted.match(/case "decode":\{/g)?.length, 2)
66 assert.strictEqual(targeted.match(/case "decodeEffect":\{/g)?.length, 2)
67 })
68
69 it("uses registry fallbacks for operations that were not requested", async () => {
70 const schema = Schema.Struct({ value: Schema.String })
71 const directory = mkdtempSync(fileURLToPath(new URL("../../.schema-aot-operations-test-", import.meta.url)))
72 try {
73 const file = join(directory, "decode.mjs")
74 writeFileSync(file, SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode"] }]))
75 const generated = await import(`${pathToFileURL(file).href}?test=${Date.now()}`)
76 generated.install([schema.ast])
77
78 const entry = CompilerRegistry.resolve(schema.ast)
79 assert.strictEqual(typeof entry.compiled, "function")
80 assert.isDefined(entry.decode)
81 assert.isUndefined(entry.is)
82 assert.isUndefined(entry.make)
83 assert.strictEqual(SchemaParser.is(schema)({ value: "a" }), true)
84 assert.deepStrictEqual(SchemaParser.make(schema)({ value: "a" }), { value: "a" })
85 } finally {
86 rmSync(directory, { recursive: true, force: true })
87 }
88 })
89
90 it("honors enumerable excess checking directly in generated operations", async () => {
91 const visible = Symbol("visible")
92 const hidden = Symbol("hidden")
93 const schema = Schema.StructWithRest(Schema.Struct({ fixed: Schema.optionalKey(Schema.String) }), [
94 Schema.Record(Schema.TemplateLiteral(["field-", Schema.Number]), Schema.Number),
95 Schema.Record(Schema.Symbol, Schema.Number)
96 ])
97 const directory = mkdtempSync(fileURLToPath(new URL("../../.schema-aot-excess-test-", import.meta.url)))
98 try {
99 const file = join(directory, "decode.mjs")
100 writeFileSync(file, SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode", "is"] }]))
101 const generated = await import(pathToFileURL(file).href)
102 generated.install([schema.ast])
103 const { decode, is } = CompilerRegistry.resolve(schema.ast)
104 assert.ok(decode, "Expected a compiled decoder")
105 assert.ok(is, "Expected a compiled type guard")
106 const strict = { onExcessProperty: "error" } as const
107 const expected = { "field-1": 1, [visible]: 2 }
108 const input = Object.defineProperties({ ...expected }, {
109 fixed: { value: "declared", enumerable: false },
110 "field-2": { value: "invalid", enumerable: false },
111 [hidden]: {
112 enumerable: false,
113 get() {
114 throw new Error("Non-enumerable properties must not be read")
115 }
116 }
117 })
118 for (const options of [SchemaAST.defaultParseOptions, strict]) {
119 assert.deepStrictEqual(decode(input, options), { ...expected, fixed: "declared" })
120 assert.isTrue(is(input, options))
121 assert.strictEqual(decode({ [visible]: "invalid" }, options), CompilerRegistry.invalid)
122 }
123 assert.strictEqual(decode({ ...expected, extra: 1 }, strict), CompilerRegistry.invalid)
124 assert.isFalse(is({ ...expected, extra: 1 }, strict))
125 assert.deepStrictEqual(decode({}, strict), {})
126 } finally {
127 rmSync(directory, { recursive: true, force: true })
128 }
129 })
130
131 it("initializes dependency operations on first use", async () => {
132 const child = Schema.String.pipe(
133 Schema.decodeTo(
134 Schema.Number,
135 SchemaTransformation.transform({ decode: Number, encode: String })
136 ),
137 Schema.annotate({ title: "lazy AOT dependency" })
138 )
139 const schema = Schema.Struct({ child })
140 const directory = mkdtempSync(fileURLToPath(new URL("../../.schema-aot-lazy-test-", import.meta.url)))
141 try {
142 const file = join(directory, "decode.mjs")
143 writeFileSync(file, SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode"] }]))
144 const generated = await import(`${pathToFileURL(file).href}?test=${Date.now()}`)
145 generated.install([schema.ast])
146
147 const dependency = CompilerRegistry.resolve(child.ast)
148 assert.isFalse(Object.hasOwn(dependency, "decodeEffect"))
149
150 const decode = SchemaParser.decodeUnknownSync(schema)
151 assert.deepStrictEqual(decode({ child: "1" }), { child: 1 })
152 assert.isFalse(Object.hasOwn(dependency, "decodeEffect"))
153
154 assert.throws(() => decode({ child: false }))
155 assert.isTrue(Object.hasOwn(dependency, "decodeEffect"))
156 } finally {
157 rmSync(directory, { recursive: true, force: true })
158 }
159 })
160
161 it("deduplicates repeated roots and dependencies shared across roots", () => {
162 const child = Schema.Struct({ value: Schema.String })
163 const first = Schema.Struct({ child })
164 const second = Schema.Array(child)
165 const source = SchemaAOTCompiler.compile([
166 { ast: first.ast, operations: ["decode"] },
167 { ast: second.ast, operations: ["decode"] }
168 ])
169 assert.strictEqual(
170 SchemaAOTCompiler.compile([
171 { ast: first.ast, operations: ["decode"] },
172 { ast: second.ast, operations: ["decode"] },
173 { ast: first.ast, operations: ["decode"] },
174 { ast: second.ast, operations: ["decode"] }
175 ]),
176 source
177 )
178 assert.strictEqual(source.match(/runtime\.setCompiler\(/g)?.length, 3)
179 })
180
181 it("reuses identical decoder factories", () => {
182 const schema = Schema.Union(
183 Array.from({ length: 8 }, (_, tag) => Schema.Struct({ tag: Schema.Literal(tag), value: Schema.Number }))
184 )
185 const source = SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode"] }])
186 assert.strictEqual(source.match(/function d\d+\(ast,resolve,operation\)/g)?.length, 2)
187 })
188
189 it("preserves required, optional, and __proto__ property presence checks", () => {
190 const schema = Schema.Struct({
191 required: Schema.UndefinedOr(Schema.String),
192 plain: Schema.String,
193 optional: Schema.optionalKey(Schema.String),
194 ["__proto__"]: Schema.String
195 })
196 const decode = SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode"] }])
197 assert.include(decode, "if(!(\"required\" in i))return I")
198 assert.notInclude(decode, "if(!(\"plain\" in i))")
199 assert.notInclude(decode, "if(!(\"optional\" in i))")
200 assert.include(decode, "if(\"optional\" in i){")
201 assert.include(decode, "if(!(Object.hasOwn(i,\"__proto__\")))return I")
202
203 const guard = SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["is"] }])
204 assert.include(guard, "if(!(\"required\" in i))return false")
205 })
206
207 it("runs generated decoders without dynamic code generation", () => {
208 const directory = mkdtempSync(fileURLToPath(new URL("../../.schema-aot-test-", import.meta.url)))
209 try {
210 for (const [name, schema] of Object.entries(schemas)) {
211 writeFileSync(
212 join(directory, `${name}.mjs`),
213 SchemaAOTCompiler.compile([{ ast: schema.ast, operations: ["decode", "is", "make"] }])
214 )
215 }
216 writeFileSync(
217 join(directory, "all.mjs"),
218 SchemaAOTCompiler.compile(roots.map((ast) => ({ ast, operations: ["decode", "is", "make"] })))
219 )
220 writeFileSync(join(directory, "empty.mjs"), SchemaAOTCompiler.compile([]))
221 assert.strictEqual(suspendEvaluations, 0)
222 for (const mode of ["single", "multiple"]) {
223 const output = execFileSync(process.execPath, [
224 "--disallow-code-generation-from-strings",
225 "--import",
226 fileURLToPath(new URL("./fixtures/aot-import-guard.ts", import.meta.url)),
227 fileURLToPath(new URL("./fixtures/aot-runner.ts", import.meta.url)),
228 directory,
229 mode
230 ], { encoding: "utf8" })
231 assert.include(output, "AOT integration passed")
232 }
233 } finally {
234 rmSync(directory, { recursive: true, force: true })
235 }
236 }, 20_000)
237})