@ilha/router 0.6.1 → 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 };
package/dist/rolldown.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-BP38YTMC.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-B_DKbd_R.js";
2
2
  import { t as ilhaPages } from "./plugin-Cq0xpVL8.js";
3
3
  //#region src/rolldown.ts
4
4
  /** Rolldown plugin — use via `@ilha/router/rolldown`. */
package/dist/rspack.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-BP38YTMC.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-B_DKbd_R.js";
2
2
  import { t as ilhaPages } from "./plugin-Cq0xpVL8.js";
3
3
  //#region src/rspack.ts
4
4
  /** Rspack plugin — use via `@ilha/router/rspack`. */
@@ -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,4 +1,4 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-BP38YTMC.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-B_DKbd_R.js";
2
2
  import { t as ilhaPages } from "./plugin-Cq0xpVL8.js";
3
3
  //#region src/vite.ts
4
4
  /** Vite plugin — use via `@ilha/router/vite`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.6.1",
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>",