@ogygia/core 0.11.4 → 0.12.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.
Files changed (94) hide show
  1. package/README.md +1 -1
  2. package/dist/KeepHost.svelte +1 -1
  3. package/dist/LiveHost.svelte +1 -1
  4. package/dist/NestedProvider.svelte +1 -1
  5. package/dist/Region.svelte +28 -21
  6. package/dist/Region.svelte.d.ts +0 -2
  7. package/dist/SlotBoundary.svelte +1 -1
  8. package/dist/compiler/bom.js +1 -1
  9. package/dist/compiler/browser.d.ts +1 -1
  10. package/dist/compiler/browser.js +1 -1
  11. package/dist/compiler/ctx.d.ts +1 -1
  12. package/dist/compiler/ctx.js +3 -3
  13. package/dist/compiler/{macros/dedent.d.ts → dedent.d.ts} +12 -1
  14. package/dist/compiler/dedent.js +43 -0
  15. package/dist/compiler/driver.d.ts +9 -6
  16. package/dist/compiler/driver.js +69 -35
  17. package/dist/compiler/ids.d.ts +3 -0
  18. package/dist/compiler/ids.js +4 -1
  19. package/dist/compiler/index.d.ts +3 -3
  20. package/dist/compiler/index.js +3 -3
  21. package/dist/compiler/kit.d.ts +7 -0
  22. package/dist/compiler/kit.js +22 -10
  23. package/dist/compiler/link/caps.js +59 -9
  24. package/dist/compiler/link/island-deps.js +225 -22
  25. package/dist/compiler/link/location-map.d.ts +10 -0
  26. package/dist/compiler/link/location-map.js +53 -0
  27. package/dist/compiler/link/manifest.js +5 -1
  28. package/dist/compiler/link/runtime-entry.d.ts +19 -2
  29. package/dist/compiler/link/runtime-entry.js +34 -1
  30. package/dist/compiler/link/server-manifest.js +43 -7
  31. package/dist/compiler/link/transport.js +39 -7
  32. package/dist/compiler/macros/source.js +2 -2
  33. package/dist/compiler/program.d.ts +4 -0
  34. package/dist/compiler/program.js +5 -1
  35. package/dist/compiler/region/transform.d.ts +13 -4
  36. package/dist/compiler/region/transform.js +15 -6
  37. package/dist/context.d.ts +3 -12
  38. package/dist/context.js +1 -16
  39. package/dist/hooks.d.ts +16 -0
  40. package/dist/hooks.js +4 -2
  41. package/dist/insights/page-insights.js +2 -2
  42. package/dist/internal.d.ts +2 -1
  43. package/dist/internal.js +2 -1
  44. package/dist/nested.d.ts +11 -0
  45. package/dist/nested.js +19 -0
  46. package/dist/ref-sink.d.ts +15 -0
  47. package/dist/ref-sink.js +19 -0
  48. package/dist/ref.d.ts +2 -2
  49. package/dist/ref.js +1 -11
  50. package/dist/runtime/beacon.js +1 -1
  51. package/dist/runtime/boot-link.js +4 -1
  52. package/dist/runtime/core.js +10 -7
  53. package/dist/runtime/entry-locations.d.ts +10 -3
  54. package/dist/runtime/entry-locations.js +132 -21
  55. package/dist/runtime/full.js +5 -1
  56. package/dist/runtime/hydrate-core.d.ts +4 -4
  57. package/dist/runtime/hydrate-core.js +8 -5
  58. package/dist/runtime/morph.d.ts +7 -0
  59. package/dist/runtime/morph.js +21 -15
  60. package/dist/runtime/nav-handle.d.ts +3 -0
  61. package/dist/runtime/nav-handle.js +12 -5
  62. package/dist/runtime/ownership.d.ts +10 -1
  63. package/dist/runtime/ownership.js +19 -1
  64. package/dist/runtime/reconcile.d.ts +5 -2
  65. package/dist/runtime/reconcile.js +52 -16
  66. package/dist/runtime/region-endpoint-url.js +1 -0
  67. package/dist/runtime/region-ids.js +2 -2
  68. package/dist/runtime/remote-seeds.js +14 -3
  69. package/dist/runtime/router-nav.js +15 -0
  70. package/dist/runtime/router.js +3 -0
  71. package/dist/runtime/seeds.d.ts +10 -5
  72. package/dist/runtime/seeds.js +16 -12
  73. package/dist/runtime/slots.d.ts +24 -2
  74. package/dist/runtime/slots.js +2 -0
  75. package/dist/runtime/wire-classes.d.ts +2 -0
  76. package/dist/runtime/wire-classes.js +10 -1
  77. package/dist/runtime/wire-format.d.ts +4 -1
  78. package/dist/runtime/wire-format.js +6 -1
  79. package/dist/seed-ref-reviver.d.ts +24 -0
  80. package/dist/seed-ref-reviver.js +44 -0
  81. package/dist/seed-refs.d.ts +2 -16
  82. package/dist/seed-refs.js +1 -33
  83. package/dist/server/document-tail.d.ts +3 -2
  84. package/dist/server/document-tail.js +4 -3
  85. package/dist/server/entry-location.js +11 -8
  86. package/dist/server/props-wire.js +4 -3
  87. package/dist/server/signing-key.d.ts +10 -2
  88. package/dist/server/signing-key.js +49 -15
  89. package/dist/vite/index.js +5 -4
  90. package/dist/vite/kit-dirs.js +1 -1
  91. package/package.json +3 -3
  92. package/dist/compiler/link/entry-shim.d.ts +0 -4
  93. package/dist/compiler/link/entry-shim.js +0 -29
  94. package/dist/compiler/macros/dedent.js +0 -36
