1import { assert, describe, it } from "@effect/vitest"
2import { Effect, FileSystem, Path, SchemaParser } from "effect"
3import * as CompilerRegistry from "effect/internal/schema/compilerRegistry"
4import * as SchemaAOTCompilerBuild from "effect/schema/SchemaAOTCompiler/Build"
5import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
6import { join } from "node:path"
7import { fileURLToPath, pathToFileURL } from "node:url"
8import * as Schemas from "./fixtures/aot-build.ts"
9
10describe("SchemaAOTCompilerBuild", { concurrent: false }, () => {
11 it.effect("writes a deterministic self-installing module", () =>
12 Effect.gen(function*() {
13 const directory = yield* Effect.acquireRelease(
14 Effect.sync(() => mkdtempSync(fileURLToPath(new URL("../../.schema-aot-build-test-", import.meta.url)))),
15 (directory) => Effect.sync(() => rmSync(directory, { recursive: true, force: true }))
16 )
17 const outFile = join(directory, "generated.mjs")
18 const fileSystem = FileSystem.layerNoop({
19 makeDirectory: (path, options) =>
20 Effect.sync(() => {
21 mkdirSync(path, options)
22 }),
23 writeFileString: (path, source) => Effect.sync(() => writeFileSync(path, source))
24 })
25 const options = {
26 modules: {
27 "./fixtures/aot-build.ts": () => Promise.resolve(Schemas)
28 },
29 baseUrl: import.meta.url,
30 outFile,
31 operations: ["make", "decode", "is", "encode", "decode"]
32 } as const
33
34 const first = yield* SchemaAOTCompilerBuild.build(options).pipe(
35 Effect.provide(fileSystem),
36 Effect.provide(Path.layer)
37 )
38 const source = yield* Effect.sync(() => readFileSync(outFile, "utf8"))
39 const second = yield* SchemaAOTCompilerBuild.build(options).pipe(
40 Effect.provide(fileSystem),
41 Effect.provide(Path.layer)
42 )
43 const secondSource = yield* Effect.sync(() => readFileSync(outFile, "utf8"))
44
45 assert.deepStrictEqual(first, {
46 outFile,
47 modules: 1,
48 schemas: 2
49 })
50 assert.deepStrictEqual(second, first)
51 assert.strictEqual(secondSource, source)
52 assert.include(source, "effect/SchemaAST")
53 assert.include(source, "A.flip(m0[\"Port\"].ast)")
54 assert.include(source, "A.toType(m0[\"Port\"].ast)")
55 assert.notInclude(source, "ignored")
56
57 const before = CompilerRegistry.resolve(Schemas.User.ast)
58 yield* Effect.promise(() => import(`${pathToFileURL(outFile).href}?test=${Date.now()}`))
59 assert.notStrictEqual(CompilerRegistry.resolve(Schemas.User.ast), before)
60 assert.deepStrictEqual(SchemaParser.decodeUnknownSync(Schemas.User)({ name: "Ada", port: "8080" }), {
61 name: "Ada",
62 port: 8080
63 })
64 assert.strictEqual(SchemaParser.is(Schemas.User)({ name: "Ada", port: 8080 }), true)
65 assert.deepStrictEqual(SchemaParser.make(Schemas.User)({ name: "Ada", port: 8080 }), {
66 name: "Ada",
67 port: 8080
68 })
69 assert.deepStrictEqual(SchemaParser.encodeUnknownSync(Schemas.User)({ name: "Ada", port: 8080 }), {
70 name: "Ada",
71 port: "8080"
72 })
73 }))
74
75 it.effect("uses decoding as the default operation and ignores non-Schema exports", () =>
76 Effect.gen(function*() {
77 let written = ""
78 const result = yield* SchemaAOTCompilerBuild.build({
79 modules: {
80 "./fixtures/aot-build.ts": () => Promise.resolve(Schemas),
81 "./fixtures/ignored.ts": () => Promise.resolve({ value: 1 })
82 },
83 baseUrl: import.meta.url,
84 outFile: "/generated/schema-aot.mjs"
85 }).pipe(
86 Effect.provide(FileSystem.layerNoop({
87 makeDirectory: () => Effect.void,
88 writeFileString: (_path, source) =>
89 Effect.sync(() => {
90 written = source
91 })
92 })),
93 Effect.provide(Path.layer)
94 )
95
96 assert.deepStrictEqual(result, {
97 outFile: "/generated/schema-aot.mjs",
98 modules: 1,
99 schemas: 2
100 })
101 assert.notInclude(written, "import * as A from \"effect/SchemaAST\"")
102 assert.notInclude(written, "ignored.ts")
103 assert.notInclude(written, "case \"is\":{")
104 assert.notInclude(written, "case \"make\":{")
105 assert.notInclude(written, "case \"makeEffect\":{")
106 assert.include(written, "install([m0[\"Port\"].ast,m0[\"User\"].ast]);")
107 }))
108
109 it.effect("reports module loading failures", () =>
110 Effect.gen(function*() {
111 const error = yield* SchemaAOTCompilerBuild.build({
112 modules: {
113 "./fixtures/failing.ts": () => Promise.reject("boom")
114 },
115 baseUrl: import.meta.url,
116 outFile: "/generated/schema-aot.mjs"
117 }).pipe(
118 Effect.provide(FileSystem.layerNoop({})),
119 Effect.provide(Path.layer),
120 Effect.flip
121 )
122
123 assert.instanceOf(error, SchemaAOTCompilerBuild.BuildError)
124 assert.strictEqual(error.kind, "LoadModule")
125 assert.strictEqual(error.module, "./fixtures/failing.ts")
126 assert.strictEqual(error.cause, "boom")
127 }))
128
129 it.effect("reports invalid loaded modules", () =>
130 Effect.gen(function*() {
131 const error = yield* SchemaAOTCompilerBuild.build({
132 modules: {
133 "./fixtures/invalid.ts": () => Promise.resolve(1)
134 },
135 baseUrl: import.meta.url,
136 outFile: "/generated/schema-aot.mjs"
137 }).pipe(
138 Effect.provide(FileSystem.layerNoop({})),
139 Effect.provide(Path.layer),
140 Effect.flip
141 )
142
143 assert.instanceOf(error, SchemaAOTCompilerBuild.BuildError)
144 assert.strictEqual(error.kind, "InvalidModule")
145 assert.strictEqual(error.module, "./fixtures/invalid.ts")
146 assert.strictEqual(error.cause, 1)
147 }))
148})