ReviewOS

also looking at this

stacks/stx

fix(client-script): bind template names shadowed by inlined imports (#1767)

#1768
Merged glennmichael123 wants to merge fix/1767-client-script-import-shadow-collision into main
4 files +97 -1
packages/stx/src/client-script-bundler.tsmodified+30-0
Changes to packages/stx/src/client-script-bundler.ts
@@ -338,6 +338,25 @@ export async function bundleClientScript(
338338 break
339339 }
340340
341 // Capture Bun's export-rename map BEFORE stripping (stacksjs/stx#1767).
342 // When an inlined import shares a top-level name with a component-declared
343 // const, Bun.build keeps the import under the bare name and RENAMES the
344 // local (`foo` -> `foo2`); the catch-all export we added above then comes
345 // back as `export { foo2 as foo }`. That `as` clause is the only record of
346 // the rename. If we strip it blindly, the scope registrar re-harvests the
347 // bare `foo` — now the raw import — and binds the template to the wrong
348 // value. So collect [originalName, renamedBinding] pairs here and re-expose
349 // them below, letting the registrar bind the original name to the correct
350 // (renamed) identifier without touching the import the local closes over.
351 const renamePairs: Array<[string, string]> = []
352 for (const exp of bundled.matchAll(/^\s*export\s*\{([^}]*)\}\s*;?\s*$/gm)) {
353 for (const spec of exp[1].split(',')) {
354 const asMatch = spec.trim().match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/)
355 if (asMatch && asMatch[1] !== asMatch[2])
356 renamePairs.push([asMatch[2], asMatch[1]]) // [exported/original name, renamed local binding]
357 }
358 }
359
341360 // Strip ESM export artifacts from the output
342361 // Bun.build with format: 'esm' may add `export { ... }` at the end
343362 bundled = bundled
@@ -345,6 +364,17 @@ export async function bundleClientScript(
345364 .replace(/^\s*export\s+default\s+.*$/gm, '')
346365 .trim()
347366
367 // Re-expose renamed component declarations under their ORIGINAL names as a
368 // plain map (originalName -> renamed binding). The scope registrar merges
369 // this so the template binds to the component's declaration, not the
370 // shadowing import. We do NOT reassign the bare name (`var foo = foo2`):
371 // the renamed local's closure still reads the import under `foo`, so
372 // clobbering it would break the binding it depends on. See #1767.
373 if (renamePairs.length > 0) {
374 const entries = renamePairs.map(([orig, id]) => `${JSON.stringify(orig)}: ${id}`).join(', ')
375 bundled += `\nvar __stxScopeRenames = { ${entries} };`
376 }
377
348378 // Strip any remaining external import statements (stx, @stores etc.)
349379 // These will be handled by transformAutoImports and transformStoreImports
350380 // Keep them as-is — the existing transforms expect to find them