@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,16 +1,14 @@
1
1
  import { frameAddress } from "../frame.js";
2
2
  import { kit_hydrates_page } from "./kit-boot.js";
3
3
  import { init_shadow_registries, parse_region_html, restore_adopt_sheets, restore_markup } from "./parse-html.js";
4
- import { wire_classes_for } from "./wire-classes.js";
5
4
  import { mark_region_changed, region_changed, unwatch_region, watch_region } from "./drift-watch.js";
6
5
  import { runtime_session } from "./session.js";
7
6
  import { RegionAnswerRefused, capability_expired, deny_of, is_allowed_region_endpoint, is_document_answer, is_redirected_answer, is_same_origin_response, island_module_url, renewal_url } from "./region-endpoint-url.js";
8
- import { import_entry, island_entry_of, on_entry_fallback, pin_region_urls } from "./entry-locations.js";
7
+ import { import_entry, island_entry_of, on_entry_fallback, pin_build_map, pin_region_urls } from "./entry-locations.js";
9
8
  import { is_awake, is_deferred, is_frozen, ours_on_kit_document, phase2_hydrate_schedule, region_hydrate_schedule, region_schedule } from "./region-attrs.js";
10
9
  import { slots } from "./slots.js";
11
10
  import { KEEP_FALLBACK_HTML } from "../keep-fallback-marker.js";
12
11
  import { PAGE_FACTS_SELECTOR } from "../page-facts-script.js";
13
- import { NAV_HANDLE_KEY, mpa_nav, publish_nav } from "./nav-handle.js";
14
12
  import { link_boot } from "./boot-link.js";
15
13
  import { background_start, hydrate_settled, hydrate_started, hydrate_turn, register_region, unregister_region, yield_task } from "./schedule.js";
16
14
  import { once_visible } from "./observe.js";
@@ -24,6 +22,7 @@ import { on_svelte_warning, set_hydrating, tap_svelte_warnings } from "./svelte-
24
22
  import { emit } from "../events/bus.js";
25
23
  import { ingest_server_events, install_window_sink } from "../events/sinks.js";
26
24
  import { CLIENT_PLUGINS, boot_client_plugins, client_region_mount, client_region_unmount } from "./client-plugins.js";
25
+ import { FRAMES } from "virtual:ogygia/runtime-flags";
27
26
  // src/runtime/core.ts
28
27
  const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
29
28
  const BEACON = typeof __OGYGIA_BEACON__ !== "undefined" ? __OGYGIA_BEACON__ : true;
