@ilha/router 0.6.0 → 0.6.2

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 CHANGED
@@ -580,6 +580,8 @@ Wraps a page island with a layout handler. Used internally by the Vite plugin co
580
580
 
581
581
  On client hydration, `wrapLayout` mounts the full layout island (layout child slots `p:*` and the keyed page slot `k:page`) from existing SSR DOM — it does not re-render layout markup from serialized props. Interactive components in `+layout.tsx` (state, event handlers, nested islands) hydrate the same way as the page.
582
582
 
583
+ With **nested** layouts (codegen: `wrapLayout(outer, wrapLayout(inner, page))`), `renderHydratable()` composes layout markup by awaiting the **leaf** page’s `hydratable()` envelope and injecting that HTML into each layout’s `k:page` slot (outer slot receives the inner layout tree; the innermost slot receives the page). Page state snapshots always come from the leaf page island.
584
+
583
585
  ```ts
584
586
  import { wrapLayout } from "@ilha/router";
585
587
 
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as wrapError, E as setHistoryMode, S as useRoute, T as getHistoryMode, _ as routeParams, a as RouterView, b as router, c as enableLinkInterception, d as loader, f as navigate, g as routeHash, h as redirect, i as RouterLink, l as error, m as prime, n as LoaderError, o as composeLoaders, p as prefetch, r as Redirect, s as defineLayout, t as LOADER_ENDPOINT, u as isActive, v as routePath, w as wrapLayout, x as src_default, y as routeSearch } from "./src-BP38YTMC.js";
1
+ import { C as wrapError, E as setHistoryMode, S as useRoute, T as getHistoryMode, _ as routeParams, a as RouterView, b as router, c as enableLinkInterception, d as loader, f as navigate, g as routeHash, h as redirect, i as RouterLink, l as error, m as prime, n as LoaderError, o as composeLoaders, p as prefetch, r as Redirect, s as defineLayout, t as LOADER_ENDPOINT, u as isActive, v as routePath, w as wrapLayout, x as src_default, y as routeSearch } from "./src-B_DKbd_R.js";
2
2
  export { LOADER_ENDPOINT, LoaderError, Redirect, RouterLink, RouterView, composeLoaders, src_default as default, defineLayout, enableLinkInterception, error, getHistoryMode, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, setHistoryMode, useRoute, wrapError, wrapLayout };
@@ -207,21 +207,17 @@ function buildClientFile(entries, clientFile, opts) {
207
207
  return r.startsWith(".") ? r : `./${r}`;
208
208
  };
209
209
  const clientImport = (abs) => `${rel(abs)}?client`;
210
- const imports = isStatic ? [`import { router as _router } from "@ilha/router";`, `import type { Island } from "ilha";`] : [`import { router, wrapLayout, wrapError } from "@ilha/router";`, `import type { Island } from "ilha";`];
210
+ const imports = isStatic ? [`import { router as _router, wrapLayout, wrapError } from "@ilha/router";`, `import type { Island } from "ilha";`] : [`import { router, wrapLayout, wrapError } from "@ilha/router";`, `import type { Island } from "ilha";`];
211
211
  const wrappedIslandLines = [];
212
212
  const registryLines = [];
213
213
  const routeLines = [];
214
214
  for (const [i, entry] of entries.entries()) {
215
215
  imports.push(`import { default as _page${i} } from ${JSON.stringify(clientImport(entry.file))};`);
216
- if (!isStatic) {
217
- for (const [j, l] of entry.layouts.entries()) imports.push(`import { default as _layout${i}_${j} } from ${JSON.stringify(clientImport(l))};`);
218
- for (const [j, e] of entry.errors.entries()) imports.push(`import { default as _error${i}_${j} } from ${JSON.stringify(clientImport(e))};`);
219
- }
216
+ for (const [j, l] of entry.layouts.entries()) imports.push(`import { default as _layout${i}_${j} } from ${JSON.stringify(clientImport(l))};`);
217
+ for (const [j, e] of entry.errors.entries()) imports.push(`import { default as _error${i}_${j} } from ${JSON.stringify(clientImport(e))};`);
220
218
  let expr = `_page${i}`;
221
- if (!isStatic) {
222
- for (let j = entry.errors.length - 1; j >= 0; j--) expr = `wrapError(_error${i}_${j}, ${expr})`;
223
- for (let j = entry.layouts.length - 1; j >= 0; j--) expr = `wrapLayout(_layout${i}_${j}, ${expr})`;
224
- }
219
+ for (let j = entry.errors.length - 1; j >= 0; j--) expr = `wrapError(_error${i}_${j}, ${expr})`;
220
+ for (let j = entry.layouts.length - 1; j >= 0; j--) expr = `wrapLayout(_layout${i}_${j}, ${expr})`;
225
221
  const wrappedId = `_wrapped${i}`;
226
222
  wrappedIslandLines.push(`const ${wrappedId} = ${expr};`);
227
223
  registryLines.push(` ${JSON.stringify(entry.name)}: ${wrappedId}` + (i < entries.length - 1 ? "," : ""));
package/dist/rolldown.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-BP38YTMC.js";
2
- import { t as ilhaPages } from "./plugin-CJBtP3fl.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-B_DKbd_R.js";
2
+ import { t as ilhaPages } from "./plugin-Cq0xpVL8.js";
3
3
  //#region src/rolldown.ts
4
4
  /** Rolldown plugin — use via `@ilha/router/rolldown`. */
5
5
  function pages(options = {}) {
package/dist/rspack.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-BP38YTMC.js";
2
- import { t as ilhaPages } from "./plugin-CJBtP3fl.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-B_DKbd_R.js";
2
+ import { t as ilhaPages } from "./plugin-Cq0xpVL8.js";
3
3
  //#region src/rspack.ts
4
4
  /** Rspack plugin — use via `@ilha/router/rspack`. */
5
5
  function pages(options = {}) {
@@ -179,10 +179,70 @@ function composeLoaders(loaders) {
179
179
  return Object.assign({}, ...results);
180
180
  };
181
181
  }
182
+ const WRAP_LAYOUT_LEAF = Symbol.for("ilha.router.wrapLayout.leaf");
183
+ function extractHydratableInnerHtml(block) {
184
+ const m = block.match(/^<([a-zA-Z][\w-]*)\s[^>]*>([\s\S]*)<\/\1>\s*$/);
185
+ return m ? m[2] : block;
186
+ }
187
+ function parseHydratableOpenTag(block) {
188
+ const m = block.match(/^<([a-zA-Z][\w-]*)\s([^>]*)>/);
189
+ if (!m) return null;
190
+ return {
191
+ tag: m[1],
192
+ attrs: m[2]
193
+ };
194
+ }
195
+ function findKPageSlotSpans(layoutHtml) {
196
+ const spans = [];
197
+ for (const m of layoutHtml.matchAll(/<div\s[^>]*data-ilha-slot="k:page"[^>]*>/g)) {
198
+ const openEnd = m.index + m[0].length;
199
+ let depth = 1;
200
+ let i = openEnd;
201
+ while (i < layoutHtml.length && depth > 0) {
202
+ const nextOpen = layoutHtml.indexOf("<div", i);
203
+ const nextClose = layoutHtml.indexOf("</div>", i);
204
+ if (nextClose === -1) break;
205
+ if (nextOpen !== -1 && nextOpen < nextClose) {
206
+ depth += 1;
207
+ i = nextOpen + 4;
208
+ } else {
209
+ depth -= 1;
210
+ if (depth === 0) {
211
+ spans.push({
212
+ openEnd,
213
+ closeStart: nextClose
214
+ });
215
+ break;
216
+ }
217
+ i = nextClose + 6;
218
+ }
219
+ }
220
+ }
221
+ return spans;
222
+ }
223
+ /** Replace inner HTML of the first or innermost `k:page` slot (empty or pre-filled). */
224
+ function injectKPageSlot(layoutHtml, slotInnerHtml, which) {
225
+ const spans = findKPageSlotSpans(layoutHtml);
226
+ if (spans.length === 0) return layoutHtml;
227
+ const target = which === "innermost" ? spans[spans.length - 1] : spans[0];
228
+ return layoutHtml.slice(0, target.openEnd) + slotInnerHtml + layoutHtml.slice(target.closeStart);
229
+ }
230
+ async function wrapLayoutSlotMarkup(innerWrapped, leafPage, props, opts) {
231
+ const pageInner = extractHydratableInnerHtml(await leafPage.hydratable(props, opts));
232
+ return injectKPageSlot(innerWrapped.toString(props), pageInner, "innermost");
233
+ }
182
234
  function wrapLayout(layout, page) {
235
+ const leafPage = page[WRAP_LAYOUT_LEAF] ?? page;
236
+ const childWrapped = leafPage !== page ? page : null;
183
237
  const Wrapped = layout(Object.assign(page.key("page"), { toString: page.toString.bind(page) }));
238
+ Wrapped[WRAP_LAYOUT_LEAF] = leafPage;
184
239
  function pageMountHost(host) {
185
- return host.querySelector("[data-ilha-slot=\"k:page\"]") ?? host;
240
+ const slots = [...host.querySelectorAll("[data-ilha-slot=\"k:page\"]")].filter((slot) => {
241
+ const boundary = slot.closest("[data-ilha]");
242
+ return boundary === null || boundary === host;
243
+ });
244
+ if (slots.length === 0) return host;
245
+ return slots[slots.length - 1];
186
246
  }
187
247
  function preparePageMountHost(outer, mountHost) {
188
248
  if (mountHost.hasAttribute("data-ilha-state")) return;
@@ -215,11 +275,13 @@ function wrapLayout(layout, page) {
215
275
  Wrapped.hydratable = async (props, opts) => {
216
276
  if (!opts?.name) throw new Error("wrapLayout: hydratable requires options.name");
217
277
  const resolvedProps = props ?? {};
218
- const pageBlock = await page.hydratable(resolvedProps, opts);
219
- const openTag = pageBlock.match(/^<([a-zA-Z][\w-]*)\s([^>]*)>/);
220
- if (!openTag) return pageBlock;
221
- const layoutInner = Wrapped.toString(resolvedProps);
222
- return `<${openTag[1]} ${openTag[2]}>${layoutInner}</${openTag[1]}>`;
278
+ const pageBlock = await leafPage.hydratable(resolvedProps, opts);
279
+ const open = parseHydratableOpenTag(pageBlock);
280
+ if (!open) return pageBlock;
281
+ const pageInner = extractHydratableInnerHtml(pageBlock);
282
+ const slotContent = childWrapped ? await wrapLayoutSlotMarkup(childWrapped, leafPage, resolvedProps, opts) : pageInner;
283
+ const layoutInnerOut = injectKPageSlot(Wrapped.toString(resolvedProps), slotContent, "first");
284
+ return `<${open.tag} ${open.attrs}>${layoutInnerOut}</${open.tag}>`;
223
285
  };
224
286
  return Wrapped;
225
287
  }
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-BP38YTMC.js";
2
- import { t as ilhaPages } from "./plugin-CJBtP3fl.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-B_DKbd_R.js";
2
+ import { t as ilhaPages } from "./plugin-Cq0xpVL8.js";
3
3
  //#region src/vite.ts
4
4
  /** Vite plugin — use via `@ilha/router/vite`. */
5
5
  function pages(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",