A framework-agnostic, universal document renderer with optional chunked loading
polyrender.wisp.place/
1/**
2 * Minimal esbuild build script — replaces Vite entirely.
3 *
4 * The only "magic" is an esbuild plugin that rewrites the variable-based
5 * dynamic `import(moduleName)` in @polyrender/core's `requirePeerDep` into
6 * static import() calls that esbuild can bundle.
7 *
8 * This is the same fundamental fix needed in Vite, webpack, or any other
9 * bundler — browsers cannot resolve bare specifiers from `import(variable)`.
10 */
11import { build, context } from "esbuild";
12import { cpSync, existsSync, mkdirSync } from "fs";
13import { resolve, dirname } from "path";
14import { fileURLToPath } from "url";
15
16const __dirname = dirname(fileURLToPath(import.meta.url));
17const isWatch = process.argv.includes("--watch");
18
19/**
20 * Plugin that provides empty shims for Node.js built-in modules when bundling
21 * for the browser. Some WASM-based packages (e.g. 7z-wasm) include conditional
22 * Node code paths that are never reached in a browser, but esbuild still tries
23 * to resolve the `require("fs")` / `require("crypto")` calls at bundle time.
24 */
25const shimNodeBuiltins = {
26 name: "shim-node-builtins",
27 setup(build) {
28 const builtins =
29 /^(fs|crypto|path|os|module|stream|util|events|buffer|assert|http|https|net|tls|url|zlib|readline|child_process|worker_threads|perf_hooks)$/;
30 build.onResolve({ filter: builtins }, (args) => ({
31 path: args.path,
32 namespace: "node-builtin-shim",
33 }));
34 build.onLoad({ filter: /.*/, namespace: "node-builtin-shim" }, () => ({
35 contents: "module.exports = {}",
36 loader: "js",
37 }));
38 },
39};
40
41/** Plugin to resolve @polyrender/core's dynamic peer-dep imports. */
42const resolvePeerDeps = {
43 name: "resolve-peer-deps",
44 setup(build) {
45 build.onLoad({ filter: /packages[\\/]core.*\.(js|ts)$/ }, async (args) => {
46 const fs = await import("fs");
47 let contents = fs.readFileSync(args.path, "utf8");
48
49 if (
50 contents.includes("moduleName") &&
51 contents.includes("import(")
52 ) {
53 // Map of peer dep names → static imports
54 const peerDeps = [
55 "pdfjs-dist",
56 "epubjs",
57 "docx-preview",
58 "papaparse",
59 "highlight.js",
60 "jszip",
61 "xlsx",
62 "node-unrar-js",
63 "@jsquash/jxl",
64 "utif",
65 ];
66 // 7z-wasm's default ESM build imports Node's 'module' built-in,
67 // so we redirect to its UMD build which is browser-safe.
68 const sevenZipCase = ` case '7z-wasm': return import('7z-wasm/7zz.umd.js').then(m => m.default || m);`;
69 const cases = peerDeps
70 .map(
71 (d) =>
72 ` case '${d}': return import('${d}').then(m => m.default || m);`,
73 )
74 .join("\n");
75
76 const replacement = [
77 "(async (name) => { switch(name) {",
78 cases,
79 sevenZipCase,
80 " default: throw new Error(`Unknown peer dep: ${name}`);",
81 " }})(moduleName)",
82 ].join("\n");
83
84 contents = contents.replace(
85 /await\s+import\(\s*(?:\/\*.*?\*\/\s*)?moduleName\s*\)/g,
86 `await ${replacement}`,
87 );
88 }
89
90 return {
91 contents,
92 loader: args.path.endsWith(".ts") ? "ts" : "js",
93 };
94 });
95 },
96};
97
98// Ensure dist directory exists
99const distDir = resolve(__dirname, "dist");
100if (!existsSync(distDir)) mkdirSync(distDir, { recursive: true });
101
102// Copy index.html to dist
103cpSync(resolve(__dirname, "index.html"), resolve(distDir, "index.html"));
104
105// Copy styles.css to dist
106const stylesPath = resolve(__dirname, "../../packages/core/src/styles.css");
107if (existsSync(stylesPath)) {
108 cpSync(stylesPath, resolve(distDir, "styles.css"));
109}
110
111// Copy pdfjs worker to dist
112const workerGlob = resolve(__dirname, "node_modules/pdfjs-dist/build");
113if (existsSync(workerGlob)) {
114 const workerFiles = [
115 "pdf.worker.min.mjs",
116 "pdf.worker.mjs",
117 "pdf.worker.min.js",
118 ];
119 for (const wf of workerFiles) {
120 const src = resolve(workerGlob, wf);
121 if (existsSync(src)) {
122 cpSync(src, resolve(distDir, wf));
123 break;
124 }
125 }
126}
127
128const buildOptions = {
129 entryPoints: [resolve(__dirname, "src/main.ts")],
130 bundle: true,
131 format: "esm",
132 platform: "browser",
133 outdir: distDir,
134 sourcemap: true,
135 target: "es2022",
136 plugins: [shimNodeBuiltins, resolvePeerDeps],
137 logLevel: "info",
138};
139
140if (isWatch) {
141 const ctx = await context(buildOptions);
142 await ctx.watch();
143 console.log("Watching for changes...");
144} else {
145 await build(buildOptions);
146 console.log(`\nBuild complete! Serve with:\n npx serve dist\n`);
147}