|
| 1 | +/** |
| 2 | + * Bundle the API server into a single ESM file using esbuild. |
| 3 | + * |
| 4 | + * All JS code (including WASM-loader modules) is bundled. The .wasm binary |
| 5 | + * files are copied next to the output so that runtime fs.readFileSync() |
| 6 | + * and import.meta.url-based resolution finds them. |
| 7 | + * |
| 8 | + * An esbuild plugin rewrites `await import("...")` dynamic imports in mupdf |
| 9 | + * to synchronous `require("...")` calls. This is required because pkg (the |
| 10 | + * standalone-binary compiler) doesn't support dynamic import() in its runtime. |
| 11 | + */ |
| 12 | +import { build } from "esbuild" |
| 13 | +import fs from "node:fs" |
| 14 | +import path from "node:path" |
| 15 | +import { fileURLToPath } from "node:url" |
| 16 | + |
| 17 | +const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
| 18 | +const root = path.resolve(__dirname, "..") |
| 19 | +const monorepoRoot = path.resolve(root, "../..") |
| 20 | +const outDir = path.join(root, "dist-pkg") |
| 21 | + |
| 22 | +/** |
| 23 | + * esbuild plugin: replace `await import("node:fs")` and `await import("module")` |
| 24 | + * with synchronous require() calls in mupdf source files. This eliminates |
| 25 | + * dynamic ESM imports that pkg's Node.js snapshot runtime can't handle. |
| 26 | + */ |
| 27 | +const replaceDynamicImports = { |
| 28 | + name: "replace-dynamic-imports", |
| 29 | + setup(build) { |
| 30 | + build.onLoad({ filter: /mupdf.*\.(js|mjs)$/ }, async (args) => { |
| 31 | + let source = await fs.promises.readFile(args.path, "utf-8") |
| 32 | + let modified = false |
| 33 | + |
| 34 | + // Replace await import("node:fs") → require("node:fs") |
| 35 | + if (source.includes('await import("node:fs")')) { |
| 36 | + source = source.replace( |
| 37 | + /await import\("node:fs"\)/g, |
| 38 | + 'require("node:fs")' |
| 39 | + ) |
| 40 | + modified = true |
| 41 | + } |
| 42 | + |
| 43 | + // Replace await import("module") → require("node:module") |
| 44 | + if (source.includes('await import("module")')) { |
| 45 | + source = source.replace( |
| 46 | + /await import\("module"\)/g, |
| 47 | + 'require("node:module")' |
| 48 | + ) |
| 49 | + modified = true |
| 50 | + } |
| 51 | + |
| 52 | + if (!modified) return undefined // let esbuild handle it normally |
| 53 | + |
| 54 | + return { |
| 55 | + contents: source, |
| 56 | + loader: args.path.endsWith(".mjs") ? "js" : "js", |
| 57 | + } |
| 58 | + }) |
| 59 | + }, |
| 60 | +} |
| 61 | + |
| 62 | +await build({ |
| 63 | + entryPoints: [path.join(root, "src/index.ts")], |
| 64 | + bundle: true, |
| 65 | + platform: "node", |
| 66 | + target: "node20", |
| 67 | + format: "esm", |
| 68 | + outfile: path.join(outDir, "api-server.mjs"), |
| 69 | + plugins: [replaceDynamicImports], |
| 70 | + banner: { |
| 71 | + js: [ |
| 72 | + // Polyfill __dirname, __filename, and require for ESM |
| 73 | + // (needed by Emscripten-generated WASM loaders that use CJS patterns) |
| 74 | + 'import { createRequire as __polyfill_createRequire } from "node:module";', |
| 75 | + 'import { fileURLToPath as __polyfill_fileURLToPath } from "node:url";', |
| 76 | + 'import { dirname as __polyfill_dirname } from "node:path";', |
| 77 | + "var __filename = __polyfill_fileURLToPath(import.meta.url);", |
| 78 | + "var __dirname = __polyfill_dirname(__filename);", |
| 79 | + "var require = __polyfill_createRequire(import.meta.url);", |
| 80 | + ].join("\n"), |
| 81 | + }, |
| 82 | +}) |
| 83 | + |
| 84 | +// Copy .wasm files next to the bundle so runtime loaders find them. |
| 85 | +// Search the pnpm store since these packages are transitive deps. |
| 86 | +const WASM_PACKAGES = ["node-sqlite3-wasm", "mupdf", "@resvg/resvg-wasm"] |
| 87 | + |
| 88 | +fs.mkdirSync(outDir, { recursive: true }) |
| 89 | + |
| 90 | +for (const pkg of WASM_PACKAGES) { |
| 91 | + const pnpmDir = path.join(monorepoRoot, "node_modules/.pnpm") |
| 92 | + const safeName = pkg.replace(/\//g, "+").replace(/@/g, "") |
| 93 | + const dirs = fs.readdirSync(pnpmDir).filter((d) => { |
| 94 | + const normalized = d.replace(/@/g, "").replace(/\//g, "+") |
| 95 | + return normalized.startsWith(safeName) |
| 96 | + }) |
| 97 | + |
| 98 | + for (const dir of dirs) { |
| 99 | + const pkgPath = pkg.startsWith("@") |
| 100 | + ? path.join(pnpmDir, dir, "node_modules", ...pkg.split("/")) |
| 101 | + : path.join(pnpmDir, dir, "node_modules", pkg) |
| 102 | + |
| 103 | + if (!fs.existsSync(pkgPath)) continue |
| 104 | + |
| 105 | + for (const sub of [".", "dist", "lib"]) { |
| 106 | + const searchDir = path.join(pkgPath, sub) |
| 107 | + if (!fs.existsSync(searchDir)) continue |
| 108 | + for (const file of fs.readdirSync(searchDir)) { |
| 109 | + if (file.endsWith(".wasm")) { |
| 110 | + fs.copyFileSync(path.join(searchDir, file), path.join(outDir, file)) |
| 111 | + console.log(` Copied ${file}`) |
| 112 | + } |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +console.log("✓ Bundled → dist-pkg/api-server.mjs") |
0 commit comments