@ogygia/core 0.12.2 → 0.13.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 (227) hide show
  1. package/dist/LateIsland.svelte +3 -2
  2. package/dist/LateIslandAwait.svelte +6 -2
  3. package/dist/Region.svelte +22 -33
  4. package/dist/boundary.d.ts +0 -2
  5. package/dist/boundary.js +8 -6
  6. package/dist/compiler/debarrel/barrel.d.ts +15 -8
  7. package/dist/compiler/debarrel/barrel.js +92 -45
  8. package/dist/compiler/debarrel/plugin.js +41 -17
  9. package/dist/compiler/dev/dev-hmr.d.ts +3 -0
  10. package/dist/compiler/dev/dev-hmr.js +3 -0
  11. package/dist/compiler/driver.js +11 -4
  12. package/dist/compiler/flags.js +21 -7
  13. package/dist/compiler/fouc-css.js +5 -1
  14. package/dist/compiler/kit.js +5 -1
  15. package/dist/compiler/link/build-output.js +12 -3
  16. package/dist/compiler/macros/bake.js +18 -4
  17. package/dist/compiler/macros/dollar.js +6 -1
  18. package/dist/compiler/macros/source.js +6 -1
  19. package/dist/compiler/macros/store.js +10 -2
  20. package/dist/compiler/macros/wire.js +6 -1
  21. package/dist/compiler/parse/scan.js +10 -3
  22. package/dist/compiler/region/transform.js +188 -74
  23. package/dist/context-bridge.js +13 -5
  24. package/dist/context.d.ts +9 -0
  25. package/dist/context.js +30 -1
  26. package/dist/document.d.ts +1 -1
  27. package/dist/document.js +7 -1
  28. package/dist/early-holes-head.d.ts +5 -0
  29. package/dist/early-holes-head.js +20 -0
  30. package/dist/early-holes.d.ts +34 -0
  31. package/dist/early-holes.js +51 -0
  32. package/dist/entry-hydrate.js +9 -12
  33. package/dist/events/bus.js +7 -2
  34. package/dist/events/schema.d.ts +2 -2
  35. package/dist/fn-transport.js +4 -3
  36. package/dist/hooks.js +94 -163
  37. package/dist/insights/page-assets.js +11 -3
  38. package/dist/insights/page-insights.d.ts +0 -2
  39. package/dist/insights/page-insights.js +14 -10
  40. package/dist/insights/session-insights.js +2 -2
  41. package/dist/internal-instruments.d.ts +3 -1
  42. package/dist/internal-instruments.js +3 -1
  43. package/dist/internal-server.d.ts +3 -2
  44. package/dist/internal-server.js +3 -2
  45. package/dist/internal.d.ts +1 -2
  46. package/dist/internal.js +1 -2
  47. package/dist/keep-fallback.d.ts +0 -2
  48. package/dist/keep-fallback.js +4 -5
  49. package/dist/live-transport.js +3 -2
  50. package/dist/messages/build.d.ts +585 -0
  51. package/dist/messages/build.js +800 -0
  52. package/dist/messages/core.d.ts +464 -0
  53. package/dist/messages/core.js +843 -0
  54. package/dist/messages/define.d.ts +62 -0
  55. package/dist/messages/define.js +49 -0
  56. package/dist/messages/index.d.ts +6 -0
  57. package/dist/messages/index.js +6 -0
  58. package/dist/messages/island.d.ts +4 -0
  59. package/dist/messages/island.js +34 -0
  60. package/dist/messages/lazy.d.ts +3 -0
  61. package/dist/messages/lazy.js +22 -0
  62. package/dist/messages/prose.d.ts +15 -0
  63. package/dist/messages/prose.js +43 -0
  64. package/dist/messages/server.d.ts +142 -0
  65. package/dist/messages/server.js +246 -0
  66. package/dist/nested.d.ts +3 -0
  67. package/dist/nested.js +8 -1
  68. package/dist/plugin/css.js +14 -13
  69. package/dist/plugin/facts.d.ts +2 -0
  70. package/dist/plugin/facts.js +10 -6
  71. package/dist/plugin/hooks.d.ts +7 -17
  72. package/dist/plugin/hooks.js +32 -4
  73. package/dist/plugin/index.d.ts +9 -2
  74. package/dist/plugin/request-scope.d.ts +20 -0
  75. package/dist/plugin/request-scope.js +149 -0
  76. package/dist/plugin/server.d.ts +20 -2
  77. package/dist/plugin/server.js +98 -28
  78. package/dist/plugin/testing.d.ts +6 -0
  79. package/dist/plugin/testing.js +14 -2
  80. package/dist/policies.d.ts +21 -0
  81. package/dist/policies.js +66 -0
  82. package/dist/preference.js +6 -1
  83. package/dist/preload.js +6 -9
  84. package/dist/ref-watch.d.ts +0 -10
  85. package/dist/ref-watch.js +14 -1
  86. package/dist/ref.js +17 -5
  87. package/dist/region-snippet.js +9 -3
  88. package/dist/region.js +2 -1
  89. package/dist/report-thrown.d.ts +9 -0
  90. package/dist/report-thrown.js +15 -0
  91. package/dist/rules.d.ts +17 -0
  92. package/dist/rules.js +57 -0
  93. package/dist/runtime/beacon.d.ts +0 -4
  94. package/dist/runtime/beacon.js +58 -43
  95. package/dist/runtime/boot-link.js +19 -2
  96. package/dist/runtime/client-hooks.js +2 -1
  97. package/dist/runtime/concurrency.d.ts +16 -5
  98. package/dist/runtime/concurrency.js +123 -7
  99. package/dist/runtime/core.d.ts +3 -1
  100. package/dist/runtime/core.js +718 -384
  101. package/dist/runtime/devalue-lane.d.ts +13 -0
  102. package/dist/runtime/devalue-lane.js +49 -0
  103. package/dist/runtime/disclose-version.d.ts +1 -0
  104. package/dist/runtime/disclose-version.js +14 -0
  105. package/dist/runtime/early-holes.d.ts +4 -0
  106. package/dist/runtime/early-holes.js +40 -0
  107. package/dist/runtime/entry-locations.js +40 -49
  108. package/dist/runtime/foreign-props.d.ts +0 -7
  109. package/dist/runtime/foreign-props.js +2 -1
  110. package/dist/runtime/form-continuity.d.ts +2 -2
  111. package/dist/runtime/form-continuity.js +30 -8
  112. package/dist/runtime/frame-nav.d.ts +7 -1
  113. package/dist/runtime/frame-nav.js +84 -18
  114. package/dist/runtime/frame-store.d.ts +25 -16
  115. package/dist/runtime/frame-store.js +124 -98
  116. package/dist/runtime/frames.js +8 -4
  117. package/dist/runtime/hole-facts.js +6 -7
  118. package/dist/runtime/hole-fetch.d.ts +17 -0
  119. package/dist/runtime/hole-fetch.js +47 -0
  120. package/dist/runtime/hydrate-core.d.ts +4 -4
  121. package/dist/runtime/hydrate-core.js +80 -64
  122. package/dist/runtime/import-again.d.ts +33 -0
  123. package/dist/runtime/import-again.js +217 -0
  124. package/dist/runtime/interaction-replay.d.ts +5 -3
  125. package/dist/runtime/interaction-replay.js +6 -6
  126. package/dist/runtime/interaction.d.ts +14 -3
  127. package/dist/runtime/interaction.js +46 -23
  128. package/dist/runtime/kit-page-thread.svelte.d.ts +2 -1
  129. package/dist/runtime/kit-page-thread.svelte.js +44 -20
  130. package/dist/runtime/lakes.d.ts +1 -1
  131. package/dist/runtime/lakes.js +18 -5
  132. package/dist/runtime/live.d.ts +1 -2
  133. package/dist/runtime/live.js +6 -8
  134. package/dist/runtime/load-scheduler.d.ts +29 -18
  135. package/dist/runtime/load-scheduler.js +236 -136
  136. package/dist/runtime/nav-handle.js +25 -6
  137. package/dist/runtime/navigation.d.ts +24 -0
  138. package/dist/runtime/navigation.js +24 -0
  139. package/dist/runtime/ownership.d.ts +1 -1
  140. package/dist/runtime/page-defer.js +3 -3
  141. package/dist/runtime/parse-html.d.ts +15 -4
  142. package/dist/runtime/parse-html.js +24 -5
  143. package/dist/runtime/pushable-props.svelte.d.ts +21 -0
  144. package/dist/runtime/pushable-props.svelte.js +37 -0
  145. package/dist/runtime/region-endpoint-url.js +4 -6
  146. package/dist/runtime/region-machines.d.ts +111 -0
  147. package/dist/runtime/region-machines.js +97 -0
  148. package/dist/runtime/region-sheets.d.ts +31 -0
  149. package/dist/runtime/region-sheets.js +49 -0
  150. package/dist/runtime/restore-report.d.ts +1 -0
  151. package/dist/runtime/restore-report.js +36 -0
  152. package/dist/runtime/restore.d.ts +5 -2
  153. package/dist/runtime/restore.js +33 -14
  154. package/dist/runtime/router-nav.d.ts +32 -12
  155. package/dist/runtime/router-nav.js +320 -228
  156. package/dist/runtime/router.d.ts +83 -19
  157. package/dist/runtime/router.js +297 -129
  158. package/dist/runtime/rules.d.ts +35 -0
  159. package/dist/runtime/rules.js +137 -0
  160. package/dist/runtime/schedule.d.ts +6 -3
  161. package/dist/runtime/schedule.js +172 -57
  162. package/dist/runtime/seeds.d.ts +5 -1
  163. package/dist/runtime/seeds.js +11 -5
  164. package/dist/runtime/session.d.ts +46 -7
  165. package/dist/runtime/session.js +78 -10
  166. package/dist/runtime/slots.d.ts +53 -20
  167. package/dist/runtime/slots.js +2 -3
  168. package/dist/runtime/soft-refresh.d.ts +16 -0
  169. package/dist/runtime/soft-refresh.js +14 -0
  170. package/dist/runtime/svelte-warnings.d.ts +4 -10
  171. package/dist/runtime/svelte-warnings.js +7 -4
  172. package/dist/runtime/wire-classes.js +21 -8
  173. package/dist/runtime/wire-format.js +6 -3
  174. package/dist/script.js +8 -7
  175. package/dist/seed-ref-reviver.d.ts +0 -9
  176. package/dist/seed-ref-reviver.js +2 -1
  177. package/dist/seed-refs.d.ts +46 -36
  178. package/dist/seed-refs.js +131 -92
  179. package/dist/server/document-tail.d.ts +6 -0
  180. package/dist/server/document-tail.js +7 -1
  181. package/dist/server/early-holes-csp.d.ts +14 -0
  182. package/dist/server/early-holes-csp.js +73 -0
  183. package/dist/server/page-facts.d.ts +4 -1
  184. package/dist/server/page-facts.js +4 -1
  185. package/dist/server/page-seed.d.ts +2 -2
  186. package/dist/server/page-seed.js +104 -11
  187. package/dist/server/page-stream.d.ts +14 -1
  188. package/dist/server/page-stream.js +119 -2
  189. package/dist/server/personal-response.d.ts +0 -5
  190. package/dist/server/personal-response.js +6 -2
  191. package/dist/server/props-wire.js +5 -1
  192. package/dist/server/region-endpoint.js +10 -2
  193. package/dist/server/render-cache.d.ts +24 -20
  194. package/dist/server/render-cache.js +99 -40
  195. package/dist/server/render-page.js +46 -15
  196. package/dist/server/request-stats.d.ts +10 -8
  197. package/dist/server/request-stats.js +20 -12
  198. package/dist/server/rules.d.ts +11 -0
  199. package/dist/server/rules.js +64 -0
  200. package/dist/server/signing-key.js +31 -18
  201. package/dist/server/stream-regions.d.ts +14 -1
  202. package/dist/server/stream-regions.js +38 -2
  203. package/dist/shared-state.js +5 -1
  204. package/dist/shims/app-navigation.js +3 -2
  205. package/dist/shims/page-store.svelte.js +16 -2
  206. package/dist/source.js +7 -2
  207. package/dist/store-transport.js +12 -5
  208. package/dist/transport.js +3 -2
  209. package/dist/vite/index.js +83 -74
  210. package/dist/vite/kit-dirs.js +7 -1
  211. package/dist/vite/options.d.ts +2 -1
  212. package/dist/vite/options.js +11 -20
  213. package/dist/vite/package-files.js +13 -3
  214. package/dist/vite/paths.d.ts +2 -0
  215. package/dist/vite/paths.js +4 -4
  216. package/dist/vite/plugin-api.d.ts +6 -5
  217. package/dist/vite/plugin-api.js +42 -14
  218. package/dist/vite/style-preprocess.js +10 -3
  219. package/package.json +8 -1
  220. package/dist/KeepHost.svelte +0 -19
  221. package/dist/KeepHost.svelte.d.ts +0 -10
  222. package/dist/LiveHost.svelte +0 -19
  223. package/dist/LiveHost.svelte.d.ts +0 -10
  224. package/dist/NestedProvider.svelte +0 -13
  225. package/dist/NestedProvider.svelte.d.ts +0 -8
  226. package/dist/runtime/page-cache.d.ts +0 -23
  227. package/dist/runtime/page-cache.js +0 -74
