@void/svelte 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/dist/plugin.d.mts +16 -0
- package/dist/plugin.mjs +681 -0
- package/package.json +54 -0
- package/src/runtime/App.svelte +81 -0
- package/src/runtime/Link.svelte +316 -0
- package/src/runtime/action.ts +50 -0
- package/src/runtime/index.ts +8 -0
- package/src/runtime/prefetch.ts +2 -0
- package/src/runtime/use-form.svelte.ts +205 -0
- package/src/runtime/use-island-form.svelte.ts +175 -0
- package/src/runtime/use-navigation.ts +22 -0
- package/src/runtime/use-router.ts +9 -0
- package/src/runtime/use-shared.ts +10 -0
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { normalizePath } from "vite";
|
|
4
|
+
import { generateComponentManifest, resolveEffectiveLayoutChain, resolveStaticPageDataBuildOptions, scanPages } from "void/pages";
|
|
5
|
+
import { ISLANDS_CLIENT_ENTRY_ID, ISLANDS_RENDERER_ID, computePagesNeedingClientJS, configureIslandCssServer, extractCssImports, fileHasCssImports, needsIslandClientBundle, rebuildIslandCssMap, resolveSpecifier, rewriteCssImportForVirtualModule, watchPagesForRescan } from "void/pages-islands-plugin";
|
|
6
|
+
//#region ../../node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
|
|
7
|
+
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
|
|
8
|
+
function normalizeWindowsPath(input = "") {
|
|
9
|
+
if (!input) return input;
|
|
10
|
+
return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
const _UNC_REGEX = /^[/\\]{2}/;
|
|
13
|
+
const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
|
|
14
|
+
const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
|
|
15
|
+
const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
|
|
16
|
+
const normalize = function(path) {
|
|
17
|
+
if (path.length === 0) return ".";
|
|
18
|
+
path = normalizeWindowsPath(path);
|
|
19
|
+
const isUNCPath = path.match(_UNC_REGEX);
|
|
20
|
+
const isPathAbsolute = isAbsolute(path);
|
|
21
|
+
const trailingSeparator = path[path.length - 1] === "/";
|
|
22
|
+
path = normalizeString(path, !isPathAbsolute);
|
|
23
|
+
if (path.length === 0) {
|
|
24
|
+
if (isPathAbsolute) return "/";
|
|
25
|
+
return trailingSeparator ? "./" : ".";
|
|
26
|
+
}
|
|
27
|
+
if (trailingSeparator) path += "/";
|
|
28
|
+
if (_DRIVE_LETTER_RE.test(path)) path += "/";
|
|
29
|
+
if (isUNCPath) {
|
|
30
|
+
if (!isPathAbsolute) return `//./${path}`;
|
|
31
|
+
return `//${path}`;
|
|
32
|
+
}
|
|
33
|
+
return isPathAbsolute && !isAbsolute(path) ? `/${path}` : path;
|
|
34
|
+
};
|
|
35
|
+
const join = function(...segments) {
|
|
36
|
+
let path = "";
|
|
37
|
+
for (const seg of segments) {
|
|
38
|
+
if (!seg) continue;
|
|
39
|
+
if (path.length > 0) {
|
|
40
|
+
const pathTrailing = path[path.length - 1] === "/";
|
|
41
|
+
const segLeading = seg[0] === "/";
|
|
42
|
+
if (pathTrailing && segLeading) path += seg.slice(1);
|
|
43
|
+
else path += pathTrailing || segLeading ? seg : `/${seg}`;
|
|
44
|
+
} else path += seg;
|
|
45
|
+
}
|
|
46
|
+
return normalize(path);
|
|
47
|
+
};
|
|
48
|
+
function cwd() {
|
|
49
|
+
if (typeof process !== "undefined" && typeof process.cwd === "function") return process.cwd().replace(/\\/g, "/");
|
|
50
|
+
return "/";
|
|
51
|
+
}
|
|
52
|
+
const resolve = function(...arguments_) {
|
|
53
|
+
arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
|
|
54
|
+
let resolvedPath = "";
|
|
55
|
+
let resolvedAbsolute = false;
|
|
56
|
+
for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
|
|
57
|
+
const path = index >= 0 ? arguments_[index] : cwd();
|
|
58
|
+
if (!path || path.length === 0) continue;
|
|
59
|
+
resolvedPath = `${path}/${resolvedPath}`;
|
|
60
|
+
resolvedAbsolute = isAbsolute(path);
|
|
61
|
+
}
|
|
62
|
+
resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
|
|
63
|
+
if (resolvedAbsolute && !isAbsolute(resolvedPath)) return `/${resolvedPath}`;
|
|
64
|
+
return resolvedPath.length > 0 ? resolvedPath : ".";
|
|
65
|
+
};
|
|
66
|
+
function normalizeString(path, allowAboveRoot) {
|
|
67
|
+
let res = "";
|
|
68
|
+
let lastSegmentLength = 0;
|
|
69
|
+
let lastSlash = -1;
|
|
70
|
+
let dots = 0;
|
|
71
|
+
let char = null;
|
|
72
|
+
for (let index = 0; index <= path.length; ++index) {
|
|
73
|
+
if (index < path.length) char = path[index];
|
|
74
|
+
else if (char === "/") break;
|
|
75
|
+
else char = "/";
|
|
76
|
+
if (char === "/") {
|
|
77
|
+
if (lastSlash === index - 1 || dots === 1);
|
|
78
|
+
else if (dots === 2) {
|
|
79
|
+
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
|
|
80
|
+
if (res.length > 2) {
|
|
81
|
+
const lastSlashIndex = res.lastIndexOf("/");
|
|
82
|
+
if (lastSlashIndex === -1) {
|
|
83
|
+
res = "";
|
|
84
|
+
lastSegmentLength = 0;
|
|
85
|
+
} else {
|
|
86
|
+
res = res.slice(0, lastSlashIndex);
|
|
87
|
+
lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
|
88
|
+
}
|
|
89
|
+
lastSlash = index;
|
|
90
|
+
dots = 0;
|
|
91
|
+
continue;
|
|
92
|
+
} else if (res.length > 0) {
|
|
93
|
+
res = "";
|
|
94
|
+
lastSegmentLength = 0;
|
|
95
|
+
lastSlash = index;
|
|
96
|
+
dots = 0;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (allowAboveRoot) {
|
|
101
|
+
res += res.length > 0 ? "/.." : "..";
|
|
102
|
+
lastSegmentLength = 2;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
if (res.length > 0) res += `/${path.slice(lastSlash + 1, index)}`;
|
|
106
|
+
else res = path.slice(lastSlash + 1, index);
|
|
107
|
+
lastSegmentLength = index - lastSlash - 1;
|
|
108
|
+
}
|
|
109
|
+
lastSlash = index;
|
|
110
|
+
dots = 0;
|
|
111
|
+
} else if (char === "." && dots !== -1) ++dots;
|
|
112
|
+
else dots = -1;
|
|
113
|
+
}
|
|
114
|
+
return res;
|
|
115
|
+
}
|
|
116
|
+
const isAbsolute = function(p) {
|
|
117
|
+
return _IS_ABSOLUTE_RE.test(p);
|
|
118
|
+
};
|
|
119
|
+
const relative = function(from, to) {
|
|
120
|
+
const _from = resolve(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
121
|
+
const _to = resolve(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
122
|
+
if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) return _to.join("/");
|
|
123
|
+
const _fromCopy = [..._from];
|
|
124
|
+
for (const segment of _fromCopy) {
|
|
125
|
+
if (_to[0] !== segment) break;
|
|
126
|
+
_from.shift();
|
|
127
|
+
_to.shift();
|
|
128
|
+
}
|
|
129
|
+
return [..._from.map(() => ".."), ..._to].join("/");
|
|
130
|
+
};
|
|
131
|
+
const dirname = function(p) {
|
|
132
|
+
const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
|
|
133
|
+
if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) segments[0] += "/";
|
|
134
|
+
return segments.join("/") || (isAbsolute(p) ? "/" : ".");
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/plugin-islands.ts
|
|
138
|
+
const COMPONENT_EXTENSIONS = [
|
|
139
|
+
".svelte",
|
|
140
|
+
".ts",
|
|
141
|
+
".js"
|
|
142
|
+
];
|
|
143
|
+
/**
|
|
144
|
+
* Prefix for virtual island wrapper modules. Must NOT use the `\0` prefix
|
|
145
|
+
* because `@sveltejs/vite-plugin-svelte` skips `\0`-prefixed modules in
|
|
146
|
+
* its transform filter.
|
|
147
|
+
*/
|
|
148
|
+
const WRAPPER_PREFIX = "/@void-island-wrap/";
|
|
149
|
+
function islandsPlugin(pagesDir) {
|
|
150
|
+
let pageScan = null;
|
|
151
|
+
const root = dirname(pagesDir);
|
|
152
|
+
const islandManifest = /* @__PURE__ */ new Map();
|
|
153
|
+
const islandCssFiles = /* @__PURE__ */ new Set();
|
|
154
|
+
let isDev = false;
|
|
155
|
+
let viteRoot = "";
|
|
156
|
+
const islandCssMap = /* @__PURE__ */ new Map();
|
|
157
|
+
let resolvedConfig = null;
|
|
158
|
+
/** Rebuild islandCssMap from current file contents (re-runs fileHasCssImports). */
|
|
159
|
+
function rebuildIslandCssMap$1() {
|
|
160
|
+
if (!pageScan) return;
|
|
161
|
+
rebuildIslandCssMap(pageScan, pagesDir, islandCssMap, fileHasCssImports);
|
|
162
|
+
}
|
|
163
|
+
function computePagesNeedingClientJS$1() {
|
|
164
|
+
if (!pageScan) return /* @__PURE__ */ new Set();
|
|
165
|
+
return computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
|
|
166
|
+
}
|
|
167
|
+
return [{
|
|
168
|
+
name: "void-svelte:islands",
|
|
169
|
+
enforce: "pre",
|
|
170
|
+
configResolved(config) {
|
|
171
|
+
resolvedConfig = config;
|
|
172
|
+
viteRoot = config.root;
|
|
173
|
+
isDev = config.command === "serve";
|
|
174
|
+
},
|
|
175
|
+
configureServer(server) {
|
|
176
|
+
configureIslandCssServer(server, islandCssFiles, viteRoot, rebuildIslandCssMap$1);
|
|
177
|
+
watchPagesForRescan(server, pagesDir, root, (scan) => {
|
|
178
|
+
pageScan = scan;
|
|
179
|
+
rebuildIslandCssMap$1();
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
async buildStart() {
|
|
183
|
+
if (existsSync(pagesDir)) {
|
|
184
|
+
pageScan = await scanPages(root);
|
|
185
|
+
const importAttrRe = /import\s+\w+\s+from\s+("[^"]+"|'[^']+')\s+with\s*\{\s*island\s*:\s*("[^"]+"|'[^']+')\s*\}/g;
|
|
186
|
+
const filesToScan = [
|
|
187
|
+
...pageScan.pages.map((p) => ({
|
|
188
|
+
filePath: p.componentPath,
|
|
189
|
+
island: p.island
|
|
190
|
+
})),
|
|
191
|
+
...pageScan.layouts.map((l) => ({
|
|
192
|
+
filePath: l.filePath,
|
|
193
|
+
island: l.island
|
|
194
|
+
})),
|
|
195
|
+
...pageScan.namedLayouts.map((nl) => ({
|
|
196
|
+
filePath: nl.filePath,
|
|
197
|
+
island: nl.island
|
|
198
|
+
}))
|
|
199
|
+
];
|
|
200
|
+
for (const entry of filesToScan) {
|
|
201
|
+
if (!entry.island) continue;
|
|
202
|
+
const filePath = resolve(pagesDir, entry.filePath);
|
|
203
|
+
const code = readFileSync(filePath, "utf-8");
|
|
204
|
+
const importerDir = dirname(filePath);
|
|
205
|
+
importAttrRe.lastIndex = 0;
|
|
206
|
+
let m;
|
|
207
|
+
while ((m = importAttrRe.exec(code)) !== null) {
|
|
208
|
+
const spec = m[1].slice(1, -1);
|
|
209
|
+
const strategy = m[2].slice(1, -1);
|
|
210
|
+
const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
|
|
211
|
+
if (absPath) {
|
|
212
|
+
const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
|
|
213
|
+
islandManifest.set(islandId, {
|
|
214
|
+
componentPath: absPath,
|
|
215
|
+
islandId,
|
|
216
|
+
hydrate: strategy
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const page of pageScan.pages) {
|
|
222
|
+
if (!page.island) continue;
|
|
223
|
+
if (page.componentPath.endsWith(".svelte")) islandCssFiles.add(normalizePath(resolve(pagesDir, page.componentPath)));
|
|
224
|
+
const layoutChain = resolveEffectiveLayoutChain(page, pageScan.layouts, pageScan.namedLayouts);
|
|
225
|
+
for (const layout of layoutChain) islandCssFiles.add(normalizePath(resolve(pagesDir, layout.filePath)));
|
|
226
|
+
}
|
|
227
|
+
rebuildIslandCssMap$1();
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
resolveId: {
|
|
231
|
+
filter: { id: /^(virtual:void-islands-(renderer|client)|\/@void-island-wrap\/)|\.svelte\.css$/ },
|
|
232
|
+
handler(source) {
|
|
233
|
+
if (source === ISLANDS_RENDERER_ID || source === ISLANDS_CLIENT_ENTRY_ID) return "\0" + source;
|
|
234
|
+
if (source.startsWith(WRAPPER_PREFIX)) return source;
|
|
235
|
+
if (source.endsWith(".css")) {
|
|
236
|
+
const srcPath = normalizePath(source.slice(0, -4));
|
|
237
|
+
if (islandCssFiles.has(srcPath)) return srcPath + ".css";
|
|
238
|
+
if (srcPath.startsWith("/")) {
|
|
239
|
+
const absPath = normalizePath(resolve(viteRoot, srcPath.slice(1)));
|
|
240
|
+
if (islandCssFiles.has(absPath)) return absPath + ".css";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
load: {
|
|
246
|
+
filter: { id: /^(\0virtual:void-islands-(renderer|client)|\/@void-island-wrap\/)|\.svelte\.css$/ },
|
|
247
|
+
handler(id) {
|
|
248
|
+
if (id === "\0" + ISLANDS_RENDERER_ID) {
|
|
249
|
+
if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
|
|
250
|
+
const mdPlugin = resolvedConfig?.plugins?.find((p) => p.name === "void-md");
|
|
251
|
+
const mdScriptIds = new Set(mdPlugin?.api?.getClientScripts?.()?.keys() ?? []);
|
|
252
|
+
const needsJS = computePagesNeedingClientJS$1();
|
|
253
|
+
return generateIslandRenderer(pageScan, pagesDir, isDev, islandCssMap, viteRoot, mdScriptIds, needsJS);
|
|
254
|
+
}
|
|
255
|
+
if (id === "\0" + ISLANDS_CLIENT_ENTRY_ID) {
|
|
256
|
+
if (!pageScan) return "";
|
|
257
|
+
return generateIslandClientEntry(islandManifest, islandCssFiles, isDev);
|
|
258
|
+
}
|
|
259
|
+
if (id.startsWith(WRAPPER_PREFIX)) return generateWrapperComponent(id);
|
|
260
|
+
if (id.endsWith(".css") && !id.startsWith("\0")) {
|
|
261
|
+
const srcPath = id.slice(0, -4);
|
|
262
|
+
if (islandCssFiles.has(srcPath)) return extractCssImports(srcPath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
transform: {
|
|
267
|
+
filter: { id: /\.island\.svelte$/ },
|
|
268
|
+
handler(code, id) {
|
|
269
|
+
if (/import\s+\{[^}]*\buseForm\b/.test(code)) this.warn(`islands: useForm cannot be imported in island page '${id}'. Use useIslandForm instead.`);
|
|
270
|
+
const importAttrRe = /import\s+(\w+)\s+from\s+("[^"]+"|'[^']+')\s+with\s*\{\s*island\s*:\s*("[^"]+"|'[^']+')\s*\}/g;
|
|
271
|
+
const importerDir = dirname(id);
|
|
272
|
+
let transformed = code;
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = importAttrRe.exec(code)) !== null) {
|
|
275
|
+
const name = match[1];
|
|
276
|
+
const specRaw = match[2];
|
|
277
|
+
const strategyRaw = match[3];
|
|
278
|
+
const spec = specRaw.slice(1, -1);
|
|
279
|
+
const strategy = strategyRaw.slice(1, -1);
|
|
280
|
+
const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
|
|
281
|
+
if (absPath) {
|
|
282
|
+
const islandId = normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "");
|
|
283
|
+
islandManifest.set(islandId, {
|
|
284
|
+
componentPath: absPath,
|
|
285
|
+
islandId,
|
|
286
|
+
hydrate: strategy
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const islandId = absPath ? normalizePath(relative(root, absPath)).replace(/\.[^.]+$/, "") : spec;
|
|
290
|
+
const wrapperUrl = WRAPPER_PREFIX + encodeURIComponent(islandId) + ".svelte?src=" + encodeURIComponent(absPath || spec) + "&island=" + encodeURIComponent(islandId) + "&hydrate=" + encodeURIComponent(strategy);
|
|
291
|
+
const replacement = `import ${name} from ${JSON.stringify(wrapperUrl)}`;
|
|
292
|
+
transformed = transformed.replace(match[0], replacement);
|
|
293
|
+
}
|
|
294
|
+
if (transformed !== code) return {
|
|
295
|
+
code: transformed,
|
|
296
|
+
map: null
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
config(_, env) {
|
|
301
|
+
if (!existsSync(pagesDir)) return;
|
|
302
|
+
if (env.command === "serve") return { optimizeDeps: { include: ["svelte"] } };
|
|
303
|
+
if (needsIslandClientBundle(pagesDir, /\.island\.svelte$/, true)) return { environments: { client: { build: {
|
|
304
|
+
manifest: true,
|
|
305
|
+
rollupOptions: { input: { "islands-client": ISLANDS_CLIENT_ENTRY_ID } }
|
|
306
|
+
} } } };
|
|
307
|
+
}
|
|
308
|
+
}, {
|
|
309
|
+
name: "void-svelte:islands-style",
|
|
310
|
+
resolveId: {
|
|
311
|
+
filter: { id: /\?island-style&/ },
|
|
312
|
+
handler(source) {
|
|
313
|
+
return source;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
load: {
|
|
317
|
+
filter: { id: /\?island-style&/ },
|
|
318
|
+
handler(id) {
|
|
319
|
+
const filePath = id.replace(/\?island-style&.*$/, "");
|
|
320
|
+
const params = new URLSearchParams(id.slice(id.indexOf("?island-style&") + 1));
|
|
321
|
+
const index = parseInt(params.get("index") || "0", 10);
|
|
322
|
+
const content = readFileSync(filePath, "utf-8");
|
|
323
|
+
const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/g;
|
|
324
|
+
let match;
|
|
325
|
+
let i = 0;
|
|
326
|
+
while ((match = styleBlockRe.exec(content)) !== null) {
|
|
327
|
+
if (i === index) return match[1];
|
|
328
|
+
i++;
|
|
329
|
+
}
|
|
330
|
+
return "/* style block not found */";
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}];
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Generate a virtual Svelte wrapper component from a wrapper URL.
|
|
337
|
+
* The URL encodes: src (absolute component path), island (ID), hydrate (strategy).
|
|
338
|
+
*/
|
|
339
|
+
function generateWrapperComponent(id) {
|
|
340
|
+
const url = new URL(id, "file://");
|
|
341
|
+
const src = decodeURIComponent(url.searchParams.get("src") || "");
|
|
342
|
+
const islandId = decodeURIComponent(url.searchParams.get("island") || "");
|
|
343
|
+
const hydrate = decodeURIComponent(url.searchParams.get("hydrate") || "load");
|
|
344
|
+
return `<script>
|
|
345
|
+
import Cmp from ${JSON.stringify(src)};
|
|
346
|
+
let props = $props();
|
|
347
|
+
<\/script>
|
|
348
|
+
<div data-island="${islandId}" data-props={JSON.stringify(props)} data-hydrate="${hydrate}">
|
|
349
|
+
<Cmp {...props} />
|
|
350
|
+
</div>
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
function generateIslandRenderer(scan, pagesDir, isDev, islandCssMap, viteRoot, mdScriptIds, pagesNeedingClientJS) {
|
|
354
|
+
let cssMapCode = "";
|
|
355
|
+
if (isDev && islandCssMap.size > 0) {
|
|
356
|
+
const cssMapObj = {};
|
|
357
|
+
for (const [compId, paths] of islandCssMap) cssMapObj[compId] = paths.map((p) => "/" + normalizePath(relative(viteRoot, p)) + ".css");
|
|
358
|
+
cssMapCode = `const __cssPaths = ${JSON.stringify(cssMapObj)};`;
|
|
359
|
+
}
|
|
360
|
+
const mdScriptIdsCode = `const __mdClientScriptIds = new Set(${JSON.stringify([...mdScriptIds])});`;
|
|
361
|
+
const needsJSCode = `const __pagesNeedingClientJS = new Set(${JSON.stringify([...pagesNeedingClientJS])});`;
|
|
362
|
+
return `
|
|
363
|
+
import { render } from "svelte/server";
|
|
364
|
+
import App from "@void/svelte/App.svelte";
|
|
365
|
+
import { renderHeadToString, renderHtmlAttrs, renderBodyAttrs } from "void/pages-head";
|
|
366
|
+
|
|
367
|
+
${generateComponentManifest(scan, pagesDir)}
|
|
368
|
+
${cssMapCode}
|
|
369
|
+
${mdScriptIdsCode}
|
|
370
|
+
${needsJSCode}
|
|
371
|
+
|
|
372
|
+
function escapeAttr(s) {
|
|
373
|
+
return String(s).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function renderIslandPage(pageObj, assetTags) {
|
|
377
|
+
const PageComponent = (await components[pageObj.component]()).default;
|
|
378
|
+
const layoutIds = layoutTree[pageObj.component] || [];
|
|
379
|
+
const layoutComponents = await Promise.all(
|
|
380
|
+
layoutIds.map(async (id) => (await components[id]()).default)
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const { html, head } = render(App, {
|
|
384
|
+
props: {
|
|
385
|
+
page: PageComponent,
|
|
386
|
+
layouts: layoutComponents,
|
|
387
|
+
pageProps: pageObj.props,
|
|
388
|
+
shared: pageObj.shared || {},
|
|
389
|
+
errors: pageObj.errors || {},
|
|
390
|
+
router: null,
|
|
391
|
+
stores: null,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const __mdScriptAttr = __mdClientScriptIds.has(pageObj.component) ? \` data-md-script="\${escapeAttr(pageObj.component)}"\` : "";
|
|
396
|
+
const headHtml = pageObj.head ? renderHeadToString(pageObj.head) : "";
|
|
397
|
+
const htmlAttrs = pageObj.head ? renderHtmlAttrs(pageObj.head) : "";
|
|
398
|
+
const bodyAttrs = pageObj.head ? renderBodyAttrs(pageObj.head) : "";
|
|
399
|
+
|
|
400
|
+
return \`<!doctype html>
|
|
401
|
+
<html\${htmlAttrs}>
|
|
402
|
+
<head>\${headHtml}\${typeof __cssPaths !== "undefined" ? (__cssPaths[pageObj.component] || []).map(url => '<link rel="stylesheet" href="' + url + '">').join("\\n") : ""}\${head}\${assetTags.css}\${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.preloads : ""}</head>
|
|
403
|
+
<body\${bodyAttrs}>
|
|
404
|
+
<div id="app"\${__mdScriptAttr}>\${html}</div>
|
|
405
|
+
\${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.body : ""}
|
|
406
|
+
</body>
|
|
407
|
+
</html>\`;
|
|
408
|
+
}
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
411
|
+
function generateIslandClientEntry(manifest, cssFiles, isDev) {
|
|
412
|
+
const entries = [...manifest.values()];
|
|
413
|
+
const uniqueIslands = /* @__PURE__ */ new Map();
|
|
414
|
+
for (const entry of entries) uniqueIslands.set(entry.islandId, entry.componentPath);
|
|
415
|
+
const lines = [];
|
|
416
|
+
lines.push("import { initIslands } from \"void/pages-client\";");
|
|
417
|
+
if (!isDev) for (const file of cssFiles) {
|
|
418
|
+
const content = readFileSync(file, "utf-8");
|
|
419
|
+
const styleRe = /<style\b([^>]*)>/g;
|
|
420
|
+
styleRe.lastIndex = 0;
|
|
421
|
+
let styleIndex = 0;
|
|
422
|
+
let styleMatch;
|
|
423
|
+
while ((styleMatch = styleRe.exec(content)) !== null) {
|
|
424
|
+
const langMatch = styleMatch[1].match(/\blang\s*=\s*["'](\w+)["']/);
|
|
425
|
+
const lang = langMatch ? langMatch[1] : "css";
|
|
426
|
+
lines.push(`import ${JSON.stringify(file + "?island-style&index=" + styleIndex + "&lang." + lang)};`);
|
|
427
|
+
styleIndex++;
|
|
428
|
+
}
|
|
429
|
+
const scriptRe = /<script\b[^>]*>([\s\S]*?)<\/script>/g;
|
|
430
|
+
const cssImportRe = /^\s*import\b[^"']*["']([^"']+\.(?:css|scss|sass|less|styl|stylus|pcss|postcss))["']/gm;
|
|
431
|
+
let scriptMatch;
|
|
432
|
+
while ((scriptMatch = scriptRe.exec(content)) !== null) {
|
|
433
|
+
const scriptContent = scriptMatch[1];
|
|
434
|
+
let cssMatch;
|
|
435
|
+
cssImportRe.lastIndex = 0;
|
|
436
|
+
while ((cssMatch = cssImportRe.exec(scriptContent)) !== null) lines.push(rewriteCssImportForVirtualModule(cssMatch[0], cssMatch[1], file));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
lines.push("const islands = {");
|
|
440
|
+
for (const [id, path] of uniqueIslands) lines.push(` ${JSON.stringify(id)}: () => import(${JSON.stringify(path)}),`);
|
|
441
|
+
lines.push("};");
|
|
442
|
+
lines.push(`
|
|
443
|
+
async function hydrateIsland(el, mod, props) {
|
|
444
|
+
const { hydrate } = await import("svelte");
|
|
445
|
+
const Component = mod.default || mod;
|
|
446
|
+
hydrate(Component, { target: el, props });
|
|
447
|
+
el.setAttribute("data-hydrated", "true");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
initIslands(islands, hydrateIsland);
|
|
451
|
+
`);
|
|
452
|
+
return lines.join("\n");
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/plugin-pages.ts
|
|
456
|
+
const RENDERER_ID = "virtual:void-pages-renderer";
|
|
457
|
+
const CLIENT_ENTRY_ID = "virtual:void-pages-client";
|
|
458
|
+
/** Check if the pages directory has any non-island Svelte page files. */
|
|
459
|
+
function hasNonIslandPages(dir) {
|
|
460
|
+
try {
|
|
461
|
+
for (const entry of readdirSync(dir, {
|
|
462
|
+
withFileTypes: true,
|
|
463
|
+
recursive: true
|
|
464
|
+
})) {
|
|
465
|
+
if (!entry.isFile()) continue;
|
|
466
|
+
const name = entry.name;
|
|
467
|
+
if (name.startsWith("_") || name.startsWith("layout.")) continue;
|
|
468
|
+
if (name.endsWith(".svelte") && !name.includes(".island.")) return true;
|
|
469
|
+
}
|
|
470
|
+
} catch {}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
function pagesPlugin(pagesDir, options) {
|
|
474
|
+
let pageScan = null;
|
|
475
|
+
let isDev = false;
|
|
476
|
+
let viteConfig;
|
|
477
|
+
const root = dirname(pagesDir);
|
|
478
|
+
return {
|
|
479
|
+
name: "void-svelte:pages",
|
|
480
|
+
async buildStart() {
|
|
481
|
+
if (existsSync(pagesDir)) pageScan = await scanPages(root);
|
|
482
|
+
},
|
|
483
|
+
resolveId: {
|
|
484
|
+
filter: { id: /^virtual:void-pages-(renderer|client)$/ },
|
|
485
|
+
handler(id) {
|
|
486
|
+
return "\0" + id;
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
load: {
|
|
490
|
+
filter: { id: /^\0virtual:void-pages-(renderer|client)$/ },
|
|
491
|
+
handler(id) {
|
|
492
|
+
if (id === "\0" + RENDERER_ID) {
|
|
493
|
+
if (!pageScan) return "export function renderPage() { throw new Error('No pages found'); }";
|
|
494
|
+
return generateRendererModule(pageScan, pagesDir, isDev);
|
|
495
|
+
}
|
|
496
|
+
if (id === "\0" + CLIENT_ENTRY_ID) {
|
|
497
|
+
if (!pageScan) return "";
|
|
498
|
+
return generateClientEntry(pageScan, pagesDir, options?.viewTransitions, options?.prefetch, resolveStaticPageDataBuildOptions(root, { publicDir: viteConfig?.publicDir }));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
configResolved(config) {
|
|
503
|
+
viteConfig = config;
|
|
504
|
+
},
|
|
505
|
+
config(_, env) {
|
|
506
|
+
if (!existsSync(pagesDir)) return;
|
|
507
|
+
if (env.command === "serve") {
|
|
508
|
+
isDev = true;
|
|
509
|
+
return { optimizeDeps: {
|
|
510
|
+
exclude: ["@void/svelte"],
|
|
511
|
+
include: [
|
|
512
|
+
"svelte",
|
|
513
|
+
"svelte/store",
|
|
514
|
+
"svelte/internal",
|
|
515
|
+
"svelte/internal/client"
|
|
516
|
+
]
|
|
517
|
+
} };
|
|
518
|
+
}
|
|
519
|
+
if (!hasNonIslandPages(pagesDir)) return;
|
|
520
|
+
return { build: {
|
|
521
|
+
manifest: true,
|
|
522
|
+
rollupOptions: { input: { "pages-client": CLIENT_ENTRY_ID } }
|
|
523
|
+
} };
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function generateRendererModule(scan, pagesDir, _isDev) {
|
|
528
|
+
return `
|
|
529
|
+
import { render } from "svelte/server";
|
|
530
|
+
import App from "@void/svelte/App.svelte";
|
|
531
|
+
import { createSsrRouter, idleNavigationState } from "void/pages-client";
|
|
532
|
+
import { renderHeadToString, renderHtmlAttrs, renderBodyAttrs } from "void/pages-head";
|
|
533
|
+
import { serializePageData } from "void/pages-server";
|
|
534
|
+
|
|
535
|
+
${generateComponentManifest(scan, pagesDir)}
|
|
536
|
+
|
|
537
|
+
export async function renderPage(pageObj, assetTags) {
|
|
538
|
+
const PageComponent = (await components[pageObj.component]()).default;
|
|
539
|
+
const layoutIds = layoutTree[pageObj.component] || [];
|
|
540
|
+
const layoutComponents = await Promise.all(
|
|
541
|
+
layoutIds.map(async (id) => (await components[id]()).default)
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
const { html, head } = render(App, {
|
|
545
|
+
props: {
|
|
546
|
+
page: PageComponent,
|
|
547
|
+
layouts: layoutComponents,
|
|
548
|
+
pageProps: pageObj.props,
|
|
549
|
+
shared: pageObj.shared || {},
|
|
550
|
+
errors: pageObj.errors || {},
|
|
551
|
+
router: createSsrRouter(pageObj.url || "/"),
|
|
552
|
+
routeUrl: pageObj.url || "/",
|
|
553
|
+
navigation: idleNavigationState(),
|
|
554
|
+
stores: null,
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const pageData = serializePageData(pageObj);
|
|
559
|
+
const headHtml = pageObj.head ? renderHeadToString(pageObj.head) : "";
|
|
560
|
+
const htmlAttrs = pageObj.head ? renderHtmlAttrs(pageObj.head) : "";
|
|
561
|
+
const bodyAttrs = pageObj.head ? renderBodyAttrs(pageObj.head) : "";
|
|
562
|
+
|
|
563
|
+
return \`<!doctype html>
|
|
564
|
+
<html\${htmlAttrs}>
|
|
565
|
+
<head>\${headHtml}\${head}\${assetTags.css}\${assetTags.preloads}</head>
|
|
566
|
+
<body\${bodyAttrs}>
|
|
567
|
+
<script id="__VOID_PAGE_DATA__" type="application/json">\${pageData}<\/script>
|
|
568
|
+
<div id="app">\${html}</div>
|
|
569
|
+
\${assetTags.body}
|
|
570
|
+
</body>
|
|
571
|
+
</html>\`;
|
|
572
|
+
}
|
|
573
|
+
`;
|
|
574
|
+
}
|
|
575
|
+
function generateClientEntry(scan, pagesDir, viewTransitions, prefetch, staticPageData) {
|
|
576
|
+
return `
|
|
577
|
+
import { hydrate } from "svelte";
|
|
578
|
+
import { writable } from "svelte/store";
|
|
579
|
+
import App from "@void/svelte/App.svelte";
|
|
580
|
+
import { setActionRouter } from "@void/svelte";
|
|
581
|
+
import { createRouterFacade, createVoidRouter, idleNavigationState } from "void/pages-client";
|
|
582
|
+
import { parseCacheFor } from "void/pages-prefetch";
|
|
583
|
+
|
|
584
|
+
${generateComponentManifest(scan, pagesDir)}
|
|
585
|
+
|
|
586
|
+
const pageStore = writable(null);
|
|
587
|
+
const propsStore = writable({});
|
|
588
|
+
const layoutsStore = writable([]);
|
|
589
|
+
const sharedStore = writable({});
|
|
590
|
+
const errorsStore = writable({});
|
|
591
|
+
let routeUrl = window.location.pathname + window.location.search + window.location.hash;
|
|
592
|
+
const routeStore = writable(routeUrl);
|
|
593
|
+
const navigationStore = writable(idleNavigationState());
|
|
594
|
+
|
|
595
|
+
const { router, initialPageData, initialComponent, initialLayouts, start } =
|
|
596
|
+
await createVoidRouter({
|
|
597
|
+
adapter: {
|
|
598
|
+
createDeferred: () => ({ value: null, loading: true, error: null }),
|
|
599
|
+
resolveDeferred(ref, value) { return { value, loading: false, error: null }; },
|
|
600
|
+
rejectDeferred(ref, error) { return { value: null, loading: false, error: new Error(error) }; },
|
|
601
|
+
isLoading: (ref) => ref.loading,
|
|
602
|
+
applyUpdate(pageData, comp, layouts) {
|
|
603
|
+
propsStore.set(pageData.props);
|
|
604
|
+
sharedStore.update((s) => ({ ...s, ...(pageData.shared || {}) }));
|
|
605
|
+
errorsStore.set(pageData.errors || {});
|
|
606
|
+
layoutsStore.set(layouts);
|
|
607
|
+
pageStore.set(comp);
|
|
608
|
+
},
|
|
609
|
+
onDeferredUpdate(props) {
|
|
610
|
+
propsStore.set({ ...props });
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
components, layoutTree, routeMeta, matchRoute,
|
|
614
|
+
staticPageData: ${!!staticPageData?.enabled},
|
|
615
|
+
staticPageDataFastPath: ${!!staticPageData?.fastPath},
|
|
616
|
+
viewTransitions: ${!!viewTransitions},
|
|
617
|
+
prefetchHoverDelay: ${prefetch?.hoverDelay ?? 75},
|
|
618
|
+
prefetchDefaultCacheFor: parseCacheFor(${JSON.stringify(prefetch?.cacheFor ?? "30s")}),
|
|
619
|
+
onRouteChange(url) {
|
|
620
|
+
routeUrl = url;
|
|
621
|
+
routeStore.set(url);
|
|
622
|
+
},
|
|
623
|
+
onNavigationChange(navigation) {
|
|
624
|
+
navigationStore.set(navigation);
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const reactiveRouter = createRouterFacade(router, () => routeUrl);
|
|
629
|
+
|
|
630
|
+
setActionRouter(reactiveRouter);
|
|
631
|
+
|
|
632
|
+
pageStore.set(initialComponent);
|
|
633
|
+
propsStore.set(initialPageData.props);
|
|
634
|
+
layoutsStore.set(initialLayouts);
|
|
635
|
+
sharedStore.set(initialPageData.shared || {});
|
|
636
|
+
errorsStore.set(initialPageData.errors || {});
|
|
637
|
+
|
|
638
|
+
const appEl = document.getElementById("app");
|
|
639
|
+
hydrate(App, {
|
|
640
|
+
target: appEl,
|
|
641
|
+
props: {
|
|
642
|
+
page: initialComponent,
|
|
643
|
+
layouts: initialLayouts,
|
|
644
|
+
pageProps: initialPageData.props,
|
|
645
|
+
shared: initialPageData.shared || {},
|
|
646
|
+
errors: initialPageData.errors || {},
|
|
647
|
+
router,
|
|
648
|
+
routeUrl,
|
|
649
|
+
navigation: idleNavigationState(),
|
|
650
|
+
stores: {
|
|
651
|
+
page: pageStore,
|
|
652
|
+
props: propsStore,
|
|
653
|
+
layouts: layoutsStore,
|
|
654
|
+
shared: sharedStore,
|
|
655
|
+
errors: errorsStore,
|
|
656
|
+
route: routeStore,
|
|
657
|
+
navigation: navigationStore,
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
appEl.setAttribute("data-hydrated", "true");
|
|
663
|
+
export { reactiveRouter as router };
|
|
664
|
+
start();
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/plugin.ts
|
|
669
|
+
function voidSvelte(options) {
|
|
670
|
+
const pagesDir = join(process.cwd(), "pages");
|
|
671
|
+
return [
|
|
672
|
+
...svelte(options?.svelte),
|
|
673
|
+
pagesPlugin(pagesDir, {
|
|
674
|
+
viewTransitions: options?.viewTransitions,
|
|
675
|
+
prefetch: options?.prefetch
|
|
676
|
+
}),
|
|
677
|
+
...islandsPlugin(pagesDir)
|
|
678
|
+
];
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
export { voidSvelte };
|