@@ -1,5 +1,5 @@
1
1
  import { dispose_ids } from "../ref.js";
2
- import { boot_link, morph_sync_attributes } from "./slots.js";
2
+ import { boot_link, morph_sync_attributes, slots } from "./slots.js";
3
3
  import { region_owned_ids } from "./region-ids.js";
4
4
  import { emit } from "../events/bus.js";
5
5
  import { endpoint_key, fingerprint_of, fnv1a, signature_of } from "./hash.js";
@@ -27,22 +27,48 @@ import { endpoint_key, fingerprint_of, fnv1a, signature_of } from "./hash.js";
27
27
  const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
28
28
  const props_sidecar_of = (region) => boot_link().props_sidecar_of(region);
29
29
  /**
30
- * `<body>`'s attributes have two owners (runtime/ownership.ts): the server, page by page (a theme
31
- * class, a layout flag), and the scripts living on the page (a scroll lock, a consent banner's state,
32
- * an analytics marker). A navigation moves the server's attributes to the new page's — and removes
33
- * only those the PREVIOUS page's server sent that the new one doesn't. An attribute a script set is
34
- * not the server's to take away (before this, every navigation stripped them all). The server's set
35
- * starts as the body's attributes when this module loads, which the runtime does before the page's
36
- * own scripts (head-presence.ts puts it first).
30
+ * `<body>` has two owners (runtime/ownership.ts): the server, page by page (a theme class, a layout
31
+ * flag, the content), and the scripts living on the page (a scroll lock, a consent banner's state; a
32
+ * component mounted into the body, such as a popover's portal). A navigation moves the server's part
33
+ * to the new page's and leaves the page's part alone:
34
+ * - ATTRIBUTES: it removes only those the PREVIOUS page's server sent that the new one doesn't;
35
+ * - CHILDREN: the morph steps over a child the server never sent (a portal's mount anchor: taking
36
+ * it away leaves the portal inserting its content beside a detached node, so a popover in a kept
37
+ * island never opens again).
38
+ * ONE record per reconciled root (ownership.ts `record_server_body`), reached through the boot
39
+ * registry like every boot helper this lazy chunk uses: the router takes the body's when it starts,
40
+ * before any island wakes; each reconcile renews its root's (a harness driving it on its own root
41
+ * gets one from its first call). A root with no record yet is all the server's: every child is
42
+ * matched, and no attribute is taken away.
37
43
  */
38
- const attr_names = (el) => new Set(Array.from(el.attributes, (a) => a.name));
39
- let server_body_attrs = typeof document !== "undefined" && document.body ? attr_names(document.body) : null;
44
+ /** The record of what the server last sent `live`, or null while there is none. */
45
+ function body_record(live) {
46
+ const link = slots.boot;
47
+ return link && link.server_body_attributes(live) !== null ? link : null;
48
+ }
40
49
  function sync_body_attributes(live, next) {
41
50
  morph_sync_attributes()(live, next, true);
42
- if (server_body_attrs) {
43
- for (const name of server_body_attrs) if (!next.hasAttribute(name)) live.removeAttribute(name);
51
+ const sent = body_record(live)?.server_body_attributes(live);
52
+ if (sent) {
53
+ for (const name of sent) if (!next.hasAttribute(name)) live.removeAttribute(name);
44
54
  }
45
- server_body_attrs = attr_names(next);
55
+ }
56
+ /** The live body's children that are the page's own, not the server's (none without a record). */
57
+ function page_own_children(live) {
58
+ const own = /* @__PURE__ */ new Set();
59
+ const record = body_record(live);
60
+ if (record === null) return own;
61
+ for (let n = live.firstChild; n; n = n.nextSibling) if (!record.server_body_child(n)) own.add(n);
62
+ return own;
63
+ }
64
+ /** After a navigation the server's body is the new page's: its attributes, and every live child that
65
+ * is not the page's own. */
66
+ function renew_server_body(live, next, own) {
67
+ const link = slots.boot;
68
+ if (!link) return;
69
+ const children = [];
70
+ for (let n = live.firstChild; n; n = n.nextSibling) if (!own.has(n)) children.push(n);
71
+ link.record_server_body(live, next, children);
46
72
  }
47
73
  /** The `<script data-ogygia-props>` seed text of a region — keyed at the end of the body or the
48
74
  * adjacent sibling (runtime/sidecar.ts) — or '' — the same lookup the hydrator uses, read-only.
@@ -143,6 +169,7 @@ function dt_emit_reconcile(live, next) {
143
169
  function reconcile_body(live, next, morph) {
144
170
  stamp_region_keys(live);
145
171
  stamp_region_keys(next);
172
+ const own = page_own_children(live);
146
173
  sync_body_attributes(live, next);
147
174
  if (DEVTOOLS) dt_emit_reconcile(live, next);
148
175
  const next_keys = /* @__PURE__ */ new Set();
@@ -158,7 +185,8 @@ function reconcile_body(live, next, morph) {
158
185
  if (set !== void 0) for (const id of set) removed_ids.push(id);
159
186
  }
160
187
  absorb_kept_props(live, next);
161
- morph(live, next.childNodes);
188
+ morph(live, next.childNodes, own.size > 0 ? { stays: (n) => own.has(n) } : void 0);
189
+ renew_server_body(live, next, own);
162
190
  if (removed_ids.length > 0) dispose_ids(removed_ids);
163
191
  }