@@ -6,6 +6,7 @@ import { regionId, regionIdentity, strategyKey } from "./identity.js";
6
6
  import { regionBindingVirtualId } from "../ids.js";
7
7
  import { island_entry_source, lake_wrapper_source, make_region_binding, make_wake_island, server_wrapper_source } from "./emit.js";
8
8
  import { blank_styles } from "../blank-styles.js";
9
+ import { OgygiaError, warn } from "../../messages/build.js";
9
10
  import { parse } from "svelte/compiler";
10
11
  import MagicString from "magic-string";
11
12
  // src/compiler/region/transform.ts
@@ -16,7 +17,11 @@ function warn_unparsed(id, err) {
16
17
  if (warned_unparsed.has(id)) return;
17
18
  warned_unparsed.add(id);
18
19
  const e = err;
19
- console.warn(`[ogygia] regions: skipped ${id}: ${e.code ?? (e.message ?? "parse error").split("\n")[0].slice(0, 80)}${e.start?.line ? ` at ${e.start.line}:${(e.start.column ?? 0) + 1}` : ""} — its region imports stay plain imports`);
20
+ warn("region_host_skipped", {
21
+ file: id,
22
+ reason: e.code ?? (e.message ?? "parse error").split("\n")[0].slice(0, 80),
23
+ at: e.start?.line ? `${e.start.line}:${(e.start.column ?? 0) + 1}` : ""
24
+ });
20
25
  }
21
26
  const REGEXP_META = /[.*+?^${}()|[\]\\]/g;
22
27
  const PATH_SEP = /[/\\]/;
@@ -93,10 +98,10 @@ function line_col(src, offset) {
93
98
  }
94
99
  /** Once per key for the process — the ssr AND client passes both transform the same host. */
95
100
  const store_warned = /* @__PURE__ */ new Set();
96
- function warn_once(key, msg) {
101
+ function warn_once(key, say) {
97
102
  if (store_warned.has(key)) return;
98
103
  store_warned.add(key);
99
- console.warn(msg);
104
+ say();
100
105
  }
101
106
  const KNOWN_STRATEGIES = /* @__PURE__ */ new Set([
102
107
  "load",
@@ -158,14 +163,17 @@ function normalize_import_keys_uncached(overrides) {
158
163
  ["render", render],
159
164
  ["preset", preset],
160
165
  ["region", region]
161
- ]) if (!name || !JS_IDENT.test(name)) throw new Error(`[ogygia] importKeys.${role} must be a non-empty JS identifier (got ${JSON.stringify(overrides?.[role])}).`);
166
+ ]) if (!name || !JS_IDENT.test(name)) throw new OgygiaError("import_key_invalid", {
167
+ role,
168
+ got: String(JSON.stringify(overrides?.[role]))
169
+ });
162
170
  if ((/* @__PURE__ */ new Set([
163
171
  wake,
164
172
  fetch,
165
173
  render,
166
174
  preset,
167
175
  region
168
- ])).size !== 5) throw new Error("[ogygia] importKeys.wake, importKeys.fetch, importKeys.render, importKeys.preset, and importKeys.region must be distinct.");
176
+ ])).size !== 5) throw new OgygiaError("import_keys_not_distinct");
169
177
  return {
170
178
  wake,
171
179
  fetch,
@@ -339,7 +347,10 @@ function parse_ts_as_region_options(arg, fail, regionKey, wakeKey, renderKey, fe
339
347
  */
340
348
  function normalize_region_value(raw, where, regionKey = "region") {
341
349
  if ((raw ?? "").trim() === "raw") return;
342
- throw new Error(`[ogygia] ${where}: \`${regionKey}\` only takes the value 'raw' (a held region with no baked schedule). To bake a schedule use a \`wake:\` mark, or set it at the \`region()\` call.`);
350
+ throw new OgygiaError("region_value_not_raw", {
351
+ host: where,
352
+ key: regionKey
353
+ });
343
354
  }
344
355
  /**
345
356
  * Validate a `wake:` schedule value on a held import (a `.ts` registry / remote). Accepts the wake
@@ -349,7 +360,11 @@ function normalize_region_value(raw, where, regionKey = "region") {
349
360
  function normalize_hydrate_value(raw, where, wakeKey = "wake") {
350
361
  const v = (raw ?? "").trim();
351
362
  if (HYDRATE_STRATEGIES.has(v) || is_media_query(v)) return v;
352
- throw new Error(`[ogygia] ${where}: unknown \`${wakeKey}\` strategy '${v}'. Use 'load' | 'idle' | 'visible' | 'interaction' | a media query.`);
363
+ throw new OgygiaError("wake_strategy_unknown", {
364
+ host: where,
365
+ key: wakeKey,
366
+ value: v
367
+ });
353
368
  }
354
369
  /** Reconstruct an import declaration without its `with { ... }` attributes clause. */
355
370
  function clean_import_text(source, node) {
@@ -613,7 +628,10 @@ function assert_swr_lake_crossable(node, err) {
613
628
  */
614
629
  function resolve_component_path(spec, host_id, ctx) {
615
630
  if (typeof spec !== "string" || !spec.trim()) return null;
616
- if (spec.startsWith(".") && VIRTUAL_HOST_RE.test(host_id)) throw new Error(`[ogygia] ${host_id}: cannot resolve the relative import '${spec}' from a generated module — it has no directory. Its real origin must be threaded (HostCtx.originOf); this is an ogygia bug, please report it.`);
631
+ if (spec.startsWith(".") && VIRTUAL_HOST_RE.test(host_id)) throw new OgygiaError("generated_module_relative_import", {
632
+ host: host_id,
633
+ specifier: spec
634
+ });
617
635
  if (spec === "$lib" || spec.startsWith("$lib/")) return ctx.pathModule.join(ctx.libDir, spec === "$lib" ? "" : spec.slice(5));
618
636
  if (spec.startsWith(".")) return ctx.pathModule.resolve(ctx.pathModule.dirname(host_id), spec);
619
637
  return spec;
@@ -736,21 +754,19 @@ var FileCompilation = class {
736
754
  if (early) return early.done;
737
755
  return this.#lower();
738
756
  }
739
- /** Walk an AST subtree for an Identifier / Component reference to `local`. Pure over the AST. */
740
- #ast_refs_local(root, local) {
741
- let found = false;
757
+ /** Every name the host's code references outside its imports and the `skip`ped instance
758
+ * statements: Identifier names, Component names and each dotted prefix of one (`<Ui.Card>`
759
+ * references `Ui`). ONE walk answers every "is `local` used?" question — asking per import walked
760
+ * the whole file once per island, quadratic in the island count. Pure over the AST. */
761
+ #referenced_names(skip) {
762
+ const names = /* @__PURE__ */ new Set();
742
763
  const walk = (node) => {
743
- if (found || !node || typeof node !== "object") return;
744
- if (node.type === "Identifier" && node.name === local) {
745
- found = true;
746
- return;
747
- }
748
- if (node.type === "Component") {
764
+ if (!node || typeof node !== "object") return;
765
+ if (node.type === "Identifier") names.add(node.name);
766
+ else if (node.type === "Component") {
749
767
  const n = node.name || "";
750
- if (n === local || n.startsWith(local + ".")) {
751
- found = true;
752
- return;
753
- }
768
+ names.add(n);
769
+ for (let i = n.indexOf("."); i !== -1; i = n.indexOf(".", i + 1)) names.add(n.slice(0, i));
754
770
  }
755
771
  for (const k of Object.keys(node)) {
756
772
  if (k === "start" || k === "end" || k === "loc") continue;
@@ -759,21 +775,15 @@ var FileCompilation = class {
759
775
  else if (v && typeof v === "object" && typeof v.type === "string") walk(v);
760
776
  }
761
777
  };
762
- if (Array.isArray(root)) for (const n of root) walk(n);
763
- else walk(root);
764
- return found;
778
+ for (const n of this.#instance_body) if (n.type !== "ImportDeclaration" && !skip?.has(n)) walk(n);
779
+ for (const n of this.#module_body) if (n.type !== "ImportDeclaration") walk(n);
780
+ for (const n of this.#ast.fragment?.nodes ?? []) walk(n);
781
+ return names;
765
782
  }
783
+ #used_names;
766
784
  /** Is a marked import's `local` used anywhere but its own import (else it is dead, strip it)? */
767
785
  #marked_import_referenced(local) {
768
- for (const n of this.#instance_body) {
769
- if (n.type === "ImportDeclaration") continue;
770
- if (this.#ast_refs_local(n, local)) return true;
771
- }
772
- for (const n of this.#module_body) {
773
- if (n.type === "ImportDeclaration") continue;
774
- if (this.#ast_refs_local(n, local)) return true;
775
- }
776
- return this.#ast_refs_local(this.#ast.fragment?.nodes ?? [], local);
786
+ return (this.#used_names ??= this.#referenced_names()).has(local);
777
787
  }
778
788
  #resolve_region_mark(inline, fail) {
779
789
  const import_keys = this.#import_keys;
@@ -841,7 +851,14 @@ var FileCompilation = class {
841
851
  const said = from_preset ? `preset '${from_preset}'` : "this import";
842
852
  if (render_mode === "deferred" && wake_val != null) {
843
853
  if (fetch_val != null) throw fail(`${said} times a \`${import_keys.render}: 'deferred'\` hole twice, with \`${import_keys.fetch}: '${fetch_val}'\` and \`${import_keys.wake}: '${wake_val}'\`. Keep \`${import_keys.fetch}\`: it says when a hole's HTML arrives.`);
844
- warn_once(`deferred-wake:${rel_host}:${said}:${wake_val}`, `[ogygia] ${rel_host}: ${said} times a \`${import_keys.render}: 'deferred'\` hole with \`${import_keys.wake}: '${wake_val}'\`, which is deprecated. Write \`${import_keys.render}: 'deferred', ${import_keys.fetch}: '${wake_val}'\` instead: \`${import_keys.fetch}\` says when a hole's HTML arrives. (\`${import_keys.wake}\` is when an island's JS runs; a hole ships no JS.)`);
854
+ warn_once(`deferred-wake:${rel_host}:${said}:${wake_val}`, () => warn("hole_wake_deprecated", {
855
+ host: rel_host,
856
+ said,
857
+ render_key: import_keys.render,
858
+ wake_key: import_keys.wake,
859
+ fetch_key: import_keys.fetch,
860
+ value: wake_val
861
+ }));
845
862
  fetch_val = wake_val;
846
863
  }
847
864
  if (fetch_val != null && render_mode !== "deferred") throw fail(`${said} sets \`${import_keys.fetch}: '${fetch_val}'\` without \`${import_keys.render}: 'deferred'\` — \`${import_keys.fetch}\` is a hole's timing. Write \`${import_keys.render}: 'deferred', ${import_keys.fetch}: '${fetch_val}'\` for a hole` + (render_mode == null ? `, or \`${import_keys.wake}: '${fetch_val}'\` for an island.` : "."));
@@ -944,7 +961,11 @@ var FileCompilation = class {
944
961
  const kids = (node.fragment?.nodes ?? []).filter((n) => !(n.type === "Text" && !String(n.data ?? "").trim()));
945
962
  if (kids.length === 0) return;
946
963
  if (is_server && kids.length === 1 && kids[0].type === "SnippetBlock" && kids[0].expression?.name === "ogygiaFallback") return;
947
- throw new Error(`[ogygia] ${this.#rel_host}: <${local}> has host children/snippets that cannot cross the island boundary. Under portable bindings, pass serializable props and put UI inside the island component` + (is_server ? ` (only the reserved \`{#snippet ogygiaFallback()}\` may appear at the call site).` : `.`));
964
+ throw new OgygiaError("region_children_not_portable", {
965
+ host: this.#rel_host,
966
+ tag: local,
967
+ server: is_server
968
+ });
948
969
  }
949
970
  /** Walk the markup: enforce region-usage rules + surface whether a hydrate island has real
950
971
  * children (the app then needs the wire slot revivers). Mutates #has_island_children. */
@@ -954,7 +975,11 @@ var FileCompilation = class {
954
975
  const name = node.name || "";
955
976
  if (name.includes(".")) {
956
977
  const root = name.split(".")[0];
957
- if (this.#marked_components.has(root) && this.#marked_components.get(root).strategy !== "lake") throw new Error(`[ogygia] ${this.#rel_host}: dotted tag \`<${name}>\` is not supported for region import '${root}'. Import the leaf component with \`with { hydrate|defer }\` instead.`);
978
+ if (this.#marked_components.has(root) && this.#marked_components.get(root).strategy !== "lake") throw new OgygiaError("region_dotted_tag", {
979
+ host: this.#rel_host,
980
+ tag: name,
981
+ import: root
982
+ });
958
983
  } else if (this.#marked_components.has(name)) {
959
984
  this.#static_placed.add(name);
960
985
  const mark = this.#marked_components.get(name);
@@ -983,13 +1008,22 @@ var FileCompilation = class {
983
1008
  if (!this.#ctx.pathModule.isAbsolute(p)) return p;
984
1009
  return (typeof this.#ctx.pkg_identity === "function" ? this.#ctx.pkg_identity(p) : null) ?? this.#posix_rel(p);
985
1010
  }
986
- /** `[ogygia] host: import { … } — msg` — the region-import error formatter. */
1011
+ /** `host: import { … } — msg` (`region_import_invalid`) — the region-import error formatter. */
987
1012
  #err(specifiers, msg) {
988
- return /* @__PURE__ */ new Error(`[ogygia] ${this.#rel_host}: import { ${specifiers} } — ${msg}`);
1013
+ return new OgygiaError("region_import_invalid", {
1014
+ host: this.#rel_host,
1015
+ names: specifiers,
1016
+ problem: msg
1017
+ });
989
1018
  }
990
- /** `[ogygia] host: import.meta.og.asRegion (local) — msg` — the asRegion error formatter. */
1019
+ /** `host: import.meta.og.asRegion (local) — msg` (`as_region_invalid`) — the asRegion error
1020
+ * formatter. */
991
1021
  #as_err(local, msg) {
992
- return /* @__PURE__ */ new Error(`[ogygia] ${this.#rel_host}: import.meta.og.asRegion (${local}) — ${msg}`);
1022
+ return new OgygiaError("as_region_invalid", {
1023
+ host: this.#rel_host,
1024
+ name: local,
1025
+ problem: msg
1026
+ });
993
1027
  }
994
1028
  /** Read asRegion's options object into the same key→value map an import attribute produces, so the
995
1029
  * SHARED parser gives asRegion EXACTLY the import-attribute option surface. Object-only and
@@ -1035,12 +1069,17 @@ var FileCompilation = class {
1035
1069
  const lang = script_lang_attr(ast.instance) || script_lang_attr(ast.module);
1036
1070
  const path = ctx.pathModule;
1037
1071
  const rel_host = this.#rel_host = path.relative(ctx.root, id).split(PATH_SEP).join("/");
1038
- reject_dynamic_region_imports(instance_body, import_keys, (keys) => {
1039
- throw new Error(`[ogygia] ${rel_host}: dynamic import() with { with: { ${keys.join(", ")} } } is not supported. Mark islands with a static \`import X from '…' with { ${import_keys.wake}: '…' }\` (or \`${import_keys.render}\` / \`${import_keys.preset}\`) — the binding becomes a portable island component. For a click-loaded chunk that is not an island, use plain \`await import('./Comp.svelte')\` (no region attributes) inside a host island. To delay a real island until click, gate a static region import with \`{#if}\`.`);
1040
- });
1041
- reject_dynamic_region_imports(module_body, import_keys, (keys) => {
1042
- throw new Error(`[ogygia] ${rel_host}: dynamic import() with { with: { ${keys.join(", ")} } } is not supported. Mark islands with a static \`import X from '…' with { ${import_keys.wake}: '…' }\` (or \`${import_keys.render}\` / \`${import_keys.preset}\`) — the binding becomes a portable island component. For a click-loaded chunk that is not an island, use plain \`await import('./Comp.svelte')\` (no region attributes) inside a host island. To delay a real island until click, gate a static region import with \`{#if}\`.`);
1043
- });
1072
+ const reject_dynamic = (keys) => {
1073
+ throw new OgygiaError("dynamic_region_import", {
1074
+ host: rel_host,
1075
+ keys: keys.join(", "),
1076
+ wake_key: import_keys.wake,
1077
+ render_key: import_keys.render,
1078
+ preset_key: import_keys.preset
1079
+ });
1080
+ };
1081
+ reject_dynamic_region_imports(instance_body, import_keys, reject_dynamic);
1082
+ reject_dynamic_region_imports(module_body, import_keys, reject_dynamic);
1044
1083
  /** localName -> { node, cleaned } */
1045
1084
  const imports = /* @__PURE__ */ new Map();
1046
1085
  /** localName -> { strategy, options } */
@@ -1140,7 +1179,7 @@ var FileCompilation = class {
1140
1179
  * Lower — the compilation's fields → rewritten source + `IslandDescriptor[]`. Consumes ONLY the
1141
1180
  * analyze pass's fields (it never re-reads the file): creates the MagicString, rewrites each marked
1142
1181
  * import to its wrapper / binding / stub, mints the island records, and brands portable snippets.
1143
- * `err` / `ast_refs_local` / `marked_import_referenced` are rebuilt here from those fields — pure
1182
+ * `err` / `referenced_names` / `marked_import_referenced` are rebuilt here from those fields — pure
1144
1183
  * over the AST + host path.
1145
1184
  */
1146
1185
  #lower() {
@@ -1251,7 +1290,11 @@ var FileCompilation = class {
1251
1290
  }
1252
1291
  const entry_spec = info.node.source?.value;
1253
1292
  const componentPath = resolve_component_path(entry_spec, origin, ctx);
1254
- if (!componentPath) throw new Error(`[ogygia] ${rel_host}: region import '${local}' needs a module specifier ($lib/…, relative, or a package specifier like 'pkg/component').`);
1293
+ if (!componentPath) throw new OgygiaError("region_import_no_specifier", {
1294
+ host: rel_host,
1295
+ name: local,
1296
+ held: false
1297
+ });
1255
1298
  const exportName = synthetic_export.get(local);
1256
1299
  const comp_rel = this.#component_identity(componentPath);
1257
1300
  const id_base = exportName && exportName !== "default" ? `${comp_rel}#${exportName}` : comp_rel;
@@ -1344,17 +1387,8 @@ var FileCompilation = class {
1344
1387
  }
1345
1388
  const as_region_locals = new Set(as_regions.map((r) => r.compLocal));
1346
1389
  if (as_region_locals.size) {
1347
- const referenced_outside = (local) => {
1348
- for (const n of instance_body) {
1349
- if (n.type === "ImportDeclaration" || as_region_nodes.has(n)) continue;
1350
- if (this.#ast_refs_local(n, local)) return true;
1351
- }
1352
- for (const n of module_body) {
1353
- if (n.type === "ImportDeclaration") continue;
1354
- if (this.#ast_refs_local(n, local)) return true;
1355
- }
1356
- return this.#ast_refs_local(ast.fragment?.nodes ?? [], local);
1357
- };
1390
+ const outside = this.#referenced_names(as_region_nodes);
1391
+ const referenced_outside = (local) => outside.has(local);
1358
1392
  const seen = /* @__PURE__ */ new Set();
1359
1393
  for (const compLocal of as_region_locals) {
1360
1394
  const info = imports.get(compLocal);
@@ -1394,7 +1428,16 @@ var FileCompilation = class {
1394
1428
  }
1395
1429
  };
1396
1430
  if (portable_allowed) walk_portable(ast.fragment?.nodes ?? []);
1397
- const outer_candidates = portable_candidates.filter((c) => !portable_candidates.some((o) => o !== c && c.snip.start >= o.snip.start && c.snip.end <= o.snip.end));
1431
+ const by_start = [...portable_candidates].sort(outer_spans_first);
1432
+ const nested = /* @__PURE__ */ new Set();
1433
+ let reach = -Infinity;
1434
+ for (let i = 0; i < by_start.length; i++) {
1435
+ const c = by_start[i];
1436
+ const twin = by_start[i + 1];
1437
+ if (c.snip.end <= reach || twin && twin.snip.start === c.snip.start && twin.snip.end === c.snip.end) nested.add(c);
1438
+ if (c.snip.end > reach) reach = c.snip.end;
1439
+ }
1440
+ const outer_candidates = portable_candidates.filter((c) => !nested.has(c));
1398
1441
  const block_bound = /* @__PURE__ */ new Set();
1399
1442
  const bind_pattern = (n) => {
1400
1443
  if (!n || typeof n !== "object") return;
@@ -1476,7 +1519,11 @@ var FileCompilation = class {
1476
1519
  const store_captures = [];
1477
1520
  const store_rewrites = [];
1478
1521
  for (const [nm, sites] of stores) {
1479
- if (nm.startsWith("$$")) throw new Error(`[ogygia] ${rel_host}: {#snippet ${name}} crosses an island boundary but reads \`${nm}\` — the host's $$-props cannot cross. Read the value into a plain host variable outside the snippet and use that instead.`);
1522
+ if (nm.startsWith("$$")) throw new OgygiaError("snippet_reads_dollar_props", {
1523
+ host: rel_host,
1524
+ snippet: name,
1525
+ name: nm
1526
+ });
1480
1527
  const base = nm.slice(1);
1481
1528
  const prop = `__og_sub_${base}`;
1482
1529
  store_captures.push({
@@ -1489,8 +1536,18 @@ var FileCompilation = class {
1489
1536
  prop
1490
1537
  });
1491
1538
  const trace = sites.map((p) => `${rel_host}:${line_col(source, p.start)}`).join(", ");
1492
- if (host_declared.has(base) || imports.has(base)) warn_once(`${id}\0${name}\0${nm}\0frozen`, `[ogygia] ${rel_host}: {#snippet ${name}} crosses an island boundary and reads \`${nm}\` (at ${trace}) — captured as a FROZEN snapshot of the store's value at render time; later store updates never reach the island. To make the snapshot explicit (and silence this), read it at host scope outside the snippet — e.g. \`const ${base}_value = ${nm};\` — and use that.`);
1493
- else warn_once(`${id}\0${name}\0${nm}\0unresolved`, `[ogygia] ${rel_host}: {#snippet ${name}} reads \`${nm}\` (at ${trace}), but \`${base}\` is not a host declaration or import, so ogygia cannot verify it is a store. Its value is captured at the host anyway — if the build now errors in ${rel_host}, start there.`);
1539
+ if (host_declared.has(base) || imports.has(base)) warn_once(`${id}\0${name}\0${nm}\0frozen`, () => warn("snippet_store_snapshot", {
1540
+ host: rel_host,
1541
+ snippet: name,
1542
+ store: nm,
1543
+ at: trace
1544
+ }));
1545
+ else warn_once(`${id}\0${name}\0${nm}\0unresolved`, () => warn("snippet_store_unverified", {
1546
+ host: rel_host,
1547
+ snippet: name,
1548
+ store: nm,
1549
+ at: trace
1550
+ }));
1494
1551
  }
1495
1552
  const cleaned_imports = [];
1496
1553
  const seen_imports = /* @__PURE__ */ new Set();
@@ -1506,7 +1563,11 @@ var FileCompilation = class {
1506
1563
  cleaned_imports.push(text);
1507
1564
  }
1508
1565
  } else if (host_declared.has(nm)) {
1509
- if (store_like.has(nm)) warn_once(`${id}\0${name}\0${nm}\0object`, `[ogygia] ${rel_host}: {#snippet ${name}} crosses an island boundary and captures the store \`${nm}\` as an OBJECT — a host-declared store cannot cross (its functions don't serialize). Use its value (\`$${nm}\`) inside the snippet, or share live state with a wired class instead.`);
1566
+ if (store_like.has(nm)) warn_once(`${id}\0${name}\0${nm}\0object`, () => warn("snippet_captures_store", {
1567
+ host: rel_host,
1568
+ snippet: name,
1569
+ store: nm
1570
+ }));
1510
1571
  captures.push(nm);
1511
1572
  } else if (module_imports.has(nm)) {
1512
1573
  const text = module_imports.get(nm).trim();
@@ -1604,6 +1665,10 @@ var FileCompilation = class {
1604
1665
  * @param raw the text between the `with {` and `}`
1605
1666
  * @returns
1606
1667
  */
1668
+ /** Span order for the portable-snippet nesting sweep: by start, the longer span first on a tie. */
1669
+ function outer_spans_first(a, b) {
1670
+ return a.snip.start - b.snip.start || b.snip.end - a.snip.end;
1671
+ }
1607
1672
  function parse_import_attrs(raw) {
1608
1673
  const attrs = /* @__PURE__ */ new Map();
1609
1674
  IMPORT_ATTR_PAIR_G.lastIndex = 0;
@@ -1640,7 +1705,10 @@ function ts_literal_ranges(source, id) {
1640
1705
  const ranges = [];
1641
1706
  const walk = (n) => {
1642
1707
  if (!n || typeof n !== "object") return;
1643
- if (Array.isArray(n)) return void n.forEach(walk);
1708
+ if (Array.isArray(n)) {
1709
+ n.forEach(walk);
1710
+ return;
1711
+ }
1644
1712
  const node = n;
1645
1713
  const t = node.type;
1646
1714
  const is_str = (t === "Literal" || t === "StringLiteral") && typeof node.value === "string";
@@ -1699,9 +1767,13 @@ var TsRegionCompilation = class {
1699
1767
  #posix_rel(abs) {
1700
1768
  return this.#path.relative(this.#root, abs).split(PATH_SEP).join("/");
1701
1769
  }
1702
- /** `[ogygia] host: import.meta.og.asRegion (local) — msg`. */
1770
+ /** `host: import.meta.og.asRegion (local) — msg` (`as_region_invalid`). */
1703
1771
  #as_err(local, msg) {
1704
- return /* @__PURE__ */ new Error(`[ogygia] ${this.#rel_host}: import.meta.og.asRegion (${local}) — ${msg}`);
1772
+ return new OgygiaError("as_region_invalid", {
1773
+ host: this.#rel_host,
1774
+ name: local,
1775
+ problem: msg
1776
+ });
1705
1777
  }
1706
1778
  /**
1707
1779
  * Same policy as transformHost's resolve_component_path: $lib/relative → absolute file path,
@@ -1732,18 +1804,49 @@ var TsRegionCompilation = class {
1732
1804
  const id_base = exportName && exportName !== "default" ? `${comp_rel}#${exportName}` : comp_rel;
1733
1805
  const fetchKey = this.#import_keys.fetch;
1734
1806
  const renderKey = this.#import_keys.render;
1735
- if (marker.fetch != null && marker.render == null) throw new Error(`[ogygia] ${this.#rel_host}: \`${fetchKey}: '${marker.fetch}'\` without \`${renderKey}: 'deferred'\` — \`${fetchKey}\` is a hole's timing. Write \`${renderKey}: 'deferred', ${fetchKey}: '${marker.fetch}'\` for a hole, or \`${this.#wakeKey}: '${marker.fetch}'\` for an island.`);
1807
+ if (marker.fetch != null && marker.render == null) throw new OgygiaError("held_fetch_without_render", {
1808
+ host: this.#rel_host,
1809
+ fetch_key: fetchKey,
1810
+ render_key: renderKey,
1811
+ wake_key: this.#wakeKey,
1812
+ value: marker.fetch
1813
+ });
1736
1814
  if (marker.render != null) {
1737
- if (marker.render !== "deferred") throw new Error(marker.render === "live" ? `[ogygia] ${this.#rel_host}: \`${renderKey}: 'live'\` is not supported on a .ts import — use it in a .svelte file.` : `[ogygia] ${this.#rel_host}: unknown \`${renderKey}\` '${marker.render}'. Use 'deferred'.`);
1815
+ if (marker.render !== "deferred") throw marker.render === "live" ? new OgygiaError("held_render_live", {
1816
+ host: this.#rel_host,
1817
+ render_key: renderKey
1818
+ }) : new OgygiaError("held_render_unknown", {
1819
+ host: this.#rel_host,
1820
+ render_key: renderKey,
1821
+ value: marker.render
1822
+ });
1738
1823
  if (marker.wake != null) {
1739
- if (marker.fetch != null) throw new Error(`[ogygia] ${this.#rel_host}: a \`${renderKey}: 'deferred'\` hole timed twice, with \`${fetchKey}: '${marker.fetch}'\` and \`${this.#wakeKey}: '${marker.wake}'\`. Keep \`${fetchKey}\`: it says when a hole's HTML arrives.`);
1740
- warn_once(`deferred-wake:${this.#rel_host}:ts:${marker.wake}`, `[ogygia] ${this.#rel_host}: a \`${renderKey}: 'deferred'\` hole timed with \`${this.#wakeKey}: '${marker.wake}'\`, which is deprecated. Write \`${renderKey}: 'deferred', ${fetchKey}: '${marker.wake}'\` instead: \`${fetchKey}\` says when a hole's HTML arrives. (\`${this.#wakeKey}\` is when an island's JS runs; a hole ships no JS.)`);
1824
+ if (marker.fetch != null) throw new OgygiaError("held_hole_timed_twice", {
1825
+ host: this.#rel_host,
1826
+ render_key: renderKey,
1827
+ fetch_key: fetchKey,
1828
+ wake_key: this.#wakeKey,
1829
+ fetch: marker.fetch,
1830
+ wake: marker.wake
1831
+ });
1832
+ const wake = marker.wake;
1833
+ warn_once(`deferred-wake:${this.#rel_host}:ts:${wake}`, () => warn("hole_wake_deprecated", {
1834
+ host: this.#rel_host,
1835
+ render_key: renderKey,
1836
+ wake_key: this.#wakeKey,
1837
+ fetch_key: fetchKey,
1838
+ value: wake
1839
+ }));
1741
1840
  }
1742
1841
  const dval = marker.fetch ?? marker.wake ?? "load";
1743
1842
  let when;
1744
1843
  if (DEFER_STRATEGIES.has(dval)) when = dval;
1745
1844
  else if (is_media_query(dval)) when = dval;
1746
- else throw new Error(`[ogygia] ${this.#rel_host}: \`${fetchKey}: '${dval}'\` is not a timing. Use \`${fetchKey}: 'load' | 'idle' | 'visible' | 'interaction'\` or a media query (a hole must fetch — not 'none').`);
1845
+ else throw new OgygiaError("held_fetch_not_timing", {
1846
+ host: this.#rel_host,
1847
+ fetch_key: fetchKey,
1848
+ value: dval
1849
+ });
1747
1850
  const options = { when };
1748
1851
  if (when === "visible" && this.#ctx.visibleMargin != null) options.margin = this.#ctx.visibleMargin;
1749
1852
  const identity = regionIdentity(id_base, {
@@ -1853,9 +1956,20 @@ var TsRegionCompilation = class {
1853
1956
  const has_fetch = attrs.has(fetchKey);
1854
1957
  if (!has_render && !has_region && !has_wake && !has_fetch) continue;
1855
1958
  if (inside_literal(m.index)) continue;
1856
- if (has_region && attrs.size > 1 || has_render && [...attrs.keys()].some((k) => k !== renderKey && k !== wakeKey && k !== fetchKey) || !has_render && has_wake && attrs.size > 1) throw new Error(`[ogygia] ${rel_host}: a held-region import on '${local}' takes ONE marker — \`${regionKey}: 'raw'\`, \`${wakeKey}: '…'\`, or \`${renderKey}: 'deferred'\` (with \`${fetchKey}\` as its timing).`);
1959
+ if (has_region && attrs.size > 1 || has_render && [...attrs.keys()].some((k) => k !== renderKey && k !== wakeKey && k !== fetchKey) || !has_render && has_wake && attrs.size > 1) throw new OgygiaError("held_marker_mixed", {
1960
+ host: rel_host,
1961
+ name: local,
1962
+ region_key: regionKey,
1963
+ wake_key: wakeKey,
1964
+ render_key: renderKey,
1965
+ fetch_key: fetchKey
1966
+ });
1857
1967
  const componentPath = this.#resolve_spec(spec);
1858
- if (!componentPath) throw new Error(`[ogygia] ${rel_host}: held-region import '${local}' needs a module specifier ($lib/…, relative, or a package specifier like 'pkg/component').`);
1968
+ if (!componentPath) throw new OgygiaError("region_import_no_specifier", {
1969
+ host: rel_host,
1970
+ name: local,
1971
+ held: true
1972
+ });
1859
1973
  const marker = has_render ? {
1860
1974
  render: attrs.get(renderKey),
1861
1975
  wake: attrs.get(wakeKey),
@@ -1,12 +1,13 @@
1
- import { batch } from "./ref-watch.js";
2
1
  import { REF_WIRE_KEY, ref_reducer, ref_reviver } from "./ref.js";
2
+ import { batch } from "./ref-watch.js";
3
3
  import { register_wire_kind } from "./live-transport.js";
4
4
  import { register_derived_kind, register_store_kind } from "./store-transport.js";
5
5
  import { register_snippet_kind } from "./region-snippet.js";
6
6
  import { is_branded_fn, register_fn_kind } from "./fn-transport.js";
7
- import { boundary_problems, format_boundary_finding } from "./boundary.js";
7
+ import { boundary_problems } from "./boundary.js";
8
8
  import { PAGE_CTX_MARKER, record_ctx } from "./context-registry.js";
9
9
  import { escape_script_text } from "./escape.js";
10
+ import { warn } from "./messages/island.js";
10
11
  import { getContext, setContext as setContext$1 } from "svelte";
11
12
  import { DEV } from "esm-env";
12
13
  import { parse, stringify } from "devalue";
@@ -102,16 +103,23 @@ function warn_ctx_size(map, total) {
102
103
  } catch {}
103
104
  sizes.sort((a, b) => b[1] - a[1]);
104
105
  const top = sizes.slice(0, 3).map(([k, n]) => `'${k}' ~${(n / 1024).toFixed(1)}kB`).join(", ");
105
- console.warn(`[ogygia] the island context marker is ${(total / 1024).toFixed(1)}kB — it ships in every page's HTML. Biggest keys: ${top}. Slim the values, or mark host-only keys with setContext(key, value, { islands: false }).`);
106
+ warn("context_marker_large", {
107
+ size: (total / 1024).toFixed(1),
108
+ top
109
+ });
106
110
  }
107
111
  /** DEV: explain a dropped (or degraded-but-bridged) context key via the classifier. */
108
112
  function warn_boundary(key, value, dropped) {
109
113
  const problems = boundary_problems(value, key);
110
114
  if (dropped && problems.length === 0) {
111
- console.warn(`[ogygia] context '${key}' could not be serialized for islands and was dropped.`);
115
+ warn("context_dropped", { key });
112
116
  return;
113
117
  }
114
- for (const f of problems) if (dropped || f.kind === "warn") console.warn(format_boundary_finding(key, f));
118
+ for (const f of problems) if (dropped || f.kind === "warn") warn("context_unserializable", {
119
+ key,
120
+ path: f.path || void 0,
121
+ detail: f.detail
122
+ });
115
123
  }
116
124
  /** Decode one serialized provider payload; a corrupt payload yields nothing (never breaks hydration). */
117
125
  function parse_ctx(text) {
package/dist/context.d.ts CHANGED
@@ -60,4 +60,13 @@ export declare function claim_region_css(hrefs: string[]): string[];
60
60
  * Client / no-request → false (SSR already emitted).
61
61
  */
62
62
  export declare function claimRuntimeEmit(): boolean;
63
+ /**
64
+ * Note a top-level `defer: 'load'` hole's endpoint for this request: the head starts its request
65
+ * during the parse (early-holes.ts), written once by the handle or `document()`, which take the
66
+ * list. Identical holes share one endpoint, and one request. Client / no-request: nothing to note.
67
+ */
68
+ export declare function note_early_hole(endpoint: string): void;
69
+ /** This request's noted `load` hole endpoints, taken once (the head that writes them). `request`: the
70
+ * one the caller holds (the handle's event); else the current render's. */
71
+ export declare function take_early_holes(request?: Request): string[];
63
72
  export { isNested, setNested };
package/dist/context.js CHANGED
@@ -140,4 +140,33 @@ function claimRuntimeEmit() {
140
140
  return false;
141
141
  }
142
142
  }
143
- export { claimRuntimeEmit, claim_kit_island, claim_region_css, documentIsCsrTrue, error_route_is_csr_true, isHoleInline, isInLake, isInLiveSnippet, isNested, isOgygiaPage, route_is_csr_true, setHoleInline, setInLake, setInLiveSnippet, setNested };
143
+ /** Each request's noted `load` hole endpoints, by its `Request`: the render's event and the handle's
144
+ * are not one object, the request they carry is. */
145
+ const early_holes = /* @__PURE__ */ new WeakMap();
146
+ /**
147
+ * Note a top-level `defer: 'load'` hole's endpoint for this request: the head starts its request
148
+ * during the parse (early-holes.ts), written once by the handle or `document()`, which take the
149
+ * list. Identical holes share one endpoint, and one request. Client / no-request: nothing to note.
150
+ */
151
+ function note_early_hole(endpoint) {
152
+ try {
153
+ const { request } = getRequestEvent();
154
+ let seen = early_holes.get(request);
155
+ if (!seen) early_holes.set(request, seen = /* @__PURE__ */ new Set());
156
+ seen.add(endpoint);
157
+ } catch {}
158
+ }
159
+ /** This request's noted `load` hole endpoints, taken once (the head that writes them). `request`: the
160
+ * one the caller holds (the handle's event); else the current render's. */
161
+ function take_early_holes(request) {
162
+ if (!request) try {
163
+ request = getRequestEvent().request;
164
+ } catch {
165
+ return [];
166
+ }
167
+ const seen = early_holes.get(request);
168
+ if (!seen) return [];
169
+ early_holes.delete(request);
170
+ return [...seen];
171
+ }
172
+ export { claimRuntimeEmit, claim_kit_island, claim_region_css, documentIsCsrTrue, error_route_is_csr_true, isHoleInline, isInLake, isInLiveSnippet, isNested, isOgygiaPage, note_early_hole, route_is_csr_true, setHoleInline, setInLake, setInLiveSnippet, setNested, take_early_holes };
@@ -13,7 +13,7 @@ export interface DocumentOptions {
13
13
  lang?: string;
14
14
  /** Seed for `$app/state` inside islands (`page.data`/`params`/`url`, `route.id`, `form`). The router
15
15
  * passes it so islands on a rendered page read `$page` just like on a Kit-served page. Promises in
16
- * `data`/`form` aren't streamed here — a non-serializable leaf drops that one field. */
16
+ * `data`/`form` aren't streamed here — a non-serializable leaf is left out where it sits. */
17
17
  pageState?: {
18
18
  url?: {
19
19
  href?: string;
package/dist/document.js CHANGED
@@ -8,6 +8,9 @@ import { kit_render_context } from "./server/kit-context.js";
8
8
  import { open_ask_scope, read_seed_ask, record_page } from "./page-seed-registry.js";
9
9
  import { shape_page_data } from "./server/seed-shape.js";
10
10
  import { escape_text } from "./escape.js";
11
+ import { take_early_holes } from "./context.js";
12
+ import { early_holes_head } from "./early-holes-head.js";
13
+ import { add_early_holes_csp, with_early_holes_meta_csp } from "./server/early-holes-csp.js";
11
14
  import { page_declares_dev_hmr_script, page_declares_router_meta, page_declares_runtime_script, runtime_first } from "./server/head-presence.js";
12
15
  import { render } from "svelte/server";
13
16
  import { stringify } from "devalue";
@@ -95,6 +98,8 @@ async function document(of, options = {}) {
95
98
  if (enabled && !page_declares_router_meta(head_so_far())) head.push(`<meta name="ogygia-router" content="${viewTransitions ? "vt" : "plain"}">`);
96
99
  if (runtime_url && !page_declares_runtime_script(head_so_far())) head.push(runtime_bootstrap());
97
100
  if (dev_hmr_url && !page_declares_dev_hmr_script(head_so_far())) head.push(`<script type="module" data-ogygia-dev-hmr src="${dev_hmr_url}"><\/script>`);
101
+ const early = take_early_holes();
102
+ head.push(early_holes_head(early));
98
103
  const ask = options.pageState ? read_seed_ask() : void 0;
99
104
  if (options.pageState && (!ask || ask.wanted)) {
100
105
  const s = options.pageState;
@@ -113,10 +118,11 @@ async function document(of, options = {}) {
113
118
  head.push(`<script type="application/ogygia-page" data-ogygia-page${format}>${payload.text}<\/script>`);
114
119
  }
115
120
  }
116
- const html = `<!doctype html>\n<html lang="${escape_text(options.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` + runtime_first(head.join(""), null) + `</head><body>${r.body}</body></html>`;
121
+ const html = `<!doctype html>\n<html lang="${escape_text(options.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` + (early.length ? with_early_holes_meta_csp(runtime_first(head.join(""), null)) : runtime_first(head.join(""), null)) + `</head><body>${r.body}</body></html>`;
117
122
  const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
118
123
  if (!current_facts()?.storable) headers.set("cache-control", "no-store");
119
124
  if (options.headers) for (const [k, v] of new Headers(options.headers)) headers.set(k, v);
125
+ if (early.length) add_early_holes_csp(headers);
120
126
  return new Response(html, {
121
127
  status: options.status ?? 200,
122
128
  headers
@@ -0,0 +1,5 @@
1
+ /** The early start's script tag, the same bytes on every page (its CSP hash is computed from it). */
2
+ export declare const EARLY_HOLES_TAG: string;
3
+ /** The head's early start for `endpoints` (the request's `load` holes, each once): the data block,
4
+ * then the script that reads it. Nothing for none. */
5
+ export declare function early_holes_head(endpoints: readonly string[]): string;
@@ -0,0 +1,20 @@
1
+ import { EARLY_HOLES_ID, EARLY_HOLES_KEY, start_early_holes } from "./early-holes.js";
2
+ import { script } from "./script.js";
3
+ // src/early-holes-head.ts
4
+ /**
5
+ * The head's early start (./early-holes.ts), as tags: written by the handle and `document()`, and
6
+ * read by the tests that run it. Kept apart from ./early-holes.ts so the runtime, which only reads
7
+ * the global, never loads `script()`.
8
+ */
9
+ /** The early start's script tag, the same bytes on every page (its CSP hash is computed from it). */
10
+ const EARLY_HOLES_TAG = script(start_early_holes, EARLY_HOLES_ID, EARLY_HOLES_KEY);
11
+ /** The head's early start for `endpoints` (the request's `load` holes, each once): the data block,
12
+ * then the script that reads it. Nothing for none. */
13
+ function early_holes_head(endpoints) {
14
+ if (endpoints.length === 0) return "";
15
+ return script({
16
+ json: [...endpoints],
17
+ id: EARLY_HOLES_ID
18
+ }) + EARLY_HOLES_TAG;
19
+ }
20
+ export { EARLY_HOLES_TAG, early_holes_head };