Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit84576bd

Browse files
authored
fix: prevent island names from shadowing JavaScript globals (#3522)
fix#3521 Generated PR message below---## ProblemIsland files named after JavaScript global objects (e.g., Map.tsx,`Set.tsx`, `Array.tsx`, `Promise.tsx`) cause runtime errors. Thegenerated boot code uses object shorthand syntax that resolves to theglobal constructor instead of the imported component:```javascriptimport Map from "/islands/Map.tsx";boot({Map}, props); // {Map} resolves to global Map constructor```This results in `TypeError: Map is not a constructor` when the runtimeattempts to instantiate islands.## SolutionGenerate safe prefixed variable names (`__FRSH_ISLAND_N`) for islandimports and use explicit object property syntax to preserve the originalisland names:```javascriptimport __FRSH_ISLAND_0 from "/islands/Map.tsx";boot({"Map":__FRSH_ISLAND_0}, props);```This approach:- Eliminates variable shadowing in all contexts (no scope collisions)- Preserves original island names in HTML markers, error messages, andruntime registry- Allows any valid filename without restrictions- Follows existing Fresh conventions for generated identifiers## Changes- preact_hooks.ts: Updated client-side boot script generation- dev_build_cache.ts: Updated server-side snapshot generation- Added integration tests for both Builder and Vite build systems- Added documentation comment explaining why globals aren't in`JS_RESERVED`## TestingBoth tests verify that islands named after globals build successfullyand use safe variable names in generated code. Tests confirm the bug isdetected when fixes are reverted.
1 parenta45cfe1 commit84576bd

File tree

8 files changed

+231
-0
lines changed

8 files changed

+231
-0
lines changed

‎packages/fresh/src/dev/builder_test.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,3 +838,70 @@ Deno.test("specToName", () => {
838838
expect(specToName("/islands/_bar-baz-...-$.tsx")).toEqual("_bar_baz_$");
839839
expect(specToName("/islands/1_hello.tsx")).toEqual("_hello");
840840
});
841+
842+
Deno.test({
843+
name:"Builder - island named after global object (Map)",
844+
fn:async()=>{
845+
constroot=path.join(import.meta.dirname!,"..","..");
846+
await using_tmp=awaitwithTmpDir({dir:root,prefix:"tmp_builder_"});
847+
consttmp=_tmp.dir;
848+
849+
awaitwriteFiles(tmp,{
850+
"islands/Map.tsx":`import { useSignal } from "@preact/signals";
851+
import { useEffect } from "preact/hooks";
852+
853+
export default function Map() {
854+
const ready = useSignal(false);
855+
const count = useSignal(0);
856+
857+
useEffect(() => {
858+
ready.value = true;
859+
}, []);
860+
861+
return (
862+
<div class={ready.value ? "ready" : ""}>
863+
<p class="output">{count.value}</p>
864+
<button
865+
type="button"
866+
class="increment"
867+
onClick={() => count.value = count.peek() + 1}
868+
>
869+
increment
870+
</button>
871+
</div>
872+
);
873+
}`,
874+
"routes/index.tsx":`import Map from "../islands/Map.tsx";
875+
export default () => <Map />;`,
876+
"main.ts":`import { App } from "fresh";
877+
export const app = new App().fsRoutes();`,
878+
});
879+
880+
constbuilder=newBuilder({
881+
root:tmp,
882+
outDir:path.join(tmp,"dist"),
883+
});
884+
885+
// Register the island manually
886+
constislandPath=path.join(tmp,"islands","Map.tsx");
887+
builder.registerIsland(path.toFileUrl(islandPath).href);
888+
889+
awaitbuilder.build();
890+
891+
awaitwithChildProcessServer(
892+
{cwd:tmp,args:["serve","-A","dist/server.js"]},
893+
async(address)=>{
894+
constres=awaitfetch(address);
895+
consthtml=awaitres.text();
896+
897+
// Verify the fix: UniqueNamer prefixes "Map" with underscore to prevent shadowing
898+
// Without the fix: import Map from "..." and boot({Map}, ...) - shadows global Map
899+
// With the fix: import _Map_N from "..." and boot({_Map_N}, ...) - no shadowing
900+
expect(html).toMatch(/import\s+_Map_\d+\s+from/);
901+
expect(html).toMatch(/boot\(\s*\{\s*_Map_\d+\s*\}/);
902+
},
903+
);
904+
},
905+
sanitizeOps:false,
906+
sanitizeResources:false,
907+
});

‎packages/fresh/src/utils.ts‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ export class UniqueNamer {
7676
}
7777

7878
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words
79+
// Includes JavaScript reserved keywords and global objects to prevent variable shadowing
7980
constJS_RESERVED=newSet([
81+
// Reserved keywords
8082
"break",
8183
"case",
8284
"catch",
@@ -147,6 +149,98 @@ const JS_RESERVED = new Set([
147149
"get",
148150
"of",
149151
"set",
152+
// JavaScript built-in objects that could cause shadowing bugs
153+
"Array",
154+
"ArrayBuffer",
155+
"Boolean",
156+
"DataView",
157+
"Date",
158+
"Error",
159+
"EvalError",
160+
"Float32Array",
161+
"Float64Array",
162+
"Function",
163+
"Infinity",
164+
"Int8Array",
165+
"Int16Array",
166+
"Int32Array",
167+
"Intl",
168+
"JSON",
169+
"Map",
170+
"Math",
171+
"NaN",
172+
"Number",
173+
"Object",
174+
"Promise",
175+
"Proxy",
176+
"RangeError",
177+
"ReferenceError",
178+
"Reflect",
179+
"RegExp",
180+
"Set",
181+
"String",
182+
"Symbol",
183+
"SyntaxError",
184+
"TypeError",
185+
"Uint8Array",
186+
"Uint8ClampedArray",
187+
"Uint16Array",
188+
"Uint32Array",
189+
"URIError",
190+
"WeakMap",
191+
"WeakSet",
192+
"BigInt",
193+
"BigInt64Array",
194+
"BigUint64Array",
195+
// Web APIs commonly used in islands
196+
"console",
197+
"fetch",
198+
"Request",
199+
"Response",
200+
"Headers",
201+
"URL",
202+
"URLSearchParams",
203+
"Event",
204+
"EventTarget",
205+
"AbortController",
206+
"AbortSignal",
207+
"FormData",
208+
"Blob",
209+
"File",
210+
"FileReader",
211+
"TextEncoder",
212+
"TextDecoder",
213+
"ReadableStream",
214+
"WritableStream",
215+
"TransformStream",
216+
"WebSocket",
217+
"Worker",
218+
"MessageChannel",
219+
"MessagePort",
220+
"BroadcastChannel",
221+
"crypto",
222+
"atob",
223+
"btoa",
224+
"setTimeout",
225+
"setInterval",
226+
"clearTimeout",
227+
"clearInterval",
228+
"queueMicrotask",
229+
"structuredClone",
230+
// Browser-specific globals
231+
"document",
232+
"window",
233+
"navigator",
234+
"location",
235+
"history",
236+
"localStorage",
237+
"sessionStorage",
238+
// Deno-specific globals
239+
"Deno",
240+
// Node.js compatibility globals (for Deno's Node compat mode)
241+
"process",
242+
"global",
243+
"Buffer",
150244
]);
151245

152246
constPATH_TO_SPEC=/[\\/]+/g;

‎packages/plugin-vite/tests/build_test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,27 @@ Deno.test({
542542
sanitizeOps:false,
543543
sanitizeResources:false,
544544
});
545+
546+
Deno.test({
547+
name:"vite build - island named after global object (Map)",
548+
fn:async()=>{
549+
constfixture=path.join(FIXTURE_DIR,"island_global_name");
550+
await usingres=awaitbuildVite(fixture);
551+
552+
awaitlaunchProd(
553+
{cwd:res.tmp},
554+
async(address)=>{
555+
constresponse=awaitfetch(address);
556+
consthtml=awaitresponse.text();
557+
558+
// Verify the fix: UniqueNamer prefixes "Map" with underscore to prevent shadowing
559+
// Without the fix: import Map from "..." and boot({Map}, ...) - shadows global Map
560+
// With the fix: import _Map_N from "..." and boot({_Map_N}, ...) - no shadowing
561+
expect(html).toMatch(/import.*_Map(_\d+)?.*from/);
562+
expect(html).toMatch(/boot\(\s*\{\s*_Map(_\d+)?\s*\}/);
563+
},
564+
);
565+
},
566+
sanitizeOps:false,
567+
sanitizeResources:false,
568+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"imports": {
3+
"fresh":"jsr:@fresh/core",
4+
"@preact/signals":"npm:@preact/signals@^1.3.0",
5+
"preact":"npm:preact@^10.26.1",
6+
"preact/":"npm:/preact@^10.26.1/"
7+
}
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import{useSignal}from"@preact/signals";
2+
import{useEffect}from"preact/hooks";
3+
4+
exportdefaultfunctionMap(){
5+
constready=useSignal(false);
6+
constcount=useSignal(0);
7+
8+
useEffect(()=>{
9+
ready.value=true;
10+
},[]);
11+
12+
return(
13+
<divclass={ready.value ?"ready" :""}>
14+
<pclass="output">{count.value}</p>
15+
<button
16+
type="button"
17+
class="increment"
18+
onClick={()=>count.value=count.peek()+1}
19+
>
20+
increment
21+
</button>
22+
</div>
23+
);
24+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import{App}from"fresh";
2+
3+
exportconstapp=newApp().fsRoutes();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
importMapfrom"../islands/Map.tsx";
2+
3+
exportdefaultfunctionPage(){
4+
return<Map/>;
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import{defineConfig}from"vite";
2+
import{fresh}from"@fresh/plugin-vite";
3+
4+
exportdefaultdefineConfig({
5+
plugins:[fresh()],
6+
});

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp