Skip to content

Commit 399cbc3

Browse files
fix(vite): nested islands not working (#3440)
This PR makes nested islands work. Problem was that only entry files have the `src` property in the vite manifest. For that we need to ensure that every island is an entry point. Fixes #3439
1 parent 3fb3cd9 commit 399cbc3

10 files changed

Lines changed: 270 additions & 104 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useEffect, useState } from "preact/hooks";
2+
3+
export function IslandNestedInner() {
4+
const [ready, set] = useState(false);
5+
6+
useEffect(() => {
7+
set(true);
8+
}, []);
9+
10+
return (
11+
<div class={ready ? "inner-ready" : ""}>
12+
<p>Inner</p>
13+
</div>
14+
);
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useEffect, useState } from "preact/hooks";
2+
import { IslandNestedInner } from "./IslandNestedInner.tsx";
3+
4+
export function IslandNestedOuter() {
5+
const [ready, set] = useState(false);
6+
7+
useEffect(() => {
8+
set(true);
9+
}, []);
10+
11+
return (
12+
<div class={ready ? "outer-ready" : ""}>
13+
<p>Outer</p>
14+
<IslandNestedInner />
15+
</div>
16+
);
17+
}

packages/plugin-vite/demo/islands/tests/JsrIsland.tsx

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IslandNestedOuter } from "../../islands/IslandNestedOuter.tsx";
2+
3+
export default function Page() {
4+
return (
5+
<div>
6+
<IslandNestedOuter />
7+
</div>
8+
);
9+
}

packages/plugin-vite/demo/vite.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import tailwind from "@tailwindcss/vite";
66
export default defineConfig({
77
plugins: [
88
inspect(),
9-
fresh(),
9+
fresh({
10+
islandSpecifiers: ["@marvinh-test/fresh-island"],
11+
}),
1012
tailwind(),
1113
],
1214
});