164
192
  /**
@@ -168,12 +196,20 @@ function reconcile_body(live, next, morph) {
168
196
  * analytics, a scroll library) attached to `document.body`. htmx 4 boosts land the same way and use
169
197
  * `outerSync` for exactly this reason: keep the element, sync its attributes, replace its children.
170
198
  * So we do too — the live body node survives, only its guts change. Incoming children are moved out
171
- * of the parsed document (insertion adopts them across documents).
199
+ * of the parsed document (insertion adopts them across documents). The page's own children (a
200
+ * portal's anchor) stay, as in the reconcile; the server's are replaced.
172
201
  */
173
202
  function swap_body(live, next) {
203
+ const own = page_own_children(live);
174
204
  sync_body_attributes(live, next);
175
- live.replaceChildren();
205
+ if (own.size === 0) live.replaceChildren();
206
+ else for (let n = live.firstChild; n;) {
207
+ const here = n;
208
+ n = n.nextSibling;
209
+ if (!own.has(here)) live.removeChild(here);
210
+ }
176
211
  while (next.firstChild) live.appendChild(next.firstChild);
212
+ renew_server_body(live, next, own);
177
213
  }
178
214
  /** For each live region whose reconcile key also appears in `next`, push the incoming region's
179
215
  * props into the live island (kept regions only — a re-mounting region gets its props on mount). */
