diff --git a/bend2/main.ts b/bend2/main.ts index 74aada1078..32cd6c8715 100755 --- a/bend2/main.ts +++ b/bend2/main.ts @@ -481,16 +481,70 @@ function cli_base(what?: string): void { cli_say(1, want.join("\n\n") + "\n"); } +// cli_bundle builds a page. Bun skips inline module scripts (#1400), so the +// inline plugin moves each to a virtual file beside its page, padded so an +// error points at the page's own line and column. async function cli_bundle(page: string, dir: string): Promise { - const out = await Bun.build({ - entrypoints: [page], - outdir: dir, - target: "browser", - minify: true, - plugins: [PLUGIN], - }); - for (const a of out.outputs) { - cli_say(1, a.path + " (" + (a.size / 1024).toFixed(1) + "kb)\n"); + const code = new Map(); + const inline: BunPlugin = { + name: "bend inline", + setup(build) { + build.onResolve({ filter: /\.bend-inline-\d+\.js$/ }, (a) => { + const id = path.resolve(a.resolveDir, a.path); + return code.has(id) ? { path: id, namespace: "inline" } : undefined; + }); + build.onLoad({ filter: /.*/, namespace: "inline" }, (a) => + ({ contents: code.get(a.path)!, loader: "js" })); + build.onLoad({ filter: /\.html$/ }, async (a) => { + const src = await Bun.file(a.path).text(); + const mods: { id: string; text: string }[] = []; + let mod: { id: string; text: string } | null = null; + const html = new HTMLRewriter().on("script", { + element(el) { + const type = el.getAttribute("type")?.trim().toLowerCase(); + mod = null; + if (type === "module" && !el.hasAttribute("src")) { + const name = "." + path.basename(a.path) + ".bend-inline-" + + mods.length + ".js"; + mod = { id: path.join(path.dirname(a.path), name), text: "" }; + mods.push(mod); + el.setAttribute("src", "./" + name); + } + }, + text(t) { + if (mod) { + mod.text += t.text; + t.remove(); + } + }, + }).transform(src); + let end = 0; + let line = 0; + for (const m of mods) { + const at = src.indexOf(m.text, end); + line += src.slice(end, at).split("\n").length - 1; + const col = at - src.lastIndexOf("\n", at - 1) - 1; + code.set(m.id, "\n".repeat(line) + " ".repeat(col) + m.text); + line += m.text.split("\n").length - 1; + end = at + m.text.length; + } + return { contents: html, loader: "html" }; + }); + }, + }; + try { + const out = await Bun.build({ + entrypoints: [page], + outdir: dir, + target: "browser", + minify: true, + plugins: [inline, PLUGIN], + }); + for (const a of out.outputs) { + cli_say(1, a.path + " (" + (a.size / 1024).toFixed(1) + "kb)\n"); + } + } catch (e) { + throw book_err(e).replace(/\.([^/\\]+)\.bend-inline-\d+\.js/g, "$1"); } } diff --git a/tests/io/bundle_inline.bend b/tests/io/bundle_inline.bend new file mode 100644 index 0000000000..50d453c56a --- /dev/null +++ b/tests/io/bundle_inline.bend @@ -0,0 +1,13 @@ +# An inline module script in a page is bundled with its .bend imports, and +# an error in it points at the page's own line (#1400). +import Base + +def Bundle.inline() -> IO(U32): + import "./bundle_inline.js" + +def main() -> IO(Unit): + do IO: + value : U32 <- Bundle.inline() + IO.print(U32.show(value)) + +#|0 diff --git a/tests/io/bundle_inline.js b/tests/io/bundle_inline.js new file mode 100644 index 0000000000..111750914f --- /dev/null +++ b/tests/io/bundle_inline.js @@ -0,0 +1,33 @@ +function bundle_inline() { + const fs = require("node:fs"); + const path = require("node:path"); + const child = require("node:child_process"); + const dir = fs.mkdtempSync(path.join(process.cwd(), ".bend-page-inline-")); + const run = (...args) => child.spawnSync(process.execPath, args, { + encoding: "utf8", + env: { ...process.env, HOME: dir, BEND_ORIGIN: "http://127.0.0.1:9" }, + }); + const put = (name, text) => fs.writeFileSync(path.join(dir, name), text); + const build = (page) => run(path.resolve("bend2/main.ts"), + path.join(dir, page), "-o", path.join(dir, "out")); + try { + put("m.bend", "import Base\n\nlaw main:\n U32\n\ndef main():\n 42\n"); + put("ok.html", '\n"); + put("bad.html", '

\n\nconst x = ;\n\n'); + if (build("ok.html").status !== 0) { + return 1; + } + const out = path.join(dir, "out"); + const js = fs.readdirSync(out).find((f) => f.endsWith(".js")); + if (run(path.join(out, js)).stdout.trim() !== "42") { + return 2; + } + const bad = build("bad.html"); + return bad.status === 1 && bad.stderr.includes("bad.html:4:") ? 0 : 3; + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +io_eff(CID(Bundle.inline), bundle_inline);