Skip to content

Commit 8adf441

Browse files
fix(includes): defensively register scope vars to survive harvest mis-hits
`extractExports` walks a client script's bundled output looking for top-level const/let/var/function declarations. Its depth tracker handles strings, template literals, and {} braces — but not regex literals or other nested constructs. When a `<script client>` pulls in a heavier third-party (e.g. `ts-medium-editor`) via Bun.build, the bundled blob contains regex literals like `/\{.../` that flip the depth counter and let inner-scope vars (e.g. `message`, `parent`, `frag`) leak into the harvested top-level list. The downstream effect was fatal: `transformSignalScript` emitted window.stx._scopes['stx_scope_X'] = { isIE, ..., message, ... }; and at hydration `{ message }` (shorthand for `{ message: message }`) threw `ReferenceError: message is not defined` — taking the whole scope registration down with it and leaving the component unregistered. Switch the registration to a try/catch per name. Stray harvested identifiers just resolve to `undefined` (skipped) instead of crashing, so a single false-positive can no longer brick the rest of the scope's vars. Cleaner long-term fix is to teach the depth walker about regex literals — left as a follow-up since this defensive shape is correct even with a perfect harvester.
1 parent bc03a5a commit 8adf441

1 file changed

Lines changed: 17 additions & 1 deletion

File tree

packages/stx/src/includes.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,20 @@ function extractExports(setupContent: string): string {
248248
*/
249249
function transformSignalScript(scriptContent: string, scopeId: string): string {
250250
const exports = extractExports(scriptContent)
251+
const exportNames = exports.split(',').map(s => s.trim()).filter(Boolean)
252+
253+
// Build the scope assignment defensively. `extractExports` is a
254+
// heuristic walker — when client scripts include bundled third-party
255+
// sources (e.g. `ts-medium-editor` inlined via Bun.build) the depth
256+
// tracker can mis-identify inner-scope vars as top-level, putting
257+
// names like `message`, `parent`, `frag` into the export list. Doing
258+
// `{ message, ... }` then throws `ReferenceError: message is not
259+
// defined` at hydration and kills the whole scope's registration.
260+
// Wrap each property in a try/catch so a stray harvested name turns
261+
// into `undefined` instead of a fatal ReferenceError.
262+
const scopeAssign = exportNames.map(name =>
263+
` try { __scopeRegistration[${JSON.stringify(name)}] = ${name}; } catch (e) { /* not actually top-level */ }`,
264+
).join('\n')
251265

252266
// Use real window.stx APIs (signals runtime is injected in <head>, runs before this script).
253267
// No polyfill fallbacks — they create signals without ._isSignal which breaks auto-unwrap
@@ -263,7 +277,9 @@ ${scriptContent}
263277
264278
// Register scope variables for STX runtime
265279
if (!window.stx._scopes) window.stx._scopes = {};
266-
window.stx._scopes['${scopeId}'] = { ${exports}, __destroyCallbacks: __destroyHooks };
280+
var __scopeRegistration = { __destroyCallbacks: __destroyHooks };
281+
${scopeAssign}
282+
window.stx._scopes['${scopeId}'] = __scopeRegistration;
267283
})();
268284
`
269285
}

0 commit comments

Comments
 (0)