@@ -95,7 +94,7 @@ const ON_DEMAND_EVENTS = [
95
94
  const ON_DEMAND_SELECTOR = "ogygia-region[render=\"defer\"][when=\"interaction\"]";
96
95
  /**
97
96
  * ON-DEMAND holes, delegated: ONE set of capture listeners on the document for every armed hole
98
- * (five listeners per hole used to be five hundred on a page of mega-menus). Intent lands anywhere
97
+ * (five listeners per hole used to be five hundred on a page of nav-menus). Intent lands anywhere
99
98
  * inside a hole; the nearest ARMED hole above the target fires once and is forgotten.
100
99
  */
101
100
  const on_demand_armed = /* @__PURE__ */ new Map();
@@ -353,7 +352,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
353
352
  connected_regions.add(this);
354
353
  pin_region_urls(this);
355
354
  if (this.hasAttribute("live")) return;
356
- if (this.#minted_endpoint === null && is_deferred(this)) {
355
+ if (FRAMES && this.#minted_endpoint === null && is_deferred(this)) {
357
356
  const minted = this.getAttribute("endpoint");
358
357
  if (minted) this.#minted_endpoint = minted;
359
358
  else {
@@ -427,6 +426,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
427
426
  * nothing left to warm.
428
427
  */
429
428
  #prefetch_html() {
429
+ if (!FRAMES) return;
430
430
  if (this.#done || this.#fetching || !this.isConnected) return;
431
431
  const endpoint = this.#endpoint();
432
432
  if (!endpoint || !is_allowed_region_endpoint(endpoint)) return;
@@ -592,6 +592,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
592
592
  * island), schedule phase-2 hydrate — coalescing matching schedules to immediate load.
593
593
  */
594
594
  async #server() {
595
+ if (!FRAMES) return;
595
596
  const endpoint = this.#endpoint();
596
597
  if (endpoint && !this.#frame_unsub) {
597
598
  const address = this.#frame_address = frameAddress(endpoint);
@@ -718,6 +719,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
718
719
  * not (the endpoint answers `cache-control: private, max-age=30`, and stale is the whole point).
719
720
  */
720
721
  async #fetch_html(opts = {}) {
722
+ if (!FRAMES) return;
721
723
  if (this.#fetching || this.#done && !opts.revalidate) return;
722
724
  const endpoint = this.#endpoint();
723
725
  if (!endpoint) return;
@@ -879,7 +881,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
879
881
  [mod, core] = await Promise.all([
880
882
  load_island(entry, cls()),
881
883
  hydrate_core(),
882
- wire_classes_for(this)
884
+ slots.wire_classes?.(this)
883
885
  ]);
884
886
  } finally {
885
887
  release();
@@ -952,6 +954,7 @@ var OgygiaRegion = class OgygiaRegion extends HTMLElement {
952
954
  * - **Later tick, module changed:** unmount, swap, re-hydrate.
953
955
  */
954
956
  async applyLive(desc) {
957
+ if (!FRAMES) return;
955
958
  this.setAttribute("render", "defer");
956
959
  this.setAttribute("entry", desc.module || desc.id);
957
960
  if (desc.url) this.setAttribute("endpoint", desc.url);
@@ -1122,10 +1125,10 @@ function apply_dev_head_region_css() {
1122
1125
  });
1123
1126
  }
1124
1127
  function boot(installers = [], client_plugins = []) {
1125
- if (typeof document !== "undefined" && !(NAV_HANDLE_KEY in globalThis)) publish_nav(mpa_nav());
1126
1128
  runtime_session.adopt_document_head();
1127
1129
  link_boot();
1128
1130
  for (const install of installers) install();
1131
+ pin_build_map();
1129
1132
  on_entry_fallback((report) => {
1130
1133
  if (DEVTOOLS) emit({
1131
1134
  domain: "runtime",
@@ -11,6 +11,13 @@ export declare function on_entry_fallback(reporter: (report: EntryFallbackReport
11
11
  export declare function note_entry_location(entry: string | null | undefined, src: string | null | undefined, base?: string): void;
12
12
  /** The known location of an entry, or undefined. */
13
13
  export declare function entry_location(entry: string, base?: string): string | undefined;
14
+ /**
15
+ * Pin the build map's address to the page that wrote it, as `pin_region_urls` pins a region's: with
16
+ * Kit's default `paths.relative` it is written relative to that page, and it is read lazily — after a
17
+ * client navigation to another depth, resolved against the new address, it named a file that is not
18
+ * there. The runtime calls this at boot, before any navigation.
19
+ */
20
+ export declare function pin_build_map(): void;
14
21
  /**
15
22
  * PIN A REGION'S URLS TO THE PAGE THAT WROTE THEM. With Kit's default `paths.relative`, the server
16
23
  * writes `entry` / `src` / `endpoint` relative to the page it rendered (`../../_app/immutable/…`,
@@ -26,8 +33,8 @@ export declare function pin_region_urls(el: Element, base?: string): void;
26
33
  /** An island element's identity, its location noted on the way (the one read every loader uses). */
27
34
  export declare function island_entry_of(el: Element): string | null;
28
35
  /**
29
- * Import an entry: its location when known, else `url` (what the caller resolved for the identity);
30
- * the location failing, once, the identity fetched fresh — so an island on a page that outlived its
31
- * build still wakes, on the current code. No location known → the error stands.
36
+ * Import an entry: its location when known (else this build's map names it, once read); failing that,
37
+ * or with none anywhere, the CURRENT build's location — so an island on a page that outlived its build
38
+ * still wakes, on the current code. `url`: what the caller resolved for the identity.
32
39
  */
33
40
  export declare function import_entry<T>(entry: string, url?: string): Promise<T>;
@@ -6,9 +6,9 @@
6
6
  * fingerprint, the devtools' names). The file to import is its LOCATION — content-hashed, so an
7
7
  * `immutable` cache never serves a stale one. The server writes it beside the identity: the region
8
8
  * element's `src`, and the island graph's `s` map for an entry no element carries (a portable
9
- * snippet's). Every loader resolves through `island_module_url` (region-endpoint-url.ts), which asks
10
- * here first: the location once one is known, else the identity (the dev server, or a page from
11
- * before hashing — whose stable name is a shim that reaches the current build).
9
+ * snippet's), and the build's location maps for the rest (`import_entry`). Every loader resolves
10
+ * through `island_module_url` (region-endpoint-url.ts), which asks here first: the location once one
11
+ * is known, else the identity (on the dev server, the module itself; in a build, a name, no file).
12
12
  */
13
13
  /** absolute identity → absolute location */
14
14
  const locations = /* @__PURE__ */ new Map();
@@ -43,6 +43,48 @@ function note_entry_location(entry, src, base) {
43
43
  function entry_location(entry, base) {
44
44
  return locations.size ? locations.get(absolute(entry, base)) : void 0;
45
45
  }
46
+ /**
47
+ * THIS BUILD'S LOCATION MAP (compiler/link/location-map.ts, the content-hashed one): every island entry's
48
+ * location by identity, for a load that arrives with an identity alone — a region the browser
49
+ * rendered, a held region from the wire, a snippet whose host carried no graph. The runtime's script
50
+ * names it (`data-og-locations`); it is read once, on the first such load. Content-hashed, so no
51
+ * cache serves it stale. It only fills gaps: a location a page wrote (a later build's, after a
52
+ * navigation) always wins over the map of the build the runtime came from. No map, or it failed
53
+ * (the dev server, a page that outlived its build): the identity is loaded, as before. A failed read
54
+ * is not kept: the next identity-only load asks again (a blip must not cost the page every later one).
55
+ */
56
+ const BUILD_MAP_ATTR = "data-og-locations";
57
+ let build_map = null;
58
+ /**
59
+ * Pin the build map's address to the page that wrote it, as `pin_region_urls` pins a region's: with
60
+ * Kit's default `paths.relative` it is written relative to that page, and it is read lazily — after a
61
+ * client navigation to another depth, resolved against the new address, it named a file that is not
62
+ * there. The runtime calls this at boot, before any navigation.
63
+ */
64
+ function pin_build_map() {
65
+ if (typeof document === "undefined") return;
66
+ const script = document.querySelector(`script[${BUILD_MAP_ATTR}]`);
67
+ const href = script?.getAttribute(BUILD_MAP_ATTR);
68
+ if (script && href) script.setAttribute(BUILD_MAP_ATTR, absolute(href));
69
+ }
70
+ function read_build_map() {
71
+ if (build_map) return build_map;
72
+ if (typeof document === "undefined") return null;
73
+ const href = document.querySelector(`script[${BUILD_MAP_ATTR}]`)?.getAttribute(BUILD_MAP_ATTR);
74
+ if (!href) return null;
75
+ const url = absolute(href);
76
+ const read = fetch(url).then((res) => res.ok ? res.json() : null).catch(() => null).then((map) => {
77
+ if (!map) {
78
+ if (build_map === read) build_map = null;
79
+ return;
80
+ }
81
+ for (const stable in map) {
82
+ const identity = absolute(stable, url);
83
+ if (!locations.has(identity)) locations.set(identity, absolute(map[stable], url));
84
+ }
85
+ });
86
+ return build_map = read;
87
+ }
46
88
  /** The region attributes that hold a URL: its identity, its location, its hole's endpoint. */
47
89
  const REGION_URL_ATTRS = [
48
90
  "entry",
@@ -80,32 +122,101 @@ function island_entry_of(el) {
80
122
  return entry;
81
123
  }
82
124
  /**
83
- * The identity itself, past every cache: what a load falls back to when its location failed (a page
84
- * a cache kept outlived the build that made it, and that build's files are gone). The stable name
85
- * is a shim of the CURRENT build's file; the query makes the browser fetch it fresh rather than the
86
- * copy an `immutable` cache may hold. Null when no location was known (nothing to fall back to).
125
+ * THE CURRENT BUILD'S MAP (compiler/link/location-map.ts): the one file at a fixed name, in the app
126
+ * dir of the identities it maps (`<appDir>/ogygia.json`), so any app's is found from an identity alone.
127
+ * A page that outlived its build reads it when its own files are gone (or it never named a map) and
128
+ * wakes on the current code. Revalidated (a host serves it as any changing file; `no-cache` past one
129
+ * that kept it anyway), read once per document per app. Only an ogygia identity has one: the dev
130
+ * server's module URLs never ask.
87
131
  */
88
- function fresh_identity_url(entry, base) {
89
- if (!entry_location(entry, base)) return null;
90
- const identity = absolute(entry, base);
91
- return identity + (identity.includes("?") ? "&" : "?") + "og-fresh=" + Date.now().toString(36);
132
+ const current_maps = /* @__PURE__ */ new Map();
133
+ /** A build's island identity (`…/<appDir>/ogygia/og-region.<id>.js`): a name, no file. */
134
+ function is_build_identity(entry) {
135
+ return absolute(entry).includes("/ogygia/og-region.");
92
136
  }
137
+ /** The current map's file for an identity (`<appDir>/ogygia.json`), or null: not a build's identity. */
138
+ function current_map_file(identity) {
139
+ const dir = identity.lastIndexOf("/ogygia/og-region.");
140
+ return dir === -1 ? null : identity.slice(0, dir) + "/ogygia.json";
141
+ }
142
+ /** The current build's location for `entry`: null when its map has none, undefined when the map
143
+ * could not be read (that read is not kept: the next ask reads again). */
144
+ async function current_location(entry) {
145
+ const identity = absolute(entry);
146
+ const file = current_map_file(identity);
147
+ if (!file) return null;
148
+ let map = current_maps.get(file);
149
+ if (!map) {
150
+ map = fetch(file, { cache: "no-cache" }).then((res) => res.ok ? res.json() : null).catch(() => null);
151
+ current_maps.set(file, map);
152
+ }
153
+ const pairs = await map;
154
+ if (!pairs) {
155
+ forget_current_map(file, map);
156
+ return;
157
+ }
158
+ for (const name in pairs) if (absolute(name, file) === identity) return absolute(pairs[name], file);
159
+ return null;
160
+ }
161
+ function forget_current_map(file, map = current_maps.get(file)) {
162
+ if (current_maps.get(file) === map) current_maps.delete(file);
163
+ }
164
+ /** The host says this file is not there (it does not say so of a file that failed another way). */
165
+ function is_gone(url) {
166
+ return fetch(url, {
167
+ method: "HEAD",
168
+ cache: "no-store"
169
+ }).then((res) => res.status === 404 || res.status === 410, () => false);
170
+ }
171
+ /** How long a recovery waits before each ask of the current map: a rollover settles in seconds. */
172
+ const RECOVERY_WAITS = [
173
+ 0,
174
+ 1e3,
175
+ 3e3,
176
+ 9e3
177
+ ];
93
178
  /**
94
- * Import an entry: its location when known, else `url` (what the caller resolved for the identity);
95
- * the location failing, once, the identity fetched fresh — so an island on a page that outlived its
96
- * build still wakes, on the current code. No location known → the error stands.
179
+ * The current build's file for a build's identity whose file just failed to load. A deploy's rollover
180
+ * is exactly when this is asked, so an answer that cannot be the current one is asked again while it
181
+ * settles: a map that could not be read (a 404, a 5xx), and a map that names the very file that just
182
+ * failed when the host says that file is gone — an edge still answering with the old build's map. A
183
+ * file that failed some other way (the network, a broken module) is not asked about again.
184
+ */
185
+ async function recover_location(entry, failed) {
186
+ let current;
187
+ for (const wait of RECOVERY_WAITS) {
188
+ if (wait) await new Promise((resolve) => setTimeout(resolve, wait));
189
+ current = await current_location(entry);
190
+ if (current === void 0) continue;
191
+ if (current !== failed || !await is_gone(failed)) break;
192
+ forget_current_map(current_map_file(absolute(entry)));
193
+ }
194
+ return current ?? null;
195
+ }
196
+ /**
197
+ * Import an entry: its location when known (else this build's map names it, once read); failing that,
198
+ * or with none anywhere, the CURRENT build's location — so an island on a page that outlived its build
199
+ * still wakes, on the current code. `url`: what the caller resolved for the identity.
97
200
  */
98
201
  function import_entry(entry, url = entry_location(entry) ?? entry) {
99
- const loaded = import(
202
+ if (entry_location(entry)) return load_entry(entry, url);
203
+ return (read_build_map() ?? Promise.resolve()).then(async () => {
204
+ const located = entry_location(entry);
205
+ if (located) return load_entry(entry, located);
206
+ return load_entry(entry, await current_location(entry) || url);
207
+ });
208
+ }
209
+ function load_entry(entry, url) {
210
+ return import(
100
211
  /* @vite-ignore */
101
212
  url
102
- );
103
- const fresh = fresh_identity_url(entry);
104
- if (!fresh) return loaded;
105
- return loaded.catch(() => {
213
+ ).catch(async (error) => {
214
+ const failed = absolute(url);
215
+ const current = is_build_identity(entry) ? await recover_location(entry, failed) : absolute(entry);
216
+ if (!current || current === failed) throw error;
106
217
  const retry = import(
107
218
  /* @vite-ignore */
108
- fresh
219
+ current
109
220
  );
110
221
  const report = (recovered) => fallback_reporter?.({
111
222
  entry,
@@ -116,4 +227,4 @@ function import_entry(entry, url = entry_location(entry) ?? entry) {
116
227
  return retry;
117
228
  });
118
229
  }
119
- export { entry_location, import_entry, island_entry_of, note_entry_location, on_entry_fallback, pin_region_urls };
230
+ export { entry_location, import_entry, island_entry_of, note_entry_location, on_entry_fallback, pin_build_map, pin_region_urls };
@@ -5,13 +5,15 @@ import { install as install$2 } from "./morph.js";
5
5
  import { install as install$3 } from "./interaction.js";
6
6
  import { install as install$4 } from "./form-continuity.js";
7
7
  import { install as install$5 } from "./router.js";
8
+ import { install as install$6 } from "./wire-classes.js";
9
+ import { install as install$7 } from "./nav-handle.js";
8
10
  // src/runtime/full.ts
9
11
  /**
10
12
  * Kitchen-sink browser runtime — every BOOT-phase feature installed. The dev / no-marks entry: the
11
13
  * ogygia compiler injects `bootDev()` (see the plugin's `virtual:ogygia-runtime`); a per-app production
12
14
  * build instead emits a generated entry (see `vite/runtime-entry.ts`) that boots only the features it
13
15
  * uses. Both boot through {@link ./core.js core} in the same {@link ../vite/runtime-entry.js FEATURE_ORDER}.
14
- * HYDRATE-phase features (context, live, wire, remote-seeds) are never in the boot, dev included: the
16
+ * HYDRATE-phase features (context, live, wire, remote-seeds, page) are never in the boot, dev included: the
15
17
  * hydrate core installs them from `virtual:ogygia/hydrate-features` (every one, in dev).
16
18
  *
17
19
  * Exposed as an explicit FUNCTION, not a top-level side effect: the compiler always calls it, so the
@@ -21,11 +23,13 @@ import { install as install$5 } from "./router.js";
21
23
  * Idempotent via {@link ./core.js boot}. */
22
24
  function bootDev(client_plugins = []) {
23
25
  boot([
26
+ install$6,
24
27
  install,
25
28
  install$1,
26
29
  install$2,
27
30
  install$3,
28
31
  install$4,
32
+ install$7,
29
33
  install$5
30
34
  ], client_plugins);
31
35
  }
@@ -1,6 +1,7 @@
1
- import { seed_page_once, seed_remote_once } from "./seeds.js";
2
- import { kit_page_thread } from "./kit-page-thread.svelte.js";
3
1
  import { Component } from "svelte";
2
+ export declare const seed_page_once: () => void;
3
+ export declare const seed_remote_once: () => void;
4
+ export declare const kit_page_thread: () => Promise<void> | null;
4
5
  /** A hydrated island as the element holds it: how to tear it down, and (keep / live hosts) how to
5
6
  * push the next page's props into it. */
6
7
  export type IslandHandle = {
@@ -97,5 +98,4 @@ export declare function hydrate_island(region: HTMLElement, entry: string, mod:
97
98
  * HTML comes from svelte `render()` (both envelope layers) — same as the deferred swap path, so it
98
99
  * is NOT wrapped in extra `[..]` hydration comments. `null` when the live feature is absent.
99
100
  */
100
- export declare function hydrate_live(region: HTMLElement, entry: string, mod: IslandModule, props: Record<string, unknown>): IslandHandle | null;
101
- export { kit_page_thread, seed_page_once, seed_remote_once };
101
+ export declare function hydrate_live(region: HTMLElement, entry: string, mod: IslandModule, props: Record<string, unknown>): IslandHandle | null;
@@ -1,13 +1,12 @@
1
1
  import { set_current_region, set_foreign_hydrate } from "../current-region.js";
2
2
  import { capture_region_ids } from "./region-ids.js";
3
3
  import { foreign_region_prop_revivers } from "./foreign-props.js";
4
- import { SEED_REF_KEY, seed_ref_reviver } from "../seed-refs.js";
5
- import { parse_sidecar_text, seed_data_of, seed_page_once, seed_remote_once } from "./seeds.js";
4
+ import { SEED_REF_KEY, seed_ref_reviver } from "../seed-ref-reviver.js";
5
+ import { parse_sidecar_text } from "./wire-format.js";
6
6
  import { is_deferred, ours_on_kit_document, region_ssr_truncated } from "./region-attrs.js";
7
7
  import { boot_link, slots } from "./slots.js";
8
8
  import { keep_host_classes, note_host_classes } from "./host-classes.js";
9
9
  import { emit } from "../events/bus.js";
10
- import { kit_page_thread } from "./kit-page-thread.svelte.js";
11
10
  import { hydrate, unmount } from "svelte";
12
11
  import NestedProvider from "../NestedProvider.svelte";
13
12
  import { install } from "virtual:ogygia/hydrate-features";
@@ -24,6 +23,9 @@ import { install } from "virtual:ogygia/hydrate-features";
24
23
  * Stateless towards the element: the element owns its handle (`IslandHandle`) and its flags; this
25
24
  * module owns the how.
26
25
  */
26
+ const seed_page_once = () => slots.page?.seed_page_once();
27
+ const seed_remote_once = () => slots.remoteSeeds?.seed_once();
28
+ const kit_page_thread = () => slots.page?.kit_page_thread() ?? null;
27
29
  install();
28
30
  const kit_hydrates_page = () => boot_link().kit_hydrates_page();
29
31
  const props_sidecar_of = (region) => boot_link().props_sidecar_of(region);
@@ -335,9 +337,10 @@ function read_region_props(region, foreign = false) {
335
337
  if (!sidecar) return {};
336
338
  const base = foreign ? foreign_region_prop_revivers() : region_prop_revivers();
337
339
  const doc = sidecar.ownerDocument;
338
- const revivers = foreign ? base : {
340
+ const page = slots.page;
341
+ const revivers = foreign || !page ? base : {
339
342
  ...base,
340
- [SEED_REF_KEY]: seed_ref_reviver(() => seed_data_of(doc))
343
+ [SEED_REF_KEY]: seed_ref_reviver(() => page.seed_data_of(doc))
341
344
  };
342
345
  return parse_sidecar_text(sidecar, revivers);
343
346
  }
@@ -35,6 +35,13 @@ export declare function morph_children(parent: Element, new_nodes: ArrayLike<Nod
35
35
  */
36
36
  export interface MorphOptions {
37
37
  target?: "live" | "walk";
38
+ /**
39
+ * A child of the ROOT that is not the render's (the router's body reconcile: a node a script put
40
+ * in `<body>`, such as a portal's mount anchor; ownership.ts `server_body_child`). The morph steps
41
+ * over it: never matched, never moved, never removed. The render's children around it reconcile
42
+ * as usual. Root level only; below the root the ownership rules decide.
43
+ */
44
+ stays?: (node: Node) => boolean;
38
45
  }
39
46
  /** Add + update + remove attributes so `from` matches `to` exactly. Boolean attrs are attr presence.
40
47
  * `keep_extra` (a self-owned element, {@link is_self_owned}): add + update only — an attribute the
@@ -95,7 +95,7 @@ function morph_children(parent, new_nodes, options) {
95
95
  const owner = owner_of(parent);
96
96
  if (owner !== "walk" && owner !== "region") return;
97
97
  }
98
- reconcile(parent, new_nodes, build_id_sets(parent, new_nodes), walk);
98
+ reconcile(parent, new_nodes, build_id_sets(parent, new_nodes), walk, options?.stays);
99
99
  clear_aria_hidden_over_focus(parent);
100
100
  }
101
101
  /**
@@ -191,18 +191,24 @@ function reserved_for_later(sets, new_inner, cursor, idx) {
191
191
  * ({@link reconcile_by_identity}). Toward Svelte's walk, positions are always the render's (the walk
192
192
  * binds by position).
193
193
  */
194
- function reconcile(parent, new_nodes, sets, walk) {
194
+ function reconcile(parent, new_nodes, sets, walk, stays) {
195
195
  if (!walk && order_owner(parent) === "element") reconcile_by_identity(parent, new_nodes, sets);
196
- else reconcile_children(parent, new_nodes, sets, walk);
196
+ else reconcile_children(parent, new_nodes, sets, walk, stays);
197
197
  }
198
198
  /**
199
199
  * The render's sequence, by position.
200
200
  * @param walk the {@link MorphOptions.target} is Svelte's walk (the repair), threaded unchanged down
201
201
  * the recursion — a subtree the walk never enters is skipped.
202
+ * @param stays the root's children that are not the render's ({@link MorphOptions.stays}): the
203
+ * cursor steps over them, so they are never matched, moved or removed. Not threaded down.
202
204
  */
203
- function reconcile_children(parent, new_nodes, sets, walk = false) {
205
+ function reconcile_children(parent, new_nodes, sets, walk = false, stays) {
204
206
  const count = new_nodes.length;
205
- let cursor = parent.firstChild;
207
+ const step = (n) => {
208
+ if (stays) while (n && stays(n)) n = n.nextSibling;
209
+ return n;
210
+ };
211
+ let cursor = step(parent.firstChild);
206
212
  let idx = 0;
207
213
  while (idx < count && cursor) {
208
214
  const next = new_nodes[idx];
@@ -213,11 +219,11 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
213
219
  if (!same_node(cursor, next)) break;
214
220
  if (sets !== null && !id_sets_agree(sets, cursor, next)) break;
215
221
  const here = cursor;
216
- cursor = cursor.nextSibling;
222
+ cursor = step(cursor.nextSibling);
217
223
  morph_same(here, next, sets, walk);
218
224
  } else {
219
225
  const here = cursor;
220
- cursor = cursor.nextSibling;
226
+ cursor = step(cursor.nextSibling);
221
227
  morph_node(here, next, sets, walk);
222
228
  }
223
229
  idx++;
@@ -225,7 +231,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
225
231
  if (idx >= count) {
226
232
  while (cursor) {
227
233
  const gone = cursor;
228
- cursor = cursor.nextSibling;
234
+ cursor = step(cursor.nextSibling);
229
235
  parent.removeChild(gone);
230
236
  }
231
237
  return;
@@ -236,7 +242,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
236
242
  }
237
243
  let old_keys = null;
238
244
  let old_inner = null;
239
- for (let n = cursor; n; n = n.nextSibling) {
245
+ for (let n = cursor; n; n = step(n.nextSibling)) {
240
246
  const k = key_of(n);
241
247
  if (k !== null) (old_keys ??= /* @__PURE__ */ new Map()).set(k, n);
242
248
  else if (sets !== null && n.nodeType === ELEMENT) {
@@ -260,7 +266,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
260
266
  const next = new_nodes[idx];
261
267
  if (cursor && key_of(next) === null && same_node(cursor, next)) {
262
268
  const here = cursor;
263
- cursor = cursor.nextSibling;
269
+ cursor = step(cursor.nextSibling);
264
270
  morph_same(here, next, sets, walk);
265
271
  } else parent.insertBefore(clone(next), cursor);
266
272
  }
@@ -271,7 +277,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
271
277
  const matched = old_keys?.get(next_key);
272
278
  if (matched !== void 0) {
273
279
  old_keys.delete(next_key);
274
- if (matched === cursor) cursor = cursor.nextSibling;
280
+ if (matched === cursor) cursor = step(cursor.nextSibling);
275
281
  else parent.insertBefore(matched, cursor);
276
282
  morph_node(matched, next, sets, walk);
277
283
  continue;
@@ -281,7 +287,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
281
287
  const holder = wanted === void 0 ? null : find_holder(old_inner, wanted, next);
282
288
  if (holder !== null) {
283
289
  release_holder(old_inner, sets, holder);
284
- if (holder === cursor) cursor = cursor.nextSibling;
290
+ if (holder === cursor) cursor = step(cursor.nextSibling);
285
291
  else parent.insertBefore(holder, cursor);
286
292
  morph_same(holder, next, sets, walk);
287
293
  continue;
@@ -291,11 +297,11 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
291
297
  while (cursor) {
292
298
  cur_key = key_of(cursor);
293
299
  if (cur_key === null || old_keys === null || !old_keys.has(cur_key)) break;
294
- cursor = cursor.nextSibling;
300
+ cursor = step(cursor.nextSibling);
295
301
  }
296
302
  if (cursor && next_key === null && cur_key === null && same_node(cursor, next) && (old_inner === null || !reserved_for_later(sets, new_inner, cursor, idx))) {
297
303
  const here = cursor;
298
- cursor = cursor.nextSibling;
304
+ cursor = step(cursor.nextSibling);
299
305
  if (old_inner !== null) release_holder(old_inner, sets, here);
300
306
  morph_same(here, next, sets, walk);
301
307
  continue;
@@ -304,7 +310,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
304
310
  }
305
311
  while (cursor) {
306
312
  const gone = cursor;
307
- cursor = cursor.nextSibling;
313
+ cursor = step(cursor.nextSibling);
308
314
  parent.removeChild(gone);
309
315
  }
310
316
  if (old_keys) {
@@ -16,6 +16,9 @@ export type NavHandle = {
16
16
  afterNavigate(fn: AfterNavigateCallback): () => void;
17
17
  bust_page_cache(): void;
18
18
  };
19
+ /** Feature entry (`mpa-nav`, boot phase): the MPA handle, first — the router's install, later in
20
+ * the feature order, replaces it. */
21
+ export declare function install(): void;
19
22
  /** Publish the document's navigation handle (the router's replaces the MPA one). */
20
23
  export declare function publish_nav(handle: NavHandle): void;
21
24
  /**
@@ -13,16 +13,23 @@ import { speculate_url } from "./speculate-hint.js";
13
13
  * from the global realm (the same pattern as the Kit page bridge, `Symbol.for('ogygia.kit-page')`),
14
14
  * and `test/runtime-boot-svelte-free.test.ts` pins that no island-side module imports a boot module.
15
15
  *
16
- * Who publishes it, always, on a document ogygia boots:
16
+ * Who publishes it, on a document ogygia boots:
17
17
  * - the ROUTER feature (./router.ts `install`) — the SPA router's API;
18
- * - otherwise core (`boot`) publishes {@link mpa_nav}: the browser navigates, preloads become
19
- * Speculation Rules hints, and `invalidateAll` is still the soft seed refresh (./router-nav.ts,
20
- * loaded on first use) — exactly what `router: false` apps got before.
18
+ * - otherwise the `mpa-nav` feature ({@link install}) publishes {@link mpa_nav}: the browser
19
+ * navigates, preloads become Speculation Rules hints, and `invalidateAll` is still the soft seed
20
+ * refresh (./router-nav.ts, loaded on first use) — exactly what `router: false` apps got before.
21
+ * Installed only when island code can reach the handle (the `page` mark: `$app/navigation`,
22
+ * `ogygia/app`, or anything the build could not follow): with none of those, nothing reads it.
21
23
  *
22
24
  * Island code reads it as `globalThis[Symbol.for('ogygia.nav')]` (the key is repeated there on
23
25
  * purpose: importing {@link NAV_HANDLE_KEY} would be the very edge this module exists to avoid).
24
26
  */
25
27
  const NAV_HANDLE_KEY = Symbol.for("ogygia.nav");
28
+ /** Feature entry (`mpa-nav`, boot phase): the MPA handle, first — the router's install, later in
29
+ * the feature order, replaces it. */
30
+ function install() {
31
+ if (typeof document !== "undefined" && !(NAV_HANDLE_KEY in globalThis)) publish_nav(mpa_nav());
32
+ }
26
33
  /** Publish the document's navigation handle (the router's replaces the MPA one). */
27
34
  function publish_nav(handle) {
28
35
  globalThis[NAV_HANDLE_KEY] = handle;
@@ -68,4 +75,4 @@ function mpa_nav() {
68
75
  bust_page_cache() {}
69
76
  };
70
77
  }
71
- export { NAV_HANDLE_KEY, mpa_nav, publish_nav };
78
+ export { NAV_HANDLE_KEY, install, mpa_nav, publish_nav };
@@ -98,4 +98,13 @@ export declare function mark_render_made(node: Node): void;
98
98
  * produces at this level (`render_tags`) is render output — a runtime does not make the render's
99
99
  * elements. Anything else is the element's own: never matched, never removed.
100
100
  */
101
- export declare function render_made(node: Node, render_tags: ReadonlySet<string>): boolean;
101
+ export declare function render_made(node: Node, render_tags: ReadonlySet<string>): boolean;
102
+ /** Record what the server sent `root` (`<body>`): the attribute names on `sent` (the root as the
103
+ * server rendered it), and `children` (the live root's nodes that are the server's) as render output. */
104
+ export declare function record_server_body(root: Element, sent: Element, children: Iterable<Node>): void;
105
+ /** The attribute names the server last sent `root`, or null for a root with no record yet (all of
106
+ * it is then taken to be the server's). */
107
+ export declare function server_body_attributes(root: Element): ReadonlySet<string> | null;
108
+ /** Is `node`, a child of `<body>`, the server's? Recorded, or an `<ogygia-region>`. Anything else
109
+ * the page's scripts put there: a navigation neither matches nor removes it. */
110
+ export declare function server_body_child(node: Node): boolean;
@@ -112,4 +112,22 @@ function render_made(node, render_tags) {
112
112
  const name = node.localName;
113
113
  return name === REGION || render_tags.has(name);
114
114
  }
115
- export { BAKED_ATTR, HTML_PARENT_ATTR, OPAQUE_ATTR, REGION_RENDER_ATTRS, attributes_writer, is_declared_foreign, is_region_owned, is_self_owned, is_upgraded_ce, mark_render_made, order_owner, owner_of, render_made, slots_light_dom, walk_enters };
115
+ const NO_TAGS = /* @__PURE__ */ new Set();
116
+ const server_attrs = /* @__PURE__ */ new WeakMap();
117
+ /** Record what the server sent `root` (`<body>`): the attribute names on `sent` (the root as the
118
+ * server rendered it), and `children` (the live root's nodes that are the server's) as render output. */
119
+ function record_server_body(root, sent, children) {
120
+ server_attrs.set(root, new Set(Array.from(sent.attributes, (a) => a.name)));
121
+ for (const n of children) mark_render_made(n);
122
+ }
123
+ /** The attribute names the server last sent `root`, or null for a root with no record yet (all of
124
+ * it is then taken to be the server's). */
125
+ function server_body_attributes(root) {
126
+ return server_attrs.get(root) ?? null;
127
+ }
128
+ /** Is `node`, a child of `<body>`, the server's? Recorded, or an `<ogygia-region>`. Anything else
129
+ * the page's scripts put there: a navigation neither matches nor removes it. */
130
+ function server_body_child(node) {
131
+ return render_made(node, NO_TAGS);
132
+ }
133
+ export { BAKED_ATTR, HTML_PARENT_ATTR, OPAQUE_ATTR, REGION_RENDER_ATTRS, attributes_writer, is_declared_foreign, is_region_owned, is_self_owned, is_upgraded_ce, mark_render_made, order_owner, owner_of, record_server_body, render_made, server_body_attributes, server_body_child, slots_light_dom, walk_enters };
@@ -27,7 +27,9 @@ export declare function stamp_region_keys(root: ParentNode): void;
27
27
  * here, on either document.
28
28
  */
29
29
  export declare const NO_RECONCILE_SELECTOR = "[data-og-no-reconcile]";
30
- export declare function reconcile_body(live: Element, next: Element, morph: (parent: Element, nodes: ArrayLike<Node>) => void): void;
30
+ export declare function reconcile_body(live: Element, next: Element, morph: (parent: Element, nodes: ArrayLike<Node>, options?: {
31
+ stays?: (node: Node) => boolean;
32
+ }) => void): void;
31
33
  /**
32
34
  * outerSync body swap — the FALLBACK path (reconcile off, or a region trapped in a shadow root the
33
35
  * morph can't pierce). The old code did `document.body.replaceWith(doc.body)`, which THROWS AWAY the
@@ -35,7 +37,8 @@ export declare function reconcile_body(live: Element, next: Element, morph: (par
35
37
  * analytics, a scroll library) attached to `document.body`. htmx 4 boosts land the same way and use
36
38
  * `outerSync` for exactly this reason: keep the element, sync its attributes, replace its children.
37
39
  * So we do too — the live body node survives, only its guts change. Incoming children are moved out
38
- * of the parsed document (insertion adopts them across documents).
40
+ * of the parsed document (insertion adopts them across documents). The page's own children (a
41
+ * portal's anchor) stay, as in the reconcile; the server's are replaced.
39
42
  */
40
43
  export declare function swap_body(live: HTMLElement, next: HTMLElement): void;
41
44
  export { endpoint_key, fingerprint_of, fnv1a, signature_of };