@@ -143,6 +143,7 @@ function warm_key(url) {
143
143
  function warm_island_module(entry, base, cls = "speculative") {
144
144
  const url = island_module_url(entry, base);
145
145
  if (!url) return;
146
+ if (!entry_location(entry, base) && url.includes("/ogygia/og-region.")) return;
146
147
  const key = warm_key(url);
147
148
  if (warmed_modules.has(key)) return;
148
149
  warmed_modules.add(key);
@@ -1,8 +1,8 @@
1
- import { set_resolve_sink } from "../ref.js";
1
+ import { set_resolve_sink } from "../ref-sink.js";
2
2
  // src/runtime/region-ids.ts
3
3
  /**
4
4
  * R3 OWNERSHIP: which page-scoped hub ids each region's island resolved during hydration. The
5
- * hydrate core installs a resolve-sink (ref.set_resolve_sink) around each island's sync prop +
5
+ * hydrate core installs a resolve-sink (ref-sink.ts `set_resolve_sink`) around each island's sync prop +
6
6
  * context decode that funnels ids here, keyed by the region element; the reconciler (a navigation)
7
7
  * disposes exactly the ids of REMOVED regions. WeakMap → a removed element's set is GC'd with it.
8
8
  * Its own module so the boot-time reconciler and the lazily loaded hydrate core share one map
@@ -1,4 +1,4 @@
1
- import { slots } from "./slots.js";
1
+ import { boot_link, slots } from "./slots.js";
2
2
  import { seed_query_responses } from "../shims/kit-remote/client-stub.js";
3
3
  import { clear_remote_instances, clear_remote_seeds } from "../shims/kit-remote/remote-cache.js";
4
4
  // src/runtime/remote-seeds.ts
@@ -7,12 +7,23 @@ import { clear_remote_instances, clear_remote_seeds } from "../shims/kit-remote/
7
7
  * `application/ogygia-remote` side-channel (zero-flash hydration), and clear it across SPA swaps.
8
8
  *
9
9
  * A HYDRATE-phase feature (link/runtime-entry.ts): installed by the hydrate core, never the boot —
10
- * only `seeds.ts` (hydrate core) reads the slot, and the decoder imports the APP's transport codecs
11
- * (its own `src/hooks.ts`), which reach Svelte.
10
+ * only the hydrate core (and the navigation's seeds.ts) reads the slot, and the decoder imports the
11
+ * APP's transport codecs (its own `src/hooks.ts`), which reach Svelte. Installed only in an app that
12
+ * has a remote function at all.
12
13
  */
14
+ const REMOTE_SEED_SELECTOR = "script[type=\"application/ogygia-remote\"]";
15
+ function seed_once() {
16
+ const session = boot_link().runtime_session;
17
+ if (session.remote_seeded) return;
18
+ session.mark_remote_seeded();
19
+ if (typeof document === "undefined") return;
20
+ const text = document.querySelector(REMOTE_SEED_SELECTOR)?.textContent;
21
+ if (text) seed_query_responses(text);
22
+ }
13
23
  /** Feature entry: fill the `remoteSeeds` slot. */
14
24
  function install() {
15
25
  slots.remoteSeeds = {
26
+ seed_once,
16
27
  seed_query_responses,
17
28
  clear_remote_seeds,
18
29
  clear_remote_instances
@@ -389,6 +389,18 @@ function preflight(doc, dest) {
389
389
  stamp_region_keys(doc.body);
390
390
  page_seed_of(doc);
391
391
  }
392
+ /**
393
+ * Which client build a document belongs to: its runtime script's file and its location map's
394
+ * (server/document-tail.ts `runtime_bootstrap_tags`) — both content-hashed, so a deploy that changed
395
+ * any island, Svelte or the runtime names another. By file name: a `src` written relative to its
396
+ * page reads the same from any address. Null: the document names no runtime (no island on it).
397
+ */
398
+ function build_of(d) {
399
+ const s = d.querySelector("script[data-ogygia-runtime]");
400
+ if (!s) return null;
401
+ const file = (v) => v ? v.slice(v.lastIndexOf("/") + 1) : "";
402
+ return file(s.getAttribute("src")) + " " + file(s.getAttribute("data-og-locations"));
403
+ }
392
404
  async function navigate(r, url, from, { push = true, pop_scroll = null, type = "link", replace = false } = {}) {
393
405
  if (!r.run_before(from, url, type)) {
394
406
  if (r.nav_target === url.href) r.nav_target = null;
@@ -460,6 +472,9 @@ async function navigate(r, url, from, { push = true, pop_scroll = null, type = "
460
472
  if (KitBoot.document_has(doc)) return hand_off();
461
473
  const marker = doc.querySelector("meta[name=\"ogygia-router\"]");
462
474
  if (!marker) return hand_off();
475
+ const next_build = build_of(doc);
476
+ const this_build = next_build === null ? null : build_of(document);
477
+ if (this_build !== null && next_build !== this_build) return hand_off();
463
478
  if (doc.querySelector("meta[name=\"ogygia-first-load\"]")) return hand_off();
464
479
  globalThis.__og_restore?.(doc);
465
480
  if (dest !== url) replace_state({
@@ -2,6 +2,7 @@ import { kit_hydrates_page } from "./kit-boot.js";
2
2
  import { slots } from "./slots.js";
3
3
  import { publish_nav } from "./nav-handle.js";
4
4
  import { nearest_attr } from "./nearest-attr.js";
5
+ import { record_server_body } from "./ownership.js";
5
6
  // src/runtime/router.ts
6
7
  /**
7
8
  * Minimal SPA router for ogygia (Astro's ClientRouter equivalent).
@@ -300,6 +301,8 @@ var SpaRouter = class {
300
301
  this.#started = true;
301
302
  this.doc_key = document_key(new URL(location.href));
302
303
  this.current_url = new URL(location.href);
304
+ const body = document.body;
305
+ if (body) record_server_body(body, body, Array.from(body.childNodes));
303
306
  document.addEventListener("click", (event) => {
304
307
  const anchor = anchor_of(event);
305
308
  const url = this.#should_intercept(event, anchor);
@@ -1,8 +1,14 @@
1
+ import { parse_sidecar_text } from "./wire-format.js";
1
2
  import { PageSnapshot } from "../shims/page-store.svelte.js";
2
3
  export declare const PAGE_SEED_SELECTOR = "script[type=\"application/ogygia-page\"]";
3
- /** A seed or props script's payload, in its lane (wire-format.ts): JSON when the server marked
4
- * it plain, devalue with `revivers` otherwise. Every seed / props read goes through here. */
5
- export declare function parse_sidecar_text(el: Element, revivers?: Record<string, (d: never) => unknown>): unknown;
4
+ /**
5
+ * The PAGE feature (link/runtime-entry.ts, hydrate phase): what island code sees of its page — the
6
+ * page seed behind `$app/state` / `$app/stores`, the seed references in island props, and, on a
7
+ * Kit-hydrated document, the wait for Kit's own page. Installed only when island code can reach
8
+ * Kit's world (the driver's island-closure walk, failing open on anything it cannot follow): an app
9
+ * whose islands never read their page gets no seed from the server and ships none of this.
10
+ */
11
+ export declare function install(): void;
6
12
  type PageSeed = Partial<Omit<PageSnapshot, "url"> & {
7
13
  url: string | URL;
8
14
  }>;
@@ -19,7 +25,6 @@ export declare function page_seed_of(doc: Document): PageSeed | null;
19
25
  * on first ask), the once-parsed seed for a foreign one. `undefined` when the document has no seed.
20
26
  */
21
27
  export declare function seed_data_of(doc: Document): unknown;
22
- export declare function seed_remote_once(): void;
23
28
  /** Document-level page seed (one script from ogygiaHandle) — once per document; then any page facts
24
29
  * an answer brought since. Every hydrate calls it, before the island reads `page`. */
25
30
  export declare function seed_page_once(): void;
@@ -47,4 +52,4 @@ export declare function prepare_spa_document(): void;
47
52
  * already spent (`once`) — that reuse never opens SSE again and leaves "connecting…".
48
53
  */
49
54
  export declare function finish_spa_document(): void;
50
- export {};
55
+ export { parse_sidecar_text };
@@ -1,8 +1,9 @@
1
- import { parse_wire_text, wire_is_json } from "./wire-format.js";
1
+ import { parse_sidecar_text, parse_wire_text } from "./wire-format.js";
2
2
  import { page_state, reset_page, set_page } from "../shims/page-store.svelte.js";
3
3
  import { kit_bridge } from "../shims/kit-bridge.js";
4
4
  import { install_page_defer, page_defer_revivers } from "./page-defer.js";
5
5
  import { transport_decoders } from "./app-transport.js";
6
+ import { kit_page_thread } from "./kit-page-thread.svelte.js";
6
7
  import { boot_link, slots } from "./slots.js";
7
8
  // src/runtime/seeds.ts
8
9
  /**
@@ -38,10 +39,19 @@ const session = () => boot_link().runtime_session;
38
39
  const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
39
40
  const PAGE_SEED_SELECTOR = "script[type=\"application/ogygia-page\"]";
40
41
  const REMOTE_SEED_SELECTOR = "script[type=\"application/ogygia-remote\"]";
41
- /** A seed or props script's payload, in its lane (wire-format.ts): JSON when the server marked
42
- * it plain, devalue with `revivers` otherwise. Every seed / props read goes through here. */
43
- function parse_sidecar_text(el, revivers) {
44
- return parse_wire_text(el.textContent ?? "", wire_is_json(el), revivers);
42
+ /**
43
+ * The PAGE feature (link/runtime-entry.ts, hydrate phase): what island code sees of its page — the
44
+ * page seed behind `$app/state` / `$app/stores`, the seed references in island props, and, on a
45
+ * Kit-hydrated document, the wait for Kit's own page. Installed only when island code can reach
46
+ * Kit's world (the driver's island-closure walk, failing open on anything it cannot follow): an app
47
+ * whose islands never read their page gets no seed from the server and ships none of this.
48
+ */
49
+ function install() {
50
+ slots.page = {
51
+ seed_page_once,
52
+ seed_data_of,
53
+ kit_page_thread
54
+ };
45
55
  }
46
56
  let parses = 0;
47
57
  /** Test seam: how many page-seed scripts were parsed since boot (one per document is the law). */
@@ -106,12 +116,6 @@ function apply_remote_seed_text(text) {
106
116
  if (!text) return;
107
117
  slots.remoteSeeds?.seed_query_responses(text);
108
118
  }
109
- function seed_remote_once() {
110
- if (session().remote_seeded) return;
111
- session().mark_remote_seeded();
112
- if (typeof document === "undefined") return;
113
- apply_remote_seed_text(document.querySelector(REMOTE_SEED_SELECTOR)?.textContent);
114
- }
115
119
  /** Document-level page seed (one script from ogygiaHandle) — once per document; then any page facts
116
120
  * an answer brought since. Every hydrate calls it, before the island reads `page`. */
117
121
  function seed_page_once() {
@@ -205,4 +209,4 @@ function prepare_spa_document() {
205
209
  function finish_spa_document() {
206
210
  slots.remoteSeeds?.clear_remote_instances();
207
211
  }
208
- export { PAGE_SEED_SELECTOR, apply_soft_invalidate_doc, finish_spa_document, page_seed_of, parse_sidecar_text, prepare_spa_document, seed_data_of, seed_page_once, seed_parse_count, seed_remote_once };
212
+ export { PAGE_SEED_SELECTOR, apply_soft_invalidate_doc, finish_spa_document, install, page_seed_of, parse_sidecar_text, prepare_spa_document, seed_data_of, seed_page_once, seed_parse_count };
@@ -9,7 +9,7 @@ import { regions_in_shadow } from "./connected.js";
9
9
  import { yield_task } from "./schedule.js";
10
10
  import { beacon_nav } from "./beacon.js";
11
11
  import { load_slot } from "./load-scheduler.js";
12
- import { is_self_owned, walk_enters } from "./ownership.js";
12
+ import { is_self_owned, record_server_body, server_body_attributes, server_body_child, walk_enters } from "./ownership.js";
13
13
  import { document_key, jump_to_hash, push_state, replace_state } from "./router.js";
14
14
  import { resolve_address } from "./interaction.js";
15
15
  import { sync_attributes } from "./morph.js";
@@ -53,9 +53,11 @@ export type FormOps = {
53
53
  /** Wake a cold island when interaction lands inside it; returns a disarm fn. */
54
54
  export type ArmFn = (el: HTMLElement, fire: () => void) => void | (() => void);
55
55
  /** `target: 'walk'` = the hydration repair (the server sequence Svelte's walk binds); default = a live
56
- * morph toward a new answer. Same ownership rules either way (runtime/ownership.ts). */
56
+ * morph toward a new answer. Same ownership rules either way (runtime/ownership.ts). `stays`: a
57
+ * child of `parent` the morph steps over (the page's own, beside the render's). */
57
58
  export type MorphFn = (parent: Element, nodes: ArrayLike<Node>, options?: {
58
59
  target?: "live" | "walk";
60
+ stays?: (node: Node) => boolean;
59
61
  }) => void;
60
62
  export type WireOps = {
61
63
  /** THE hub key (`OgygiaRef`) — every transportable kind crosses under it. */
@@ -64,10 +66,22 @@ export type WireOps = {
64
66
  resolve: (ref: never, remember: boolean) => unknown;
65
67
  };
66
68
  export type RemoteSeedOps = {
69
+ /** this document's remote seed into the query cache, once per document */
70
+ seed_once(): void;
67
71
  seed_query_responses(text: string): void;
68
72
  clear_remote_seeds(): void;
69
73
  clear_remote_instances(): void;
70
74
  };
75
+ /** The page as island code sees it, filled by the `page` feature (./seeds.ts). Null when no island
76
+ * code can read its page: the server ships no seed then, so there is nothing to parse or wait for. */
77
+ export type PageOps = {
78
+ /** the document's page seed into the page store (once), then any page facts answers brought */
79
+ seed_page_once(): void;
80
+ /** the `page.data` a seed reference in island props resolves against */
81
+ seed_data_of(doc: Document): unknown;
82
+ /** on a Kit-hydrated document: the wait for Kit to apply its page (`null` once applied) */
83
+ kit_page_thread(): Promise<void> | null;
84
+ };
71
85
  /**
72
86
  * The client frame store, filled by the `frames` feature. Core reads it ONLY on the deferred /
73
87
  * live / SWR-lake paths (a region with a signed `endpoint`), so a plain load-hydrated app never
@@ -137,6 +151,10 @@ export type BootLink = {
137
151
  /** DOM ownership (./ownership.ts): which children Svelte's walk enters, and self-owned elements */
138
152
  walk_enters: typeof walk_enters;
139
153
  is_self_owned: typeof is_self_owned;
154
+ /** `<body>`'s two owners (./ownership.ts): the router's record of what the server sent it */
155
+ record_server_body: typeof record_server_body;
156
+ server_body_attributes: typeof server_body_attributes;
157
+ server_body_child: typeof server_body_child;
140
158
  };
141
159
  export type RouterLink = {
142
160
  document_key: typeof document_key;
@@ -156,7 +174,11 @@ export type Slots = {
156
174
  /** a kept island's host (the live feature fills it beside `live`) */
157
175
  keep: Component<Record<string, unknown>> | null;
158
176
  wire: WireOps | null;
177
+ /** The transportable classes an island's props, context and seeds need, loaded before it
178
+ * decodes (./wire-classes.ts). Null in an app without transportable classes: nothing to load. */
179
+ wire_classes: ((region: Element) => Promise<unknown> | null) | null;
159
180
  remoteSeeds: RemoteSeedOps | null;
181
+ page: PageOps | null;
160
182
  frames: FrameOps | null;
161
183
  nav: NavOps | null;
162
184
  /**
@@ -22,7 +22,9 @@ const slots = {
22
22
  live: null,
23
23
  keep: null,
24
24
  wire: null,
25
+ wire_classes: null,
25
26
  remoteSeeds: null,
27
+ page: null,
26
28
  frames: null,
27
29
  nav: null,
28
30
  context: null,
@@ -1,3 +1,5 @@
1
+ /** Feature entry: fill the `wire_classes` slot. */
2
+ export declare function install(): void;
1
3
  /** The loads this island's props, context and page seeds need, or `null` when none (the common
2
4
  * case: an app without transportables, or an island that carries none). */
3
5
  export declare function wire_classes_for(region: Element): Promise<unknown> | null;
@@ -1,5 +1,6 @@
1
1
  import { props_sidecar_of } from "./sidecar.js";
2
2
  import { yield_task } from "./schedule.js";
3
+ import { slots } from "./slots.js";
3
4
  import { wire_all, wire_loaders } from "virtual:ogygia/transportables";
4
5
  // src/runtime/wire-classes.ts
5
6
  /**
@@ -14,7 +15,15 @@ import { wire_all, wire_loaders } from "virtual:ogygia/transportables";
14
15
  * The small texts are checked for each class's tag path (`"<path>#`). An app that sends wired values
15
16
  * through Kit's `transport` (`wire_all`: remote functions, loads) gets every class with every island,
16
17
  * as before: those values decode synchronously whenever they arrive. Each module loads once.
18
+ *
19
+ * A BOOT-phase feature (link/runtime-entry.ts), installed only when the app has transportable
20
+ * classes: without one the loader map is empty, but the bundler cannot see that through the virtual
21
+ * module, so this code shipped to every island page for a lookup that always found nothing.
17
22
  */
23
+ /** Feature entry: fill the `wire_classes` slot. */
24
+ function install() {
25
+ slots.wire_classes = wire_classes_for;
26
+ }
18
27
  const loaders = wire_loaders;
19
28
  const paths = Object.keys(loaders);
20
29
  const loading = /* @__PURE__ */ new Map();
@@ -66,4 +75,4 @@ function wire_classes_for(region) {
66
75
  for (const p of want) pending.push(load(p));
67
76
  return Promise.all(pending);
68
77
  }
69
- export { wire_classes_for };
78
+ export { install, wire_classes_for };
@@ -4,4 +4,7 @@ export declare const WIRE_FORMAT_JSON = "json";
4
4
  export declare function wire_is_json(el: Element): boolean;
5
5
  /** Parse a side-channel's text in its lane: `JSON.parse` for the JSON lane (nothing to revive by
6
6
  * construction), devalue `parse` with the caller's revivers otherwise. */
7
- export declare function parse_wire_text(text: string, json: boolean, revivers?: Record<string, (value: never) => unknown>): unknown;
7
+ export declare function parse_wire_text(text: string, json: boolean, revivers?: Record<string, (value: never) => unknown>): unknown;
8
+ /** A seed or props script's payload, in its lane: JSON when the server marked it plain, devalue
9
+ * with `revivers` otherwise. Every seed / props read goes through here. */
10
+ export declare function parse_sidecar_text(el: Element, revivers?: Record<string, (d: never) => unknown>): unknown;
@@ -19,4 +19,9 @@ function wire_is_json(el) {
19
19
  function parse_wire_text(text, json, revivers) {
20
20
  return json ? JSON.parse(text) : parse(text, revivers);
21
21
  }
22
- export { WIRE_FORMAT_ATTR, WIRE_FORMAT_JSON, parse_wire_text, wire_is_json };
22
+ /** A seed or props script's payload, in its lane: JSON when the server marked it plain, devalue
23
+ * with `revivers` otherwise. Every seed / props read goes through here. */
24
+ function parse_sidecar_text(el, revivers) {
25
+ return parse_wire_text(el.textContent ?? "", wire_is_json(el), revivers);
26
+ }
27
+ export { WIRE_FORMAT_ATTR, WIRE_FORMAT_JSON, parse_sidecar_text, parse_wire_text, wire_is_json };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The CLIENT half of seed references (seed-refs.ts): the devalue type tag and the reviver that
3
+ * resolves a reference against the page seed. Its own module because the hydrate core is a lazy
4
+ * runtime chunk that shares modules with Kit's client graph (route components compiled for the
5
+ * browser, never loaded on a csr=false page). The bundler keeps a shared module whole — every
6
+ * export ANY importer uses — so while the reviver lived beside the server-side index and planner,
7
+ * every island page downloaded the planner (~4 kB) for the Kit graph's sake. A leaf holds only
8
+ * what the hydrate core runs.
9
+ */
10
+ /** The devalue type tag. */
11
+ export declare const SEED_REF_KEY = "OgygiaSeedRef";
12
+ export type SeedPath = (string | number)[];
13
+ /** Client: the node a path points at, or `undefined` when the seed does not have it. */
14
+ export declare function resolve_seed_ref(data: unknown, path: SeedPath): unknown;
15
+ /**
16
+ * Client reviver for the devalue type: resolve BY REFERENCE. The island receives the seed's own
17
+ * node — the same object its `page.data` read would hand it, shared exactly the way `page.data`
18
+ * already is between islands. A deep copy per reference (129 of them on a measured page) bought
19
+ * nothing: island props are a snapshot either way, and the DEV mutation guard the runtime wraps
20
+ * every island's props in (`PropMutationGuard`) already warns on a write into them. `get_data` is
21
+ * called lazily, once per reviver — the first reference resolves the seed. A dangling path (no seed,
22
+ * or a seed that does not carry the node) revives to `undefined` and, in dev, says why.
23
+ */
24
+ export declare function seed_ref_reviver(get_data: () => unknown): (path: SeedPath) => unknown;
@@ -0,0 +1,44 @@
1
+ // src/seed-ref-reviver.ts
2
+ /**
3
+ * The CLIENT half of seed references (seed-refs.ts): the devalue type tag and the reviver that
4
+ * resolves a reference against the page seed. Its own module because the hydrate core is a lazy
5
+ * runtime chunk that shares modules with Kit's client graph (route components compiled for the
6
+ * browser, never loaded on a csr=false page). The bundler keeps a shared module whole — every
7
+ * export ANY importer uses — so while the reviver lived beside the server-side index and planner,
8
+ * every island page downloaded the planner (~4 kB) for the Kit graph's sake. A leaf holds only
9
+ * what the hydrate core runs.
10
+ */
11
+ /** The devalue type tag. */
12
+ const SEED_REF_KEY = "OgygiaSeedRef";
13
+ /** Client: the node a path points at, or `undefined` when the seed does not have it. */
14
+ function resolve_seed_ref(data, path) {
15
+ let cur = data;
16
+ for (const key of path) {
17
+ if (cur === null || typeof cur !== "object") return void 0;
18
+ cur = cur[key];
19
+ }
20
+ return cur;
21
+ }
22
+ /**
23
+ * Client reviver for the devalue type: resolve BY REFERENCE. The island receives the seed's own
24
+ * node — the same object its `page.data` read would hand it, shared exactly the way `page.data`
25
+ * already is between islands. A deep copy per reference (129 of them on a measured page) bought
26
+ * nothing: island props are a snapshot either way, and the DEV mutation guard the runtime wraps
27
+ * every island's props in (`PropMutationGuard`) already warns on a write into them. `get_data` is
28
+ * called lazily, once per reviver — the first reference resolves the seed. A dangling path (no seed,
29
+ * or a seed that does not carry the node) revives to `undefined` and, in dev, says why.
30
+ */
31
+ function seed_ref_reviver(get_data) {
32
+ let data;
33
+ let resolved = false;
34
+ return (path) => {
35
+ if (!resolved) {
36
+ data = get_data();
37
+ resolved = true;
38
+ }
39
+ const node = resolve_seed_ref(data, path);
40
+ if (node === void 0 && typeof console !== "undefined" && import.meta.env?.DEV) console.warn(`[ogygia] island props reference page.data at ${JSON.stringify(path)} but the page seed does not carry it — the island receives undefined there.`);
41
+ return node;
42
+ };
43
+ }
44
+ export { SEED_REF_KEY, resolve_seed_ref, seed_ref_reviver };
@@ -1,6 +1,4 @@
1
- /** The devalue type tag. */
2
- export declare const SEED_REF_KEY = "OgygiaSeedRef";
3
- export type SeedPath = (string | number)[];
1
+ import { SEED_REF_KEY, SeedPath, resolve_seed_ref, seed_ref_reviver } from "./seed-ref-reviver.js";
4
2
  export interface SeedIndex {
5
3
  /** node → its path from `page.data` (identity matches: the app passed the seed's own object).
6
4
  * The path is materialised on the first ask — an indexed node that no island references
@@ -90,16 +88,4 @@ export interface SeedRefPlan {
90
88
  * no walk of its own beyond the pointer lookups.
91
89
  */
92
90
  export declare function plan_seed_refs(index: SeedIndex, props: unknown, min_bytes?: number): SeedRefPlan;
93
- /** Client: the node a path points at, or `undefined` when the seed does not have it. */
94
- export declare function resolve_seed_ref(data: unknown, path: SeedPath): unknown;
95
- /**
96
- * Client reviver for the devalue type: resolve BY REFERENCE. The island receives the seed's own
97
- * node — the same object its `page.data` read would hand it, shared exactly the way `page.data`
98
- * already is between islands. A deep copy per reference (129 of them on a measured page) bought
99
- * nothing: island props are a snapshot either way, and the DEV mutation guard the runtime wraps
100
- * every island's props in (`PropMutationGuard`) already warns on a write into them. `get_data` is
101
- * called lazily, once per reviver — the first reference resolves the seed. A dangling path (no seed,
102
- * or a seed that does not carry the node) revives to `undefined` and, in dev, says why.
103
- */
104
- export declare function seed_ref_reviver(get_data: () => unknown): (path: SeedPath) => unknown;
105
- export {};
91
+ export { SEED_REF_KEY, type SeedPath, resolve_seed_ref, seed_ref_reviver };
package/dist/seed-refs.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fnv1a } from "./runtime/hash.js";
2
+ import { SEED_REF_KEY, resolve_seed_ref, seed_ref_reviver } from "./seed-ref-reviver.js";
2
3
  // src/seed-refs.ts
3
4
  /**
4
5
  * SEED REFERENCES — island props that point into the page seed instead of copying it.
@@ -43,8 +44,6 @@ import { fnv1a } from "./runtime/hash.js";
43
44
  * Universal module (no Node imports): the index + reducer run on the server, the resolver on the
44
45
  * client, and the unit tests exercise both ends against a real devalue round trip.
45
46
  */
46
- /** The devalue type tag. */
47
- const SEED_REF_KEY = "OgygiaSeedRef";
48
47
  /** A node the codec may reference: a plain object or an array (own prototype only). */
49
48
  function is_plain(v) {
50
49
  if (v === null || typeof v !== "object") return false;
@@ -453,35 +452,4 @@ function plan_seed_refs(index, props, min_bytes = 96) {
453
452
  keys
454
453
  };
455
454
  }
456
- /** Client: the node a path points at, or `undefined` when the seed does not have it. */
457
- function resolve_seed_ref(data, path) {
458
- let cur = data;
459
- for (const key of path) {
460
- if (cur === null || typeof cur !== "object") return void 0;
461
- cur = cur[key];
462
- }
463
- return cur;
464
- }
465
- /**
466
- * Client reviver for the devalue type: resolve BY REFERENCE. The island receives the seed's own
467
- * node — the same object its `page.data` read would hand it, shared exactly the way `page.data`
468
- * already is between islands. A deep copy per reference (129 of them on a measured page) bought
469
- * nothing: island props are a snapshot either way, and the DEV mutation guard the runtime wraps
470
- * every island's props in (`PropMutationGuard`) already warns on a write into them. `get_data` is
471
- * called lazily, once per reviver — the first reference resolves the seed. A dangling path (no seed,
472
- * or a seed that does not carry the node) revives to `undefined` and, in dev, says why.
473
- */
474
- function seed_ref_reviver(get_data) {
475
- let data;
476
- let resolved = false;
477
- return (path) => {
478
- if (!resolved) {
479
- data = get_data();
480
- resolved = true;
481
- }
482
- const node = resolve_seed_ref(data, path);
483
- if (node === void 0 && typeof console !== "undefined" && import.meta.env?.DEV) console.warn(`[ogygia] island props reference page.data at ${JSON.stringify(path)} but the page seed does not carry it — the island receives undefined there.`);
484
- return node;
485
- };
486
- }
487
455
  export { SEED_REF_KEY, analyze, deep_equal_plain, index_seed, json_culprit, plan_seed_refs, resolve_seed_ref, seed_ref_reviver, set_measure_memo_reader };
@@ -41,9 +41,10 @@ export declare function modulepreload_tag(href: string): string;
41
41
  * module's imports after downloading and parsing it; hinted here, they download ALONGSIDE the
42
42
  * runtime instead of one round trip after it. Normal priority — the runtime cannot run without
43
43
  * them. `data-ogygia-runtime-dep` keeps them attached to the script when the handle moves it
44
- * (head-presence.ts `runtime_first`).
44
+ * (head-presence.ts `runtime_first`). `locations`: the build's location map, which the runtime reads
45
+ * for a load that arrives with an identity alone (runtime/entry-locations.ts).
45
46
  */
46
- export declare function runtime_bootstrap_tags(src: string, deps: readonly string[]): string;
47
+ export declare function runtime_bootstrap_tags(src: string, deps: readonly string[], locations?: string | null): string;
47
48
  /**
48
49
  * The island graph (island-graph.ts) as one inert JSON script: entry → the chunks its code needs.
49
50
  * The runtime turns an entry's list into modulepreload links when that island wakes. Empty map →
@@ -33,10 +33,11 @@ function modulepreload_tag(href) {
33
33
  * module's imports after downloading and parsing it; hinted here, they download ALONGSIDE the
34
34
  * runtime instead of one round trip after it. Normal priority — the runtime cannot run without
35
35
  * them. `data-ogygia-runtime-dep` keeps them attached to the script when the handle moves it
36
- * (head-presence.ts `runtime_first`).
36
+ * (head-presence.ts `runtime_first`). `locations`: the build's location map, which the runtime reads
37
+ * for a load that arrives with an identity alone (runtime/entry-locations.ts).
37
38
  */
38
- function runtime_bootstrap_tags(src, deps) {
39
- let html = "<script type=\"module\" data-ogygia-runtime src=\"" + src + "\"><\/script>";
39
+ function runtime_bootstrap_tags(src, deps, locations = null) {
40
+ let html = "<script type=\"module\" data-ogygia-runtime" + (locations ? " data-og-locations=\"" + locations + "\"" : "") + " src=\"" + src + "\"><\/script>";
40
41
  for (const dep of deps) html += "<link rel=\"modulepreload\" href=\"" + dep + "\" data-ogygia-runtime-dep>";
41
42
  return html;
42
43
  }