| 1 | import * as babel from "@babel/core" |
| 2 | import { assert, describe, it } from "@effect/vitest" |
| 3 | import * as esbuild from "esbuild" |
| 4 | import * as fs from "node:fs/promises" |
| 5 | import * as path from "node:path" |
| 6 | import { fileURLToPath } from "node:url" |
| 7 | |
| 8 | const packageDir = fileURLToPath(new URL("..", import.meta.url)) |
| 9 | const effectSrcDir = fileURLToPath(new URL("../../../effect/src/", import.meta.url)) |
| 10 | |
| 11 | // The published build runs babel with annotate-pure-calls over the compiler |
| 12 | // output. Bundlers rely on those annotations to drop unused module-scope |
| 13 | // calls, so the same step is reproduced here for every effect source module. |
| 14 | // Without it esbuild shakes source differently from the published package. |
| 15 | const annotatePureCalls: esbuild.Plugin = { |
| 16 | name: "annotate-pure-calls", |
| 17 | setup(build) { |
| 18 | build.onLoad({ filter: /\.ts$/ }, async (args) => { |
| 19 | if (!args.path.startsWith(effectSrcDir)) { |
| 20 | return undefined |
| 21 | } |
| 22 | const source = await fs.readFile(args.path, "utf8") |
| 23 | const transpiled = await esbuild.transform(source, { |
| 24 | loader: "ts", |
| 25 | format: "esm", |
| 26 | target: "es2022", |
| 27 | sourcefile: args.path, |
| 28 | tsconfigRaw: { compilerOptions: { verbatimModuleSyntax: true } } |
| 29 | }) |
| 30 | const annotated = await babel.transformAsync(transpiled.code, { |
| 31 | babelrc: false, |
| 32 | configFile: false, |
| 33 | compact: false, |
| 34 | cwd: packageDir, |
| 35 | filename: args.path, |
| 36 | plugins: ["annotate-pure-calls"] |
| 37 | }) |
| 38 | return { contents: annotated!.code!, loader: "js" } |
| 39 | }) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | // Bundles a fixture the way wrangler does (esbuild, worker conditions) and |
| 44 | // returns the effect source modules that made it into the output. Only the |
| 45 | // output entry of the metafile lists retained modules; `metafile.inputs` is |
| 46 | // the whole import graph, tree-shaken or not. |
| 47 | const bundledEffectModules = async (fixture: string): Promise<ReadonlyArray<string>> => { |
| 48 | const result = await esbuild.build({ |
| 49 | entryPoints: [path.join(packageDir, "fixtures", fixture)], |
| 50 | bundle: true, |
| 51 | write: false, |
| 52 | metafile: true, |
| 53 | format: "esm", |
| 54 | platform: "browser", |
| 55 | target: "es2022", |
| 56 | conditions: ["workerd", "worker", "browser"], |
| 57 | logLevel: "silent", |
| 58 | absWorkingDir: packageDir, |
| 59 | plugins: [annotatePureCalls] |
| 60 | }) |
| 61 | const [output] = Object.values(result.metafile.outputs) |
| 62 | return Object.keys(output.inputs) |
| 63 | .map((input) => path.relative(effectSrcDir, path.resolve(packageDir, input))) |
| 64 | .filter((module) => !module.startsWith("..")) |
| 65 | .sort() |
| 66 | } |
| 67 | |
| 68 | describe("http-router fixture bundled with esbuild", () => { |
| 69 | // esbuild transpiles and Babel annotates every reachable Effect source module. |
| 70 | // Under full-suite Bun CI load this can exceed Vitest's 5s default. |
| 71 | it("does not include any Schema module", { timeout: 20_000 }, async () => { |
| 72 | const modules = await bundledEffectModules("http-router.ts") |
| 73 | assert.include(modules, "http/HttpRouter.ts") |
| 74 | assert.include(modules, "http/HttpServerRespondable.ts") |
| 75 | const schemaModules = modules.filter((module) => |
| 76 | /^(?:Schema[A-Za-z]*\.ts|schema\/|internal\/schema\/)/.test(module) |
| 77 | ) |
| 78 | assert.deepStrictEqual(schemaModules, [], `Schema modules reached the router bundle: ${schemaModules.join(", ")}`) |
| 79 | }) |
| 80 | }) |