fix: escape NUL bytes in HFMASK regex (Bun blank renders) + Windows junction for studio preview links#2140
Merged
Merged
Conversation
Raw 0x00 bytes in the maskInertRegions token and restore regex made timingCompiler.ts binary to git and shipped raw NULs into dist/cli.js. Bun's transpiler (<= 1.3.11) corrupts raw NULs in regex literals into literal backslash-uFFFD text, so restore never matched: every masked <style>/<script> region was dropped, the player never initialized, and bunx renders produced blank white frames showing HFMASK tokens. Use \u0000 escapes instead, which survive any transpile layer, and add a byte-level regression test (behavior is identical under Node, so only a byte check catches this). Fixes the first half of #2139.
linkProjectIntoStudioData called symlinkSync(dir, path, "dir"), which needs Developer Mode or elevation on Windows, so preview and dev in local-studio mode died with EPERM for default-configured users. Junctions need no privilege, work for directories, and keep the live write-back the studio depends on (a copy fallback would decouple the studio from the real project). Covers both preview and dev, which share the helper. Fixes the second half of #2139.
Collaborator
Author
|
GitHub renders The actual change, with control bytes shown as diff --git a/packages/core/src/compiler/timingCompiler.ts b/packages/core/src/compiler/timingCompiler.ts
index eb55a112a..b5c3a06fe 100644
--- a/packages/core/src/compiler/timingCompiler.ts
+++ b/packages/core/src/compiler/timingCompiler.ts
@@ -86,15 +86,18 @@ function injectAttr(tag: string, attr: string, value: string): string {
const INERT_REGION_RE =
/<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>/gi;
+// The NUL delimiters must stay as \u0000 escapes: raw 0x00 bytes make this file
+// binary to git and are corrupted by Bun's transpiler when bundled (issue #2139).
function maskInertRegions(html: string): { masked: string; restore: (s: string) => string } {
const stash: string[] = [];
const masked = html.replace(INERT_REGION_RE, (region) => {
- const token = `^@HFMASK${stash.length}^@`;
+ const token = `\u0000HFMASK${stash.length}\u0000`;
stash.push(region);
return token;
});
const restore = (s: string): string =>
- s.replace(/^@HFMASK(\d+)^@/g, (_, i) => stash[Number(i)] ?? "");
+ // oxlint-disable-next-line no-control-regex -- NUL cannot appear in HTML, which is what makes it a safe mask delimiter
+ s.replace(/\u0000HFMASK(\d+)\u0000/g, (_, i) => stash[Number(i)] ?? "");
return { masked, restore };
}
|
Fixes #2139 (both defects).
1. Raw NUL bytes in the HFMASK restore regex (blank renders under Bun <= 1.3.11)
maskInertRegionsintimingCompiler.tshad literal 0x00 bytes typed into the token template and the restore regex (introduced in #1940). That made the source file binary to git and shipped two raw NULs intodist/cli.js. Bun's transpiler (<= 1.3.11) rewrites raw NULs in regex literals into literal replacement-character text, so restore matched nothing, masked<style>/<script>regions were silently dropped, the injected player never initialized, andbunx hyperframes renderproduced blank white frames showingHFMASKtokens.Fix: the delimiters are now
\u0000escapes, which survive any transpile/minify layer. Added a byte-level regression test: behavior is identical under Node, so only a byte check catches a reintroduction (lint could not parse the raw byte either, which is how it slipped through).Verified:
0.7.49bundle: 2 raw NULs, both in the restore regex; rebuilt bundle: 0@hyperframes/coredist passes under Node 22, Bun 1.3.5, and Bun 1.3.142.
previewEPERM on Windows without Developer ModelinkProjectIntoStudioDatawas the one symlink site with no Windows guard:symlinkSync(dir, path, "dir")needs Developer Mode or elevation, so local-studiopreview(anddev, same helper) died with EPERM for default-configured users.Fix: use an NTFS junction on win32. Junctions need no privilege, work for directories, and keep the live write-back the studio depends on (a
cpSyncfallback would decouple the studio's copy from the real project, so it was deliberately not used). Node normalizes junction targets to absolute paths, and libuv reports junctions as symlinks, so the existing stale-link cleanup and exit-time unlink keep working.Not reproducible on macOS; validated against the stack trace and repro steps in the issue, which point at exactly this call site.