Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions bend2/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,12 @@ function cli_base(what?: string): void {
cli_say(1, want.join("\n\n") + "\n");
}

// An inline module script; an empty svg <script/> can't have content.
function is_inline_module(el: HTMLRewriterTypes.Element): boolean {
return el.canHaveContent && !el.hasAttribute("src")
&& el.getAttribute("type")?.trim().toLowerCase() === "module";
}

// 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.
Expand All @@ -497,13 +503,25 @@ async function cli_bundle(page: string, dir: string): Promise<void> {
({ contents: code.get(a.path)!, loader: "js" }));
build.onLoad({ filter: /\.html$/ }, async (a) => {
const src = await Bun.file(a.path).text();
// HTMLRewriter gives no offsets, so a first pass marks where each
// script's text starts, with a mark the page doesn't hold.
let mark = "BEND_INLINE_";
while (src.includes(mark)) {
mark += "_";
}
const marked = new HTMLRewriter().on("script", {
element(el) {
if (is_inline_module(el)) {
el.prepend(mark);
}
},
}).transform(src);
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")) {
if (is_inline_module(el)) {
const name = "." + path.basename(a.path) + ".bend-inline-"
+ mods.length + ".js";
mod = { id: path.join(path.dirname(a.path), name), text: "" };
Expand All @@ -520,13 +538,13 @@ async function cli_bundle(page: string, dir: string): Promise<void> {
}).transform(src);
let end = 0;
let line = 0;
for (const m of mods) {
const at = src.indexOf(m.text, end);
for (const [i, m] of mods.entries()) {
const skip = i * mark.length;
const at = marked.indexOf(mark, end + skip) - skip;
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;
end = at;
}
return { contents: html, loader: "html" };
});
Expand Down
23 changes: 21 additions & 2 deletions tests/io/bundle_inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ function bundle_inline() {
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", '<script type="module">\nimport M from "./m.bend";\n'
put("ok.html", '<svg><script type="module"/></svg>\n'
+ '<script type="module">\nimport M from "./m.bend";\n'
+ "console.log(M.main());\n</script>\n");
put("bad.html", '<p>\n<script\n type="module">\nconst x = ;\n</script>\n');
put("repeated.html", '<!--\n<script type="module">\nconst x = ;\n'
+ '</script>\n-->\n<script type="module">\nconst x = ;\n</script>\n');
put("multi.html", '<svg><script type="module"/></svg>\n'
+ '<svg><script type="module"><![CDATA[\n]]></script></svg>\n'
+ '<!-- BEND_INLINE_START_0_ -->\n'
+ '<script type="module">\nconsole.log(1);\n</script>\n'
+ '<!--\nconst x = ;\n-->\n'
+ '<script type="module">\nconst x = ;\n</script>\n');
if (build("ok.html").status !== 0) {
return 1;
}
Expand All @@ -24,7 +33,17 @@ function bundle_inline() {
return 2;
}
const bad = build("bad.html");
return bad.status === 1 && bad.stderr.includes("bad.html:4:") ? 0 : 3;
if (bad.status !== 1 || !bad.stderr.includes("bad.html:4:11")) {
return 3;
}
const repeated = build("repeated.html");
if (repeated.status !== 1
|| !repeated.stderr.includes("repeated.html:7:11")) {
return 4;
}
const multi = build("multi.html");
return multi.status === 1 && multi.stderr.includes("multi.html:12:11")
? 0 : 5;
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
Expand Down