@void/svelte 1.1.11 → 1.1.12

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/dist/plugin.mjs CHANGED
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import nodePath from "node:path";
5
5
  import { preprocess } from "svelte/compiler";
6
6
  import { normalizePath } from "vite";
7
- import { ISLANDS_CLIENT_ENTRY_ID, ISLANDS_RENDERER_ID, computePagesNeedingClientJS, editIslandSource, findIslandImports, findStaticCssImports, needsIslandClientBundle, resolveSpecifier, rewriteCssImportForVirtualModule, watchPagesForRescan } from "void/pages-islands-plugin";
7
+ import { ISLANDS_CLIENT_ENTRY_ID, ISLANDS_RENDERER_ID, computePagesNeedingClientJS, editIslandSource, findIslandImports, findStaticCssImports, hasIslandPages, islandStylesPlugin, recordIslandStyles, recordedIslandStyles, resolveSpecifier, rewriteCssImportForVirtualModule, watchPagesForRescan } from "void/pages-islands-plugin";
8
8
  //#region ../../node_modules/.pnpm/pathslash@0.1.0/node_modules/pathslash/dist/index.mjs
9
9
  const BACKSLASH_RE = /\\/g;
10
10
  const isWindows = process.platform === "win32";
@@ -80,136 +80,146 @@ function islandsPlugin(pagesDir, options) {
80
80
  islandId
81
81
  };
82
82
  }