packages/plugin-vite/src/mod.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
110110
preserveEntrySignatures: "strict",
111111
input: {
112112
"client-entry": "fresh:client-entry",
113-
"client-snapshot": "fresh:client-snapshot",
114113
},
115114
},
116115
},
@@ -186,7 +185,7 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
186185
patches(),
187186
...serverSnapshot(fConfig),
188187
clientEntryPlugin(fConfig),
189-
clientSnapshot(fConfig),
188+
...clientSnapshot(fConfig),
190189
buildIdPlugin(),
191190
...devServer(),
192191
prefresh({
Lines changed: 161 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,188 @@
11
import type { Plugin, ViteDevServer } from "vite";
2-
import type { ResolvedFreshViteConfig } from "../utils.ts";
3-
import { crawlFsItem } from "fresh/internal-dev";
2+
import { pathWithRoot, type ResolvedFreshViteConfig } from "../utils.ts";
3+
import { crawlFsItem, specToName } from "fresh/internal-dev";
4+
import * as path from "@std/path";
45

5-
export function clientSnapshot(options: ResolvedFreshViteConfig): Plugin {
6+
export function clientSnapshot(options: ResolvedFreshViteConfig): Plugin[] {
67
const modName = "fresh:client-snapshot";
78

89
const islands = new Set<string>();
910
let server: ViteDevServer | undefined;
1011
let isDev = false;
1112

12-
return {
13-
name: "fresh:client-snapshot",
14-
applyToEnvironment(env) {
15-
return env.name === "client";
16-
},
17-
config(_, env) {
18-
isDev = env.command === "serve";
19-
20-
return {
21-
environments: {
22-
client: {
23-
build: {
24-
rollupOptions: {
25-
input: {
26-
"client-snapshot": "fresh:client-snapshot",
13+
const entryToIsland = new Map<string, string>();
14+
15+
return [
16+
{
17+
name: "fresh:client-snapshot",
18+
applyToEnvironment(env) {
19+
return env.name === "client";
20+
},
21+
22+
async config(cfg, env) {
23+
isDev = env.command === "serve";
24+
25+
const cwd = Deno.cwd();
26+
27+
const result = await crawlFsItem({
28+
islandDir: pathWithRoot(options.islandsDir, cfg.root ?? cwd),
29+
routeDir: pathWithRoot(options.routeDir, cfg.root ?? cwd),
30+
ignore: options.ignore,
31+
});
32+
33+
const input: Record<string, string> = {};
34+
35+
if (isDev) {
36+
input["client-snapshot"] = "fresh:client-snapshot";
37+
}
38+
39+
for (let i = 0; i < result.islands.length; i++) {
40+
const filePath = result.islands[i];
41+
islands.add(filePath);
42+
43+
if (!isDev) {
44+
const specName = specToName(filePath);
45+
const name = options.namer.getUniqueName(specName);
46+
47+
entryToIsland.set(name, filePath);
48+
input[`fresh-island::${name}`] = `fresh-client-island::${name}`;
49+
}
50+
}
51+
52+
return {
53+
environments: {
54+
client: {
55+
build: {
56+
rollupOptions: {
57+
input,
2758
},
2859
},
2960
},
3061
},
31-
},
32-
};
33-
},
34-
configResolved() {
35-
options.islandSpecifiers.forEach((_name, spec) => {
36-
islands.add(spec);
37-
});
38-
},
39-
async buildStart() {
40-
const result = await crawlFsItem({
41-
islandDir: options.islandsDir,
42-
routeDir: options.routeDir,
43-
ignore: options.ignore,
44-
});
45-
46-
for (let i = 0; i < result.islands.length; i++) {
47-
const filePath = result.islands[i];
48-
islands.add(filePath);
49-
}
50-
},
51-
configureServer(devServer) {
52-
server = devServer;
53-
},
54-
resolveId: {
55-
filter: {
56-
id: /fresh:client-snapshot/,
62+
};
5763
},
58-
handler(id) {
59-
if (id === modName) {
60-
return `\0${modName}`;
64+
configResolved(cfg) {
65+
for (const [name, spec] of entryToIsland.entries()) {
66+
const full = pathWithRoot(spec, cfg.root);
67+
entryToIsland.set(name, full);
6168
}
6269
},
63-
},
64-
load: {
65-
filter: {
66-
id: /\0fresh:client-snapshot/,
70+
options(opts) {
71+
options.islandSpecifiers.forEach((_name, spec) => {
72+
islands.add(spec);
73+
74+
if (!isDev) {
75+
const specName = specToName(spec);
76+
const name = options.namer.getUniqueName(specName);
77+
entryToIsland.set(name, spec);
78+
79+
// deno-lint-ignore no-explicit-any
80+
(opts.input as any)[`fresh-island::${name}`] =
81+
`fresh-client-island::${name}`;
82+
}
83+
});
6784
},
68-
handler() {
69-
const imports = Array.from(islands.keys()).map((file, i) => {
70-
return `export const mod_${i} = await import(${
71-
JSON.stringify(file)
72-
});`;
73-
}).join("\n");
74-
75-
if (isDev && server !== undefined) {
76-
const mod = server.environments.ssr.moduleGraph.getModuleById(
77-
"\0fresh:server-snapshot",
78-
);
79-
if (mod) {
80-
server.environments.ssr.moduleGraph.invalidateModule(mod);
85+
configureServer(devServer) {
86+
server = devServer;
87+
88+
server.watcher.on("add", (filePath) => {
89+
if (!isIslandPath(options, filePath)) return;
90+
91+
islands.add(filePath);
92+
93+
invalidateSnapshots(server!);
94+
});
95+
server.watcher.on("unlink", (filePath) => {
96+
if (!isIslandPath(options, filePath)) return;
97+
98+
islands.delete(filePath);
99+
100+
invalidateSnapshots(server!);
101+
});
102+
},
103+
resolveId: {
104+
filter: {
105+
id: /fresh:client-snapshot/,
106+
},
107+
handler(id) {
108+
if (id === modName) {
109+
return `\0${modName}`;
110+
}
111+
},
112+
},
113+
load: {
114+
filter: {
115+
id: /\0fresh:client-snapshot/,
116+
},
117+
handler() {
118+
const imports = Array.from(islands.keys()).map((file, i) => {
119+
return `export const mod_${i} = await import(${
120+
JSON.stringify(file)
121+
});`;
122+
}).join("\n");
123+
124+
if (isDev && server !== undefined) {
125+
const mod = server.environments.ssr.moduleGraph.getModuleById(
126+
"\0fresh:server-snapshot",
127+
);
128+
if (mod) {
129+
server.environments.ssr.moduleGraph.invalidateModule(mod);
130+
}
81131
}
82-
}
83132

84-
return `${imports}
133+
return `${imports}
85134
if (import.meta.hot) {
86135
import.meta.hot.accept(() => {
87136
console.log("accepting client-snapshot")
88137
});
89138
}
90139
`;
140+
},
141+
},
142+
},
143+
{
144+
name: "fresh:client-island",
145+
resolveId: {
146+
filter: {
147+
id: /^fresh-client-island::/,
148+
},
149+
handler(id) {
150+
const name = id.slice("fresh-client-island::".length);
151+
const full = entryToIsland.get(name);
152+
return full;
153+
},
91154
},
92155
},
93-
};
156+
];
157+
}
158+
159+
function isIslandPath(
160+
options: ResolvedFreshViteConfig,
161+
filePath: string,
162+
): boolean {
163+
const relIsland = path.relative(options.islandsDir, filePath);
164+
if (!relIsland.startsWith("..")) return true;
165+
166+
const relRoutes = path.relative(options.routeDir, filePath);
167+
168+
if (!relIsland.startsWith("..") && relRoutes.includes("(_islands)")) {
169+
return true;
170+
}
171+
return false;
172+
}
173+
174+
function invalidateSnapshots(server: ViteDevServer) {
175+
const client = server.environments.client.moduleGraph.getModuleById(
176+
"\0fresh:client-snapshot",
177+
);
178+
if (client !== undefined) {
179+
server.environments.client.moduleGraph.invalidateModule(client);
180+
}
181+
182+
const ssr = server.environments.ssr.moduleGraph.getModuleById(
183+
"\0fresh:server-snapshot",
184+
);
185+
if (ssr !== undefined) {
186+
server.environments.ssr.moduleGraph.invalidateModule(ssr);
187+
}
94188
}

packages/plugin-vite/src/plugins/server_snapshot.ts

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -240,42 +240,36 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] {
240240
}
241241
}
242242

243-
const namer = new UniqueNamer();
244-
if (chunk.name === "client-snapshot") {
245-
for (const id of chunk.dynamicImports ?? []) {
246-
const mod = manifest[id];
247-
248-
let serverPath = path.join(root, mod.src ?? id);
249-
const idx = mod.src?.indexOf("deno::") ?? -1;
250-
251-
if (idx > -1 && mod.src) {
252-
const src = mod.src
253-
.slice(idx)
254-
.replace(
255-
/(https?):\/([^/])/,
256-
(_m, protocol, rest) => {
257-
return `${protocol}://${rest}`;
258-
},
259-
);
260-
serverPath = resolvedIslandSpecs.get(src)!;
261-
}
262-
263-
let spec = pathToSpec(clientOutDir, mod.file);
264-
265-
if (spec.startsWith("./")) {
266-
spec = spec.slice(1);
267-
}
243+
if (chunk.name?.startsWith("fresh-island__")) {
244+
const name = chunk.name.slice("fresh-island__".length);
245+
let serverPath = path.join(root, chunk.src ?? chunk.file);
246+
const idx = chunk.src?.indexOf("deno::") ?? -1;
247+
248+
if (idx > -1 && chunk.src) {
249+
const src = chunk.src
250+
.slice(idx)
251+
.replace(
252+
/(https?):\/([^/])/,
253+
(_m, protocol, rest) => {
254+
return `${protocol}://${rest}`;
255+
},
256+
);
257+
serverPath = resolvedIslandSpecs.get(src)!;
258+
}
268259

269-
const chunkCss = mod.css?.map((id) => `/${id}`) ?? [];
260+
let spec = pathToSpec(clientOutDir, chunk.file);
270261

271-
const name = namer.getUniqueName(specToName(id));
272-
islandMods.push({
273-
name,
274-
browser: spec,
275-
server: serverPath,
276-
css: chunkCss,
277-
});
262+
if (spec.startsWith("./")) {
263+
spec = spec.slice(1);
278264
}
265+
266+
const chunkCss = chunk.css?.map((id) => `/${id}`) ?? [];
267+
islandMods.push({
268+
name,
269+
browser: spec,
270+
server: serverPath,
271+
css: chunkCss,
272+
});
279273
}
280274
}
281275

0 commit comments

Comments
 (0)