diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts
index 1bab57364..724ad9bfa 100644
--- a/packages/core/src/compiler/htmlBundler.test.ts
+++ b/packages/core/src/compiler/htmlBundler.test.ts
@@ -5,6 +5,7 @@ import { join } from "node:path";
import { parseHTML } from "linkedom";
import { describe, it, expect } from "vitest";
import { bundleToSingleHtml } from "./htmlBundler";
+import { getHyperframeRuntimeScript } from "../generated/runtime-inline";
function makeTempProject(files: Record): string {
const dir = mkdtempSync(join(tmpdir(), "hf-bundler-test-"));
@@ -82,6 +83,55 @@ describe("bundleToSingleHtml", () => {
expect(innerLength).toBeGreaterThan(1000);
});
+ it("preserves `$&` replace-pattern characters in the inlined runtime body", async () => {
+ // Regression guard: `injectInterceptor` used to insert the runtime via
+ // `sanitized.replace("", `${tag}\n`)`. `String.prototype.replace`'s
+ // second argument is a substitution template — `$&` expands to the matched
+ // substring (here, ``). The minified runtime IIFE contains legitimate
+ // `$&` sequences (e.g. `if(te&&$&!y.hasAttribute(...))`), so the bundler
+ // silently injected stray `` tags inside the runtime, producing a JS
+ // SyntaxError that broke every timeline in the bundle. Switching to the
+ // function-replacer form passes the runtime body through verbatim.
+ // Use a document with an explicit `` so the bundler takes the
+ // `sanitized.replace("", …)` injection path — the only branch that
+ // exercises the substitution-template behavior. Authoring without a
+ // `` falls back to slice+concat (safe but doesn't catch this bug).
+ const dir = makeTempProject({
+ "index.html": `
+
+
+`,
+ });
+
+ const previousUrl = process.env.HYPERFRAME_RUNTIME_URL;
+ delete process.env.HYPERFRAME_RUNTIME_URL;
+ let bundled: string;
+ try {
+ bundled = await bundleToSingleHtml(dir);
+ } finally {
+ if (previousUrl !== undefined) process.env.HYPERFRAME_RUNTIME_URL = previousUrl;
+ }
+
+ const original = getHyperframeRuntimeScript();
+ // Sanity: the built runtime exercises this regression (no `$&` means the
+ // test would tautologically pass even with the broken implementation).
+ expect(original).toContain("$&");
+
+ const runtimeBlock = bundled.match(
+ /`;
}
if (sanitized.includes("")) {
- return sanitized.replace("", `${tag}\n`);
+ // Use a function replacer so `String.prototype.replace`'s substitution
+ // patterns (`$&`, `$$`, `$'`, `` $` ``, `$1`–`$99`) inside the inlined
+ // runtime IIFE are passed through verbatim. The minified runtime
+ // contains the literal sequence `$&` as part of legitimate JS, and
+ // the older `(pattern, string)` form would expand it to the matched
+ // ``, silently corrupting the runtime and breaking every
+ // timeline in the bundle with a parse-time SyntaxError.
+ return sanitized.replace("", () => `${tag}\n`);
}
const htmlOpenMatch = sanitized.match(/]*>/i);
if (htmlOpenMatch?.index != null) {