83
- return [{
84
- name: "void-svelte:islands",
85
- enforce: "pre",
86
- configResolved(config) {
87
- resolvedConfig = config;
88
- isDev = config.command === "serve";
89
- },
90
- configureServer(server) {
91
- watchPagesForRescan(server, pagesDir, root, (scan) => {
92
- pageScan = scan;
93
- });
94
- },
95
- async buildStart() {
96
- if (existsSync(pagesDir)) {
97
- pageScan = await scanPages(root, { pagesDir });
98
- const filesToScan = [
99
- ...pageScan.pages.map((page) => ({
100
- filePath: page.componentPath,
101
- island: page.island
102
- })),
103
- ...pageScan.layouts.map((layout) => ({
104
- filePath: layout.filePath,
105
- island: layout.island
106
- })),
107
- ...pageScan.namedLayouts.map((namedLayout) => ({
108
- filePath: namedLayout.filePath,
109
- island: namedLayout.island
110
- }))
111
- ];
112
- for (const entry of filesToScan) {
113
- if (!entry.island) continue;
114
- const filePath = resolve(pagesDir, entry.filePath);
115
- const code = readFileSync(filePath, "utf-8");
116
- const importerDir = dirname(filePath);
117
- for (const islandImport of findIslandImports(code, filePath)) registerIsland(islandImport.specifier, importerDir, islandImport.hydrate);
83
+ return [
84
+ {
85
+ name: "void-svelte:islands",
86
+ enforce: "pre",
87
+ configResolved(config) {
88
+ resolvedConfig = config;
89
+ isDev = config.command === "serve";
90
+ },
91
+ configureServer(server) {
92
+ watchPagesForRescan(server, pagesDir, root, (scan) => {
93
+ pageScan = scan;
94
+ });
95
+ },
96
+ async buildStart() {
97
+ if (existsSync(pagesDir)) {
98
+ pageScan = await scanPages(root, { pagesDir });
99
+ const filesToScan = [
100
+ ...pageScan.pages.map((page) => ({
101
+ filePath: page.componentPath,
102
+ island: page.island
103
+ })),
104
+ ...pageScan.layouts.map((layout) => ({
105
+ filePath: layout.filePath,
106
+ island: layout.island
107
+ })),
108
+ ...pageScan.namedLayouts.map((namedLayout) => ({
109
+ filePath: namedLayout.filePath,
110
+ island: namedLayout.island
111
+ }))
112
+ ];
113
+ for (const entry of filesToScan) {
114
+ if (!entry.island) continue;
115
+ const filePath = resolve(pagesDir, entry.filePath);
116
+ const code = readFileSync(filePath, "utf-8");
117
+ const importerDir = dirname(filePath);
118
+ for (const islandImport of findIslandImports(code, filePath)) registerIsland(islandImport.specifier, importerDir, islandImport.hydrate);
119
+ }
120
+ for (const page of pageScan.pages) {
121
+ if (!page.island) continue;
122
+ if (page.componentPath.endsWith(".svelte")) islandCssFiles.add(normalizePath(resolve(pagesDir, page.componentPath)));
123
+ const layoutChain = resolveEffectiveLayoutChain(page, pageScan.layouts, pageScan.namedLayouts);
124
+ for (const layout of layoutChain) islandCssFiles.add(normalizePath(resolve(pagesDir, layout.filePath)));
125
+ }
118
126
  }
119
- for (const page of pageScan.pages) {
120
- if (!page.island) continue;
121
- if (page.componentPath.endsWith(".svelte")) islandCssFiles.add(normalizePath(resolve(pagesDir, page.componentPath)));
122
- const layoutChain = resolveEffectiveLayoutChain(page, pageScan.layouts, pageScan.namedLayouts);
123
- for (const layout of layoutChain) islandCssFiles.add(normalizePath(resolve(pagesDir, layout.filePath)));
127
+ },
128
+ buildEnd() {
129
+ recordIslandStyles(this, pageScan, pagesDir);
130
+ },
131
+ resolveId: {
132
+ filter: { id: /^(virtual:void-islands-(renderer|client)|\/@void-island-wrap\/)/ },
133
+ handler(source) {
134
+ if (source === ISLANDS_RENDERER_ID || source === ISLANDS_CLIENT_ENTRY_ID) return "\0" + source;
135
+ if (source.startsWith(WRAPPER_PREFIX)) return source;
124
136
  }
125
- }
126
- },
127
- resolveId: {
128
- filter: { id: /^(virtual:void-islands-(renderer|client)|\/@void-island-wrap\/)/ },
129
- handler(source) {
130
- if (source === ISLANDS_RENDERER_ID || source === ISLANDS_CLIENT_ENTRY_ID) return "\0" + source;
131
- if (source.startsWith(WRAPPER_PREFIX)) return source;
132
- }
133
- },
134
- load: {
135
- filter: { id: /^(\0virtual:void-islands-(renderer|client)|\/@void-island-wrap\/)/ },
136
- handler(id) {
137
- if (id === "\0" + ISLANDS_RENDERER_ID) {
138
- if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
139
- const mdPlugin = resolvedConfig?.plugins?.find((plugin) => plugin.name === "void-md");
140
- const mdScriptIds = new Set(mdPlugin?.api?.getClientScripts?.()?.keys() ?? []);
141
- const needsJS = computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
142
- return generateIslandRenderer(pageScan, pagesDir, mdScriptIds, needsJS);
137
+ },
138
+ load: {
139
+ filter: { id: /^(\0virtual:void-islands-(renderer|client)|\/@void-island-wrap\/)/ },
140
+ handler(id) {
141
+ if (id === "\0" + ISLANDS_RENDERER_ID) {
142
+ if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
143
+ const mdPlugin = resolvedConfig?.plugins?.find((plugin) => plugin.name === "void-md");
144
+ const mdScriptIds = new Set(mdPlugin?.api?.getClientScripts?.()?.keys() ?? []);
145
+ const needsJS = computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig);
146
+ return generateIslandRenderer(pageScan, pagesDir, mdScriptIds, needsJS);
147
+ }
148
+ if (id === "\0" + ISLANDS_CLIENT_ENTRY_ID) {
149
+ if (!pageScan) return "";
150
+ return generateIslandClientEntry(islandManifest, islandCssFiles, recordedIslandStyles(pagesDir), islandManifest.size > 0 || computePagesNeedingClientJS(pageScan, pagesDir, resolvedConfig).size > 0, isDev);
151
+ }
152
+ if (id.startsWith(WRAPPER_PREFIX)) return generateWrapperComponent(id);
143
153
  }
144
- if (id === "\0" + ISLANDS_CLIENT_ENTRY_ID) {
145
- if (!pageScan) return "";
146
- return generateIslandClientEntry(islandManifest, islandCssFiles, isDev);
154
+ },
155
+ transform: {
156
+ filter: { id: /\.island\.svelte$/ },
157
+ handler(code, id) {
158
+ if (/import\s+\{[^}]*\buseForm\b/.test(code)) this.warn(`islands: useForm cannot be imported in island page '${id}'. Use useIslandForm instead.`);
159
+ const importerDir = dirname(id);
160
+ const edits = [];
161
+ for (const islandImport of findIslandImports(code, id)) {
162
+ const name = islandImport.importName;
163
+ const spec = islandImport.specifier;
164
+ const strategy = islandImport.hydrate;
165
+ const { absPath, islandId } = registerIsland(spec, importerDir, strategy);
166
+ const wrapperUrl = WRAPPER_PREFIX + encodeURIComponent(islandId) + ".svelte?src=" + encodeURIComponent(absPath || spec) + "&island=" + encodeURIComponent(islandId) + "&hydrate=" + encodeURIComponent(strategy);
167
+ const replacement = `import ${name} from ${JSON.stringify(wrapperUrl)}`;
168
+ edits.push({
169
+ start: islandImport.start,
170
+ end: islandImport.end,
171
+ text: replacement
172
+ });
173
+ }
174
+ if (edits.length > 0) return editIslandSource(code, edits);
147
175
  }
148
- if (id.startsWith(WRAPPER_PREFIX)) return generateWrapperComponent(id);
176
+ },
177
+ async config(_, env) {
178
+ if (!existsSync(pagesDir)) return;
179
+ if (env.command === "serve") return { optimizeDeps: { include: ["svelte"] } };
180
+ if (await hasIslandPages(root, pagesDir)) return { environments: { client: { build: {
181
+ manifest: true,
182
+ rollupOptions: { input: { "islands-client": ISLANDS_CLIENT_ENTRY_ID } }
183
+ } } } };
149
184
  }
150
185
  },
151
- transform: {
152
- filter: { id: /\.island\.svelte$/ },
153
- handler(code, id) {
154
- if (/import\s+\{[^}]*\buseForm\b/.test(code)) this.warn(`islands: useForm cannot be imported in island page '${id}'. Use useIslandForm instead.`);
155
- const importerDir = dirname(id);
156
- const edits = [];
157
- for (const islandImport of findIslandImports(code, id)) {
158
- const name = islandImport.importName;
159
- const spec = islandImport.specifier;
160
- const strategy = islandImport.hydrate;
161
- const { absPath, islandId } = registerIsland(spec, importerDir, strategy);
162
- const wrapperUrl = WRAPPER_PREFIX + encodeURIComponent(islandId) + ".svelte?src=" + encodeURIComponent(absPath || spec) + "&island=" + encodeURIComponent(islandId) + "&hydrate=" + encodeURIComponent(strategy);
163
- const replacement = `import ${name} from ${JSON.stringify(wrapperUrl)}`;
164
- edits.push({
165
- start: islandImport.start,
166
- end: islandImport.end,
167
- text: replacement
168
- });
186
+ {
187
+ name: "void-svelte:islands-style",
188
+ apply: "build",
189
+ resolveId: {
190
+ filter: { id: /\.svelte\.css\?island-style$/ },
191
+ handler(source) {
192
+ return source;
169
193
  }
170
- if (edits.length > 0) return editIslandSource(code, edits);
171
- }
172
- },
173
- config(_, env) {
174
- if (!existsSync(pagesDir)) return;
175
- if (env.command === "serve") return { optimizeDeps: { include: ["svelte"] } };
176
- if (needsIslandClientBundle(pagesDir, /\.island\.svelte$/, true)) return { environments: { client: { build: {
177
- manifest: true,
178
- rollupOptions: { input: { "islands-client": ISLANDS_CLIENT_ENTRY_ID } }
179
- } } } };
180
- }
181
- }, {
182
- name: "void-svelte:islands-style",
183
- apply: "build",
184
- resolveId: {
185
- filter: { id: /\.svelte\.css\?island-style$/ },
186
- handler(source) {
187
- return source;
188
- }
189
- },
190
- load: {
191
- filter: { id: /\.svelte\.css\?island-style$/ },
192
- async handler(id) {
193
- const file = id.slice(0, -17);
194
- const api = resolvedConfig?.plugins.find((plugin) => plugin.name === "vite-plugin-svelte:config")?.api;
195
- if (typeof api?.compileSvelte !== "function" || typeof api.idParser !== "function") this.error("Svelte island page styles need the @sveltejs/vite-plugin-svelte version that @void/svelte depends on.");
196
- this.addWatchFile(file);
197
- let code = readFileSync(file, "utf-8");
198
- let map;
199
- if (api.options.preprocess) {
200
- const processed = await preprocess(code, api.options.preprocess, { filename: file });
201
- code = processed.code;
202
- map = processed.map;
203
- for (const dependency of processed.dependencies ?? []) this.addWatchFile(dependency);
194
+ },
195
+ load: {
196
+ filter: { id: [/\.svelte\.css\?island-style$/, /\.svelte\?svelte&type=style/] },
197
+ order: "pre",
198
+ async handler(id) {
199
+ const islandStyle = id.endsWith(".css?island-style");
200
+ if (!islandStyle && (this.environment.config.consumer !== "client" || !recordedIslandStyles(pagesDir)?.includes(id))) return;
201
+ const file = islandStyle ? id.slice(0, -17) : id.slice(0, id.indexOf("?"));
202
+ const api = resolvedConfig?.plugins.find((plugin) => plugin.name === "vite-plugin-svelte:config")?.api;
203
+ if (typeof api?.compileSvelte !== "function" || typeof api.idParser !== "function") this.error("Svelte island page styles need the @sveltejs/vite-plugin-svelte version that @void/svelte depends on.");
204
+ this.addWatchFile(file);
205
+ let code = readFileSync(file, "utf-8");
206
+ let map;
207
+ if (api.options.preprocess) {
208
+ const processed = await preprocess(code, api.options.preprocess, { filename: file });
209
+ code = processed.code;
210
+ map = processed.map;
211
+ for (const dependency of processed.dependencies ?? []) this.addWatchFile(dependency);
212
+ }
213
+ const { compiled } = await api.compileSvelte(api.idParser(file, false), code, api.options, this.environment, map);
214
+ return {
215
+ code: compiled.css?.code ?? "",
216
+ map: compiled.css?.map ?? null
217
+ };
204
218
  }
205
- const { compiled } = await api.compileSvelte(api.idParser(file, false), code, api.options, this.environment, map);
206
- return {
207
- code: compiled.css?.code ?? "",
208
- map: compiled.css?.map ?? null
209
- };
210
219
  }
211
- }
212
- }];
220
+ },
221
+ islandStylesPlugin(pagesDir)
222
+ ];
213
223
  }
214
224
  function generateWrapperComponent(id) {
215
225
  const url = new URL(id, "file://");
@@ -232,6 +242,7 @@ function generateIslandRenderer(scan, pagesDir, mdScriptIds, pagesNeedingClientJ
232
242
  import { render } from "svelte/server";
233
243
  import App from "@void/svelte/App.svelte";
234
244
  import { renderHeadToString, renderHtmlAttrs, renderBodyAttrs } from "void/pages-head";
245
+ import { islandFrontmatterScript } from "void/pages-server";
235
246
 
236
247
  ${generateComponentManifest(scan, pagesDir)}
237
248
  ${mdScriptIdsCode}
@@ -242,7 +253,8 @@ function escapeAttr(s) {
242
253
  }
243
254
 
244
255
  export async function renderIslandPage(pageObj, assetTags) {
245
- const PageComponent = (await components[pageObj.component]()).default;
256
+ const __pageModule = await components[pageObj.component]();
257
+ const PageComponent = __pageModule.default;
246
258
  const layoutIds = layoutTree[pageObj.component] || [];
247
259
  const layoutComponents = await Promise.all(
248
260
  layoutIds.map(async (id) => (await components[id]()).default)
@@ -255,11 +267,18 @@ export async function renderIslandPage(pageObj, assetTags) {
255
267
  pageProps: pageObj.props,
256
268
  shared: pageObj.shared || {},
257
269
  params: pageObj.params || {},
270
+ // Set above the layouts so they can read the Markdown page's values.
271
+ frontmatter: __pageModule.frontmatter,
258
272
  router: null,
259
273
  stores: null,
260
274
  },
261
275
  });
262
276
 
277
+ // Islands hydrate as separate roots. Send the page frontmatter so their
278
+ // useFrontmatter() returns the same value in the browser.
279
+ const __frontmatterScript = __pagesNeedingClientJS.has(pageObj.component) && html.includes("data-island=")
280
+ ? islandFrontmatterScript(__pageModule.frontmatter)
281
+ : "";
263
282
  const __mdScriptAttr = __mdClientScriptIds.has(pageObj.component) ? \` data-md-script="\${escapeAttr(pageObj.component)}"\` : "";
264
283
  const headHtml = pageObj.head ? renderHeadToString(pageObj.head) : "";
265
284
  const htmlAttrs = pageObj.head ? renderHtmlAttrs(pageObj.head) : "";
@@ -270,31 +289,34 @@ export async function renderIslandPage(pageObj, assetTags) {
270
289
  <head>\${headHtml}\${head}\${assetTags.css}\${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.preloads : ""}</head>
271
290
  <body\${bodyAttrs}>
272
291
  <div id="app"\${__mdScriptAttr}>\${html}</div>
273
- \${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.body : ""}
292
+ \${__frontmatterScript}\${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.body : ""}
274
293
  </body>
275
294
  </html>\`;
276
295
  }
277
296
  `;
278
297
  }
279
- function generateIslandClientEntry(manifest, cssFiles, isDev) {
298
+ function generateIslandClientEntry(manifest, cssFiles, styles, needsRuntime, isDev) {
280
299
  const entries = [...manifest.values()];
281
300
  const uniqueIslands = /* @__PURE__ */ new Map();
282
301
  for (const entry of entries) uniqueIslands.set(entry.islandId, entry.componentPath);
283
302
  const lines = [];
284
- lines.push("import { initIslands } from \"void/pages-client\";");
285
- if (!isDev) for (const file of cssFiles) {
303
+ if (!isDev && styles) for (const id of styles) lines.push(`import ${JSON.stringify(id)};`);
304
+ else if (!isDev) for (const file of cssFiles) {
286
305
  const content = readFileSync(file, "utf-8");
287
306
  if (/<style\b/.test(content)) lines.push(`import ${JSON.stringify(file + ".css?island-style")};`);
288
307
  for (const item of findStaticCssImports(content, file)) lines.push(rewriteCssImportForVirtualModule(item.statement, item.specifier, file));
289
308
  }
309
+ if (!needsRuntime && !isDev) return lines.join("\n");
310
+ lines.push("import { initIslands } from \"void/pages-client\";");
290
311
  lines.push("const islands = {");
291
312
  for (const [id, path] of uniqueIslands) lines.push(` ${JSON.stringify(id)}: () => import(${JSON.stringify(path)}),`);
292
313
  lines.push("};");
293
314
  lines.push(`
294
- async function hydrateIsland(el, mod, props) {
315
+ async function hydrateIsland(el, mod, props, frontmatter) {
295
316
  const { hydrate } = await import("svelte");
296
317
  const Component = mod.default || mod;
297
- hydrate(Component, { target: el, props });
318
+ const context = new Map([["__void_frontmatter", frontmatter]]);
319
+ hydrate(Component, { target: el, props, context });
298
320
  el.setAttribute("data-hydrated", "true");
299
321
  }
300
322
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/svelte",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
4
4
  "description": "Svelte pages, server rendering, forms, and navigation for Void.",
5
5
  "homepage": "https://void.cloud",
6
6
  "bugs": {
@@ -43,12 +43,12 @@
43
43
  "svelte": "^5.57.2",
44
44
  "vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
45
45
  "vite-plus": "1.2.0",
46
- "void": "1.1.11"
46
+ "void": "1.1.12"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "svelte": "^5.0.0",
50
50
  "vite": "^8.0.0",
51
- "void": "1.1.11"
51
+ "void": "1.1.12"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "vp pack",
@@ -13,6 +13,7 @@
13
13
  pageProps: props.pageProps,
14
14
  shared: props.shared,
15
15
  params: props.params,
16
+ frontmatter: props.frontmatter,
16
17
  router: props.router,
17
18
  routeUrl: props.routeUrl,
18
19
  navigation: props.navigation,
@@ -51,6 +52,10 @@
51
52
  setContext("__void_navigation", {
52
53
  get value() { return navigation; },
53
54
  });
55
+ // Island pages pass Markdown frontmatter so layouts can read it (useFrontmatter).
56
+ if (init.frontmatter) {
57
+ setContext("__void_frontmatter", init.frontmatter);
58
+ }
54
59
 
55
60
  // Client mode: subscribe to stores for live updates after hydration.
56
61
  // onMount only runs on the client, not during SSR.