@ogygia/core 0.12.3 → 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 (226) 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 +17 -10
  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.js +42 -14
  217. package/dist/vite/style-preprocess.js +10 -3
  218. package/package.json +8 -1
  219. package/dist/KeepHost.svelte +0 -19
  220. package/dist/KeepHost.svelte.d.ts +0 -10
  221. package/dist/LiveHost.svelte +0 -19
  222. package/dist/LiveHost.svelte.d.ts +0 -10
  223. package/dist/NestedProvider.svelte +0 -13
  224. package/dist/NestedProvider.svelte.d.ts +0 -8
  225. package/dist/runtime/page-cache.d.ts +0 -23
  226. package/dist/runtime/page-cache.js +0 -74
@@ -0,0 +1,64 @@
1
+ import { Rules } from "../rules.js";
2
+ import { warn } from "../messages/server.js";
3
+ import { observe } from "@ogygia/machine";
4
+ // src/server/rules.ts
5
+ /**
6
+ * THE SERVER'S RULES (#313, ../rules.ts): what is always true of the server, checked a task after
7
+ * each response ends (in dev, and in a build made with `OGYGIA_RULES=1`), printed with its code.
8
+ *
9
+ * - `rule_request_scope_open`: a request's scope (its timers, streams, fetches) is closed once its
10
+ * response has ended.
11
+ * - `rule_flight_kept_failure`: no flight keeps a failure: an answer kept is never an error.
12
+ *
13
+ * Each area tells this module what it holds (`note_response_end`) only when rules are on: every call
14
+ * site folds out of a build without them.
15
+ */
16
+ /** The server's rules. */
17
+ const server_rules = /* @__PURE__ */ new Rules();
18
+ /** Request scopes whose response has ended, until a check sees them closed. */
19
+ const ended = /* @__PURE__ */ new Set();
20
+ /** A response ended (its body read to the end, cancelled, or none): its request's scope must be
21
+ * closed by the time the next check runs. */
22
+ function note_response_end(owner) {
23
+ if (!installed) return;
24
+ ended.add(owner);
25
+ check_soon();
26
+ }
27
+ function request_scopes_closed() {
28
+ let open = 0;
29
+ for (const owner of ended) if (owner.closed) ended.delete(owner);
30
+ else open++;
31
+ return open === 0 ? null : `${open} request scope(s) still open after their response ended`;
32
+ }
33
+ /** The flights' entries that keep an error as their answer now, each with what it keeps. An entry
34
+ * leaves when it moves on (dropped, or a fresh answer kept): the rule is about what is kept now, so
35
+ * a check (a simulated run, the next response in dev) sees its own breaks, not an earlier one's. */
36
+ const kept_failures = /* @__PURE__ */ new Map();
37
+ function no_kept_failure() {
38
+ if (kept_failures.size === 0) return null;
39
+ return [...kept_failures.values()].slice(0, 10).join("; ");
40
+ }
41
+ let installed = false;
42
+ let pending = false;
43
+ /** Checks every rule a task from now (once, however many responses ask), and says what broke. */
44
+ function check_soon() {
45
+ if (pending) return;
46
+ pending = true;
47
+ setTimeout(() => {
48
+ pending = false;
49
+ server_rules.say((code, detail) => warn(code, { detail }));
50
+ }, 0);
51
+ }
52
+ /** Rules on: checked after each response, and what broke is said with its code. */
53
+ function install_server_rules() {
54
+ if (installed) return;
55
+ installed = true;
56
+ server_rules.add("rule_request_scope_open", request_scopes_closed);
57
+ server_rules.add("rule_flight_kept_failure", no_kept_failure);
58
+ observe((name, _from, to, machine) => {
59
+ const answer = to?.kind === "kept" ? to.answer : void 0;
60
+ if (answer instanceof Error) kept_failures.set(machine, `"${name}" kept ${answer.name}: ${answer.message.slice(0, 120)}`);
61
+ else if (kept_failures.size !== 0) kept_failures.delete(machine);
62
+ });
63
+ }
64
+ export { install_server_rules, note_response_end, server_rules };
@@ -1,3 +1,4 @@
1
+ import { OgygiaError, warn } from "../messages/server.js";
1
2
  import { buildHadSecret, buildKey } from "virtual:ogygia/secret";
2
3
  import { sign } from "virtual:ogygia/sign";
3
4
  import { building } from "$app/env";
@@ -24,23 +25,27 @@ const SOURCE_NAME = {
24
25
  env: "OGYGIA_SECRET",
25
26
  build: "this build's random key"
26
27
  };
28
+ const slots = globalThis;
27
29
  function slot() {
28
- const g = globalThis;
29
- return g[SLOT] ??= {};
30
+ return slots[SLOT] ?? { kind: "undecided" };
30
31
  }
31
32
  function signing_key() {
32
33
  const s = slot();
33
- if (s.key) return s.key;
34
+ if (s.kind === "decided") return s.key;
34
35
  const from_env = typeof process !== "undefined" && process.env?.OGYGIA_SECRET || "";
35
- const [secret, source] = s.option ? [s.option, "option"] : from_env ? [from_env, "env"] : [buildKey, "build"];
36
- s.key = {
36
+ const [secret, source] = s.kind === "secret" ? [s.option, "option"] : from_env ? [from_env, "env"] : [buildKey, "build"];
37
+ const key = {
37
38
  secret,
38
39
  stable: source !== "build",
39
40
  fp: key_fingerprint(secret),
40
41
  source
41
42
  };
42
- if (!s.key.stable && !import.meta.env.DEV) warn_build_key();
43
- return s.key;
43
+ slots[SLOT] = {
44
+ kind: "decided",
45
+ key
46
+ };
47
+ if (!key.stable && !import.meta.env.DEV) warn_build_key();
48
+ return key;
44
49
  }
45
50
  /**
46
51
  * Record the app's `handle({ secret })`. A blank one counts as none (an app can pass a value that may
@@ -50,15 +55,23 @@ function signing_key() {
50
55
  function configure_signing_secret(secret) {
51
56
  if (!secret?.trim()) return;
52
57
  const s = slot();
53
- const decided = s.key?.secret ?? s.option;
54
- if (decided === void 0 || decided === secret) {
55
- s.option = secret;
58
+ if (s.kind === "undecided") {
59
+ slots[SLOT] = {
60
+ kind: "secret",
61
+ option: secret
62
+ };
56
63
  return;
57
64
  }
58
- const from = s.key ? `${SOURCE_NAME[s.key.source]}, decided by an earlier mint or verify` : "another handle({ secret })";
59
- const message = `[ogygia] handle({ secret }) names key ${key_fingerprint(secret)}, but this server already signs with key ${key_fingerprint(decided)} (from ${from}). The first key stays: pass one secret, before anything renders.`;
60
- if (import.meta.env.DEV) throw new Error(message);
61
- console.error(message);
65
+ const decided = s.kind === "decided" ? s.key.secret : s.option;
66
+ if (decided === secret) return;
67
+ const from = s.kind === "decided" ? `${SOURCE_NAME[s.key.source]}, decided by an earlier mint or verify` : "another handle({ secret })";
68
+ const error = new OgygiaError("secret_conflict", {
69
+ key: key_fingerprint(secret),
70
+ decided: key_fingerprint(decided),
71
+ from
72
+ });
73
+ if (import.meta.env.DEV) throw error;
74
+ console.error(error);
62
75
  }
63
76
  function key_fingerprint(secret) {
64
77
  return sign(secret, "ogygia:key-fp").slice(0, 8);
@@ -70,12 +83,12 @@ function warn_build_key() {
70
83
  const K = Symbol.for("ogygia.build-key-warned");
71
84
  if (g[K]) return;
72
85
  g[K] = true;
73
- if (building) console.warn("[ogygia] prerendered page mints region capabilities signed with a per-build random key — they stop verifying after your next deploy while the static HTML lives on. Set a stable OGYGIA_SECRET env var (or pass it to handle({ secret })) so prerendered holes survive redeploys.");
74
- else if (buildHadSecret) console.error("[ogygia] OGYGIA_SECRET was set when this app was built but is not set where it runs: region capabilities are signed with this build's random key, so every hole minted by another deployment (another backend behind the same CDN, the previous deploy) is refused (403, x-ogygia-deny: foreign-key). Set OGYGIA_SECRET in the server's runtime environment, or pass it to handle({ secret }) (e.g. from $env/static/private).");
75
- else console.warn("[ogygia] OGYGIA_SECRET is not set: region capabilities are signed with this build's random key, so a page cached across a deploy, or served by another deployment than its islands endpoint, gets its holes refused (403, x-ogygia-deny: foreign-key). Set a stable OGYGIA_SECRET, or pass one to handle({ secret }).");
86
+ if (building) warn("build_key_prerender");
87
+ else if (buildHadSecret) console.error(new OgygiaError("secret_missing_at_runtime"));
88
+ else warn("secret_missing");
76
89
  }
77
90
  /** Tests: forget the decided key and the app's secret. */
78
91
  function reset_signing_key() {
79
- delete globalThis[SLOT];
92
+ delete slots[SLOT];
80
93
  }
81
94
  export { configure_signing_secret, key_fingerprint, reset_signing_key, signing_key };
@@ -1,5 +1,18 @@
1
+ import { Scope } from "@ogygia/machine";
1
2
  /** Build a parcel `<template data-ogygia-slot="…">…rendered html…</template>` (length-framed when
2
3
  * the html carries a `</template` of its own). */
3
4
  export declare function build_parcel(slot: string, html: string): string;
4
5
  /** The done-sentinel parcel (empty template) the client watches to end the batch. */
5
- export declare function done_parcel(): string;
6
+ export declare function done_parcel(): string;
7
+ /** One region call's answer in a batch: its slot (the capability signature) and its HTML. Null:
8
+ * the call is left out (the client fetches it on its own). */
9
+ export type BatchAnswer = {
10
+ slot: string;
11
+ html: string;
12
+ } | null;
13
+ /**
14
+ * The batch's frame stream: every call renders at once, and each answer goes out as its parcel the
15
+ * moment it settles, whatever the order; the done sentinel ends it. `render` is handed the batch's
16
+ * signal: it aborts when the stream is cancelled, or when `owner` (the request's) ends first.
17
+ */
18
+ export declare function batch_stream(calls: readonly string[], render: (endpoint: string, signal: AbortSignal) => Promise<BatchAnswer>, owner?: Scope): ReadableStream<Uint8Array>;
@@ -1,3 +1,4 @@
1
+ import { STALE, machine } from "@ogygia/machine";
1
2
  // src/server/stream-regions.ts
2
3
  /**
3
4
  * Region parcels for the batch endpoint (single-flight navigation, a page's first-load holes, OOO).
@@ -11,7 +12,7 @@
11
12
  * decoded string counts in — so its content can never close the box or forge a parcel for another
12
13
  * slot, and it no longer falls back to a second request.
13
14
  *
14
- * Pure (no I/O, no Kit) so it stays unit-testable.
15
+ * No I/O and no Kit (the batch stream is handed its render), so it stays unit-testable.
15
16
  */
16
17
  /** Sentinel slot appended once a batch finishes, so waiting regions know it is complete. */
17
18
  const STREAM_DONE_SLOT = "__ogygia_done__";
@@ -33,4 +34,39 @@ function build_parcel(slot, html) {
33
34
  function done_parcel() {
34
35
  return `<template data-ogygia-slot="${STREAM_DONE_SLOT}"></template>`;
35
36
  }
36
- export { build_parcel, done_parcel };
37
+ const batch_def = machine()({
38
+ name: "batch stream",
39
+ initial: "streaming",
40
+ steps: {
41
+ streaming: ["ended", "cancelled"],
42
+ ended: [],
43
+ cancelled: []
44
+ }
45
+ });
46
+ /**
47
+ * The batch's frame stream: every call renders at once, and each answer goes out as its parcel the
48
+ * moment it settles, whatever the order; the done sentinel ends it. `render` is handed the batch's
49
+ * signal: it aborts when the stream is cancelled, or when `owner` (the request's) ends first.
50
+ */
51
+ function batch_stream(calls, render, owner) {
52
+ const encoder = new TextEncoder();
53
+ const batch = batch_def.create();
54
+ owner?.defer(() => {
55
+ batch.move("streaming", "cancelled");
56
+ });
57
+ return new ReadableStream({
58
+ async start(controller) {
59
+ const streaming = batch.now;
60
+ if (await batch.scope.task((signal) => Promise.all(calls.map(async (endpoint) => {
61
+ const out = await render(endpoint, signal).catch(() => null);
62
+ if (out && batch.now === streaming) controller.enqueue(encoder.encode(build_parcel(out.slot, out.html)));
63
+ }))) === STALE || !batch.move("streaming", "ended")) return;
64
+ controller.enqueue(encoder.encode(done_parcel()));
65
+ controller.close();
66
+ },
67
+ cancel() {
68
+ batch.move("streaming", "cancelled");
69
+ }
70
+ });
71
+ }
72
+ export { batch_stream, build_parcel, done_parcel };
@@ -1,3 +1,4 @@
1
+ import { OgygiaError } from "./messages/island.js";
1
2
  import { parse, stringify } from "devalue";
2
3
  import { createSubscriber } from "svelte/reactivity";
3
4
  // src/shared-state.ts
@@ -46,7 +47,10 @@ function snapshot(name, value) {
46
47
  try {
47
48
  return parse(stringify(value));
48
49
  } catch (e) {
49
- throw new Error(`[ogygia] SharedState '${name}': values must be plain data (devalue-serializable — JSON, Date, Map, Set, BigInt, RegExp). Functions, class instances, and DOM nodes do not cross build boundaries — share facts, not live objects. (${e instanceof Error ? e.message : e})`);
50
+ throw new OgygiaError("shared_state_not_plain", {
51
+ name,
52
+ reason: e instanceof Error ? e.message : String(e)
53
+ });
50
54
  }
51
55
  }
52
56
  var SharedState = class {
@@ -1,14 +1,15 @@
1
1
  import { kit_bridge } from "./kit-bridge.js";
2
2
  import { refresh_remote_instances } from "./kit-remote/remote-cache.js";
3
3
  import { bind_to_component } from "../runtime/nav-hooks.js";
4
+ import { OgygiaError } from "../messages/island.js";
4
5
  // src/shims/app-navigation.ts
5
6
  const kit = () => kit_bridge()?.navigation ?? null;
6
7
  const og = () => globalThis[Symbol.for("ogygia.nav")] ?? null;
7
8
  /** No runtime on this document at all (neither Kit's nor ogygia's): the browser navigates. */
8
9
  function browser_goto(url, opts = {}) {
9
10
  const target = new URL(url, location.href);
10
- if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error("[ogygia] goto() only supports http(s) URLs");
11
- if (target.origin !== location.origin && !opts.external) throw new Error("[ogygia] goto() only supports same-origin URLs (pass { external: true } to leave)");
11
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new OgygiaError("goto_not_http");
12
+ if (target.origin !== location.origin && !opts.external) throw new OgygiaError("goto_cross_origin");
12
13
  if (opts.replaceState) location.replace(target.href);
13
14
  else location.assign(target.href);
14
15
  return Promise.resolve();
@@ -1,4 +1,5 @@
1
1
  import { foreign_hydrate } from "../current-region.js";
2
+ import { warn } from "../messages/island.js";
2
3
  // src/shims/page-store.svelte.ts
3
4
  const FALLBACK = {
4
5
  url: typeof location !== "undefined" ? new URL(location.href) : new URL("http://localhost/"),
@@ -32,7 +33,9 @@ var PageState = class {
32
33
  this.state = s.state ?? {};
33
34
  for (const fn of this.#subscribers) try {
34
35
  fn();
35
- } catch {}
36
+ } catch (error) {
37
+ report_thrown(error);
38
+ }
36
39
  }
37
40
  reset() {
38
41
  this.set({
@@ -51,6 +54,13 @@ var PageState = class {
51
54
  return () => this.#subscribers.delete(fn);
52
55
  }
53
56
  };
57
+ /** report-thrown.ts's `report_thrown`, here in full: island-side code never imports a module the
58
+ * runtime's boot imports (runtime-boot-svelte-free.test.ts), and the boot's ref watchers use it. */
59
+ function report_thrown(error) {
60
+ const report = globalThis.reportError;
61
+ if (typeof report === "function") report(error);
62
+ else console.error(error);
63
+ }
54
64
  const PAGE_STATE_KEY = Symbol.for("ogygia.page-state");
55
65
  const global_scope = globalThis;
56
66
  const page_state = global_scope[PAGE_STATE_KEY] ??= new PageState();
@@ -72,6 +82,10 @@ function warn_foreign_page_read(expr) {
72
82
  const f = foreign_hydrate();
73
83
  if (!f || warned_foreign_entries.has(f.entry)) return;
74
84
  warned_foreign_entries.add(f.entry);
75
- console.warn(`[ogygia] ${expr} was read inside a mounted MFE island (${f.entry}, from ${f.origin}).\nIn the browser this is the SHELL's page, not the MFE's. The HTML you see was rendered with the MFE's own load data, so this island will repaint with different values after hydrate.\nPass the value as a prop, or read it with a remote function.`);
85
+ warn("foreign_page_read", {
86
+ expr,
87
+ entry: f.entry,
88
+ origin: f.origin
89
+ });
76
90
  }
77
91
  export { PageState, page_state, reset_page, set_page, subscribe_page, warn_foreign_page_read };
package/dist/source.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fnv1a } from "./runtime/hash.js";
2
+ import { report_thrown } from "./report-thrown.js";
2
3
  // src/source.ts
3
4
  /**
4
5
  * DATA SOURCES — `import.meta.og.source(fn)`, the RUNTIME half (the compiler rewrites the macro to
@@ -29,10 +30,14 @@ function set_source_observer(fn) {
29
30
  function record_source_read(tag) {
30
31
  try {
31
32
  slots.recorder?.(tag);
32
- } catch {}
33
+ } catch (error) {
34
+ report_thrown(error);
35
+ }
33
36
  try {
34
37
  slots.observer?.(tag);
35
- } catch {}
38
+ } catch (error) {
39
+ report_thrown(error);
40
+ }
36
41
  }
37
42
  /** The identity stamp — read by `postrender.invalidate(fn, args)`. */
38
43
  const SOURCE_ID = Symbol.for("ogygia.source.id");
@@ -1,5 +1,6 @@
1
1
  import { wire } from "./live-transport.js";
2
2
  import { mint, register_kind, resolve } from "./ref.js";
3
+ import { OgygiaError, warn } from "./messages/island.js";
3
4
  import { derived, get, writable } from "svelte/store";
4
5
  // src/store-transport.ts
5
6
  /**
@@ -81,7 +82,7 @@ function mark_store(store, tag) {
81
82
  enumerable: false
82
83
  });
83
84
  } catch {
84
- if (typeof console !== "undefined") console.warn(`[ogygia] store "${tag}": the store object is not extensible (frozen/sealed), so it cannot carry its factory brand — it will cross boundaries as a plain writable (custom methods unavailable). Brand before freezing, or leave the store extensible.`);
85
+ if (typeof console !== "undefined") warn("store_not_extensible", { tag });
85
86
  }
86
87
  return store;
87
88
  }
@@ -100,12 +101,15 @@ function register_store_kind() {
100
101
  const t = ref.t ?? GENERIC_TAG;
101
102
  const factory = t === GENERIC_TAG ? void 0 : store_registry().factories.get(t);
102
103
  if (t !== GENERIC_TAG && factory === void 0) {
103
- if (typeof console !== "undefined") console.warn(`[ogygia] store "${t}": factory not loaded in this bundle — degraded to a plain writable (custom methods unavailable). Import the factory's module in the island (or its graph) so \`__register_store_factory\` runs there.`);
104
+ if (typeof console !== "undefined") warn("store_factory_missing", { tag: t });
104
105
  return writable(ref.d);
105
106
  }
106
107
  const built = factory ? factory(ref.d) : writable(ref.d);
107
108
  if (built === null || typeof built !== "object") {
108
- if (typeof console !== "undefined") console.warn(`[ogygia] store "${t}": factory returned ${type_of(built)} while rebuilding — degraded to a plain writable (custom methods unavailable). A store factory must return a store on every call; move environment guards inside the factory body.`);
109
+ if (typeof console !== "undefined") warn("store_factory_bad_rebuild", {
110
+ tag: t,
111
+ got: type_of(built)
112
+ });
109
113
  return writable(ref.d);
110
114
  }
111
115
  return built;
@@ -160,7 +164,10 @@ function __og_store(tag, factory) {
160
164
  if (product === null || typeof product !== "object") {
161
165
  if (!warned_unbranded.has(tag) && typeof console !== "undefined") {
162
166
  warned_unbranded.add(tag);
163
- console.warn(`[ogygia] store "${tag}": factory returned ${type_of(product)} — nothing to brand, value passed through as-is. Fine for a client-only store; a store that must cross an island boundary has to exist on the server too — return e.g. writable(seed) under your guard and gate the side effects inside the factory instead.`);
167
+ warn("store_factory_unbranded", {
168
+ tag,
169
+ got: type_of(product)
170
+ });
164
171
  }
165
172
  return product;
166
173
  }
@@ -221,7 +228,7 @@ function register_derived_kind() {
221
228
  },
222
229
  decode(ref) {
223
230
  const d = ref.d;
224
- if (typeof d.f !== "function") throw new Error("[ogygia] cannot resume og_derived: its formula did not cross — mark it with import.meta.og.$ at the og_derived call site so it travels as a fn ref.");
231
+ if (typeof d.f !== "function") throw new OgygiaError("og_derived_formula_missing");
225
232
  return og_derived(d.single ? d.s[0] : d.s, d.f, d.seed);
226
233
  }
227
234
  });
package/dist/transport.js CHANGED
@@ -6,6 +6,7 @@ import { register_wire_kind } from "./live-transport.js";
6
6
  import { register_derived_kind, register_store_kind } from "./store-transport.js";
7
7
  import { register_snippet_kind } from "./region-snippet.js";
8
8
  import { register_fn_kind } from "./fn-transport.js";
9
+ import { OgygiaError } from "./messages/island.js";
9
10
  // src/transport.ts
10
11
  /**
11
12
  * `ogygiaTransport` — the SvelteKit `transport` entry that lets a held region cross the wire.
@@ -38,7 +39,7 @@ function encode_region(value) {
38
39
  return pack(f.id, props, sign(f.id, props), f.module, f.hydrate, f.hydrateMargin, f.html);
39
40
  }
40
41
  if (typeof f.html === "string") return pack("", f.props ?? {}, "", "", void 0, void 0, f.html);
41
- throw new Error("[ogygia] a held region reached the wire but its component is a plain import, so nothing built a chunk for it. `await` the region before returning it from a remote/load — an awaited region bakes its SSR HTML and crosses as an HTML-only ticket. Or import the component `with { region: 'raw' }` (or a `wake:` mark) to make it a signed, fetchable capability. On a csr=false page you can also skip the wire entirely: call `get()` in a universal `+page.ts` load / the component — its data reaches the render by reference.");
42
+ throw new OgygiaError("held_region_plain_import");
42
43
  }
43
44
  }
44
45
  /** The region DECODE law (single-flight frame write for baked HTML + deferred rebuild),
@@ -88,7 +89,7 @@ function register_region_kind() {
88
89
  },
89
90
  encode(value) {
90
91
  const packed = encode_region(value);
91
- if (packed === false) throw new Error("[ogygia] region kind claimed a non-region value");
92
+ if (packed === false) throw new OgygiaError("region_kind_mismatch");
92
93
  return { d: packed };
93
94
  },
94
95
  decode(ref) {
@@ -4,8 +4,8 @@ import { island_sourcemaps_plugin } from "./sourcemaps.js";
4
4
  import { server_sourcemaps_plugin } from "./server-sourcemaps.js";
5
5
  import { install_dev_client_maps, install_dev_maps, install_dev_page_keys } from "./dev-maps.js";
6
6
  import { CLIENT_BINDING_STUB, DEFAULT_IMPORT_KEYS, asRegionLocals, import_keys_hint, is_island_path, islandChunkFileName, islandId, islandPublicUrl, normalize_import_keys, wrapperVirtualId } from "../compiler/region/transform.js";
7
- import { regionId, regionIdentity, strategyKey } from "../compiler/region/identity.js";
8
7
  import { rewrite_lake_import_to_placeholder } from "../compiler/region/emit.js";
8
+ import { regionId, regionIdentity, strategyKey } from "../compiler/region/identity.js";
9
9
  import { debarrel } from "../compiler/debarrel/plugin.js";
10
10
  import { definePlugin, find_plugins, resolve_plugins } from "./plugin-api.js";
11
11
  import { assert_no_legacy_options, resolve_options, validate_region_presets } from "./options.js";
@@ -34,7 +34,8 @@ import { async_twin_redirect, svelte_async_enabled } from "./late-island.js";
34
34
  import { flags_manifest } from "../compiler/flags.js";
35
35
  import { RESOLVED, V_ROUTER_CSS, V_ROUTE_CONFIG, V_ROUTE_CSR, V_SERVER_ISLANDS, V_SERVER_MANIFEST } from "../compiler/ids.js";
36
36
  import { OWN_PACKAGE_NAMES, is_own_specifier } from "../own-package.js";
37
- import { APP_SHIMS, CLIENT_BINDING_STUB_FILE, HMAC_MODULE, KIT_REMOTE_CLIENT, KIT_REMOTE_STATE, OGYGIA_INJECTED_FILES, OGYGIA_INJECTED_IMPORTS, OGYGIA_REGION_MODULES, PKG_ROOT, PKG_ROOT_ID, REGION_ENDPOINT_MODULE, RUNTIME_DIR, RUNTIME_HASH, STUB_CLIENT, STUB_PATHS, STUB_STATE, real_posix, scan_entry_pattern } from "./paths.js";
37
+ import { APP_SHIMS, CLIENT_BINDING_STUB_FILE, HMAC_MODULE, KIT_REMOTE_CLIENT, KIT_REMOTE_STATE, OGYGIA_INJECTED_FILES, OGYGIA_INJECTED_IMPORTS, OGYGIA_REGION_MODULES, OGYGIA_VERSION, PKG_ROOT, PKG_ROOT_ID, REGION_ENDPOINT_MODULE, RUNTIME_DIR, RUNTIME_HASH, STUB_CLIENT, STUB_PATHS, STUB_STATE, real_posix, scan_entry_pattern } from "./paths.js";
38
+ import { OgygiaError, line, warn } from "../messages/build.js";
38
39
  import crypto from "node:crypto";
39
40
  import fs from "node:fs";
40
41
  import path from "node:path";
@@ -44,6 +45,7 @@ import { loadEnv } from "vite";
44
45
  const KIT_CLIENT_APP_RE = /\/generated\/(?:build\/|dev\/)?client(-optimized)?\/app\.js$/;
45
46
  const ROUTE_HOST_OR_OPTION_RE = /[\\/]\+(page|layout)(\.server)?\.(svelte|js|ts)$/;
46
47
  const KIT_PAGE_THREAD = "\nimport { page as __og_kit_page, navigating as __og_kit_nav } from '$app/state';\nimport * as __og_kit_navigation from '$app/navigation';\nglobalThis[Symbol.for('ogygia.kit-page')] = { page: __og_kit_page, navigating: __og_kit_nav, navigation: __og_kit_navigation };\n";
48
+ const KIT_DISCLOSE_VERSION = `((globalThis.__ogygia ??= {}).v ??= new Set()).add(${JSON.stringify(OGYGIA_VERSION)});\n`;
47
49
  /** css-ish file the dev bridge manages (mirrors the bridge's glob). */
48
50
  const DEV_CSS_FILE_RE = /\.(css|scss|sass|less|styl)$/;
49
51
  const CSS_EXT_RE = /\.css$/;
@@ -290,9 +292,11 @@ function ogygia(options = {}) {
290
292
  __OGYGIA_SERVER_DELTA__: JSON.stringify(server_delta),
291
293
  __OGYGIA_DEVTOOLS__: JSON.stringify(devtools_effective),
292
294
  __OGYGIA_DEVTOOLS_LAZY__: JSON.stringify(devtools_lazy),
295
+ __OGYGIA_RULES__: JSON.stringify(process.env.OGYGIA_RULES === "1"),
293
296
  __OGYGIA_BEACON__: JSON.stringify(measure || devtools_effective),
294
297
  __OGYGIA_CLIENT_PLUGINS__: JSON.stringify(client_plugins.length > 0),
295
- __OGYGIA_KIT_MAJOR__: JSON.stringify(kit_major(path.resolve(userConfig.root ?? ".")))
298
+ __OGYGIA_KIT_MAJOR__: JSON.stringify(kit_major(path.resolve(userConfig.root ?? "."))),
299
+ __OGYGIA_VERSION__: JSON.stringify(OGYGIA_VERSION)
296
300
  },
297
301
  server: { fs: { allow: [PKG_ROOT] } },
298
302
  build: { rolldownOptions: { preserveEntrySignatures: "exports-only" } }
@@ -305,7 +309,10 @@ function ogygia(options = {}) {
305
309
  svelte_async = svelte_async_enabled(config.plugins);
306
310
  if (config.command === "serve") {
307
311
  const own = (config.optimizeDeps?.include ?? []).filter((d) => is_own_specifier(d));
308
- if (own.length) config.logger.warn(`[ogygia] optimizeDeps.include lists ${own.join(", ")}: ogygia must not be pre-bundled in dev (the page would run two copies of its runtime, and islands with snippet props fail to hydrate). Remove ${own.length === 1 ? "it" : "them"}.`);
312
+ if (own.length) config.logger.warn(line("optimize_deps_includes_ogygia", {
313
+ specifiers: own.join(", "),
314
+ count: own.length
315
+ }));
309
316
  }
310
317
  const app_dir_define = config.define?.["__SVELTEKIT_APP_DIR__"];
311
318
  if (typeof app_dir_define === "string") try {
@@ -338,7 +345,7 @@ function ogygia(options = {}) {
338
345
  }
339
346
  const env_secret = process.env.OGYGIA_SECRET?.trim() || "";
340
347
  if (env_secret) {
341
- if (is_build && !secret_has_min_entropy(env_secret)) throw new Error(`[ogygia] OGYGIA_SECRET is too short for production builds (need ≥16 UTF-8 bytes).`);
348
+ if (is_build && !secret_has_min_entropy(env_secret)) throw new OgygiaError("secret_too_short", { min_bytes: 16 });
342
349
  id_salt = derive_id_salt(env_secret);
343
350
  } else id_salt = "";
344
351
  ({kit_wire_path, kit_remote_index, universal_hooks, client_hooks, kit_transport_init} = resolve_kit_paths(root));
@@ -418,7 +425,7 @@ function ogygia(options = {}) {
418
425
  emitRuntime: emit_runtime,
419
426
  loaded
420
427
  });
421
- if (skipped && isMainThread) console.log(`[ogygia] ${skipped} island chunk(s) not emitted: their host modules are not in the server bundle (unreachable from this build's routes).`);
428
+ if (skipped && isMainThread) console.log(line("island_chunks_not_emitted", { count: skipped }));
422
429
  compiled_css.clear();
423
430
  const tree_css = tree_css_walk(this);
424
431
  router_css_refs.clear();
@@ -504,7 +511,7 @@ function ogygia(options = {}) {
504
511
  fs.mkdirSync(path.dirname(handoff), { recursive: true });
505
512
  fs.writeFileSync(handoff, JSON.stringify(compiler.ssr_transformed_hosts()));
506
513
  } catch (e) {
507
- if (isMainThread) console.warn("[ogygia] could not write the server-leg island handoff:", e);
514
+ if (isMainThread) warn("island_handoff_write_failed", e);
508
515
  }
509
516
  if (__P) {
510
517
  const keys = [...__outHash.keys()].sort();
@@ -571,7 +578,7 @@ function ogygia(options = {}) {
571
578
  }
572
579
  const ssr = options?.ssr === true;
573
580
  if (!ssr && source === "__sveltekit/remote") {
574
- if (!kit_remote_index) throw new Error("[ogygia] could not locate Kit's client remote-functions (src). Use an @sveltejs/kit that publishes its `src/` (2.70.2 or later, or 3.x).");
581
+ if (!kit_remote_index) throw new OgygiaError("kit_remote_src_missing");
575
582
  return kit_remote_index;
576
583
  }
577
584
  if (!ssr && importer && importer.includes("/remote-functions/")) {
@@ -641,7 +648,7 @@ function ogygia(options = {}) {
641
648
  },
642
649
  async transform(code, id, options) {
643
650
  let source = code;
644
- if (!options?.ssr && KIT_CLIENT_APP_RE.test(id)) source += KIT_PAGE_THREAD;
651
+ if (!options?.ssr && KIT_CLIENT_APP_RE.test(id)) source += KIT_PAGE_THREAD + KIT_DISCLOSE_VERSION;
645
652
  const result = await compiler.transform_module(source, id, {
646
653
  ssr: options?.ssr === true,
647
654
  emitFile: (chunk) => this.emitFile(chunk)
@@ -739,7 +746,7 @@ function ogygia(options = {}) {
739
746
  fs.mkdirSync(dir, { recursive: true });
740
747
  const manifest = flags_manifest(program.flag_sites);
741
748
  fs.writeFileSync(path.join(dir, "flags-manifest.json"), JSON.stringify(manifest, null, " ") + "\n");
742
- console.log(`[ogygia] flags manifest: ${manifest.names.length} flag(s) → node_modules/.ogygia/flags-manifest.json`);
749
+ console.log(line("flags_manifest_written", { count: manifest.names.length }));
743
750
  } catch {}
744
751
  await follow_pending_page_calls(program, (s, importer) => this.resolve(s, importer));
745
752
  const remote_hash = (id) => remote_hash_of(id, process.cwd());
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_KIT_DIRS, set_kit_dirs, set_kit_inline_style_threshold } from "../compiler/kit.js";
2
+ import { warn } from "../messages/build.js";
2
3
  import { existsSync } from "node:fs";
3
4
  import path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
@@ -47,7 +48,12 @@ async function kit_options_from_file(root) {
47
48
  try {
48
49
  return (await import(pathToFileURL(file).href)).default?.kit ?? {};
49
50
  } catch (e) {
50
- console.warn(`[ogygia] could not read ${path.relative(root, file)} for kit.files.routes / kit.outDir — using the defaults (${DEFAULT_KIT_DIRS.routes}, ${DEFAULT_KIT_DIRS.out}). ${e instanceof Error ? e.message : String(e)}`);
51
+ warn("kit_config_unreadable", {
52
+ file: path.relative(root, file),
53
+ routes: DEFAULT_KIT_DIRS.routes,
54
+ out: DEFAULT_KIT_DIRS.out,
55
+ reason: e instanceof Error ? e.message : String(e)
56
+ });
51
57
  return null;
52
58
  }
53
59
  }
@@ -178,7 +178,8 @@ export interface OgygiaOptions {
178
178
  */
179
179
  standalone?: boolean;
180
180
  }
181
- /** The rename map: a legacy key errors with its new spelling, never silently no-ops. */
181
+ /** The rename map (`RETIRED_OPTIONS`, with the messages): a legacy key errors with its new
182
+ * spelling, never silently no-ops. */
182
183
  export declare function assert_no_legacy_options(options: OgygiaOptions): void;
183
184
  /**
184
185
  * Config-time region-preset validation — the transform re-checks on USE (with file/line context), but
@@ -1,22 +1,9 @@
1
+ import { OgygiaError, RETIRED_OPTIONS } from "../messages/build.js";
1
2
  // src/vite/options.ts
2
- /**
3
- * Retired option spellings. Detected at config load and answered with the exact new spelling — a
4
- * rename map, never silent aliasing (a legacy key that silently did nothing would un-tune real apps).
5
- */
6
- const LEGACY_OPTION_RENAMES = {
7
- regions: "`regions` is gone — its keys sit at the root now: write `visible`, `presets`, `ttl` and `maxPageAge` directly in `ogygia({ … })`.",
8
- continuity: "`continuity` is gone — form continuity rides the router. Write `router: { forms: false }` to disable it.",
9
- regionTtl: "`regionTtl` is `ttl` now — write `ogygia({ ttl: … })`.",
10
- preload: "`preload` is gone — there is nothing to choose any more. The page carries each island’s chunk list as data, and the runtime preloads an island’s whole graph when it wakes (after the page has painted), so no island code moves before its wake and none waits on a waterfall after it. Remove the key.",
11
- plugins: "`plugins` is gone — ogygia plugins are Vite plugins: list them beside `ogygia()` in vite.config, `plugins: [ogygia(), flags(), sveltekit()]`.",
12
- freeze: "`freeze` is a plugin now, named postrender — install `@ogygia/postrender` and list `postrender({ ttl, default })` (from `@ogygia/postrender/vite`) beside `ogygia()` in vite.config `plugins`.",
13
- content: "`content` is a plugin now — install `@ogygia/content` and list `content({ markdown, presets })` (from `@ogygia/content/vite`) beside `ogygia()` in vite.config `plugins`; the svelte config takes `preprocess()` / `extensions()` from there too.",
14
- devtools: "`devtools` is a plugin now — install `@ogygia/devtools` and list `devtools()` (from `@ogygia/devtools/vite`) beside `ogygia()` in vite.config `plugins`; only where you want it, e.g. `...(mode !== 'production' ? [devtools()] : [])`.",
15
- profiler: "`profiler` is part of the devtools plugin now — install `@ogygia/devtools` and list `devtools({ profiler: { … } })` (from `@ogygia/devtools/vite`) beside `ogygia()` in vite.config `plugins`."
16
- };
17
- /** The rename map: a legacy key errors with its new spelling, never silently no-ops. */
3
+ /** The rename map (`RETIRED_OPTIONS`, with the messages): a legacy key errors with its new
4
+ * spelling, never silently no-ops. */
18
5
  function assert_no_legacy_options(options) {
19
- for (const key of Object.keys(LEGACY_OPTION_RENAMES)) if (key in options) throw new Error(`[ogygia] ${LEGACY_OPTION_RENAMES[key]}`);
6
+ for (const key of Object.keys(RETIRED_OPTIONS)) if (key in options) throw new OgygiaError("option_retired", { option: key });
20
7
  }
21
8
  const REGION_PRESET_KEYS = /* @__PURE__ */ new Set([
22
9
  "render",
@@ -36,8 +23,12 @@ const REGION_PRESET_KEYS = /* @__PURE__ */ new Set([
36
23
  */
37
24
  function validate_region_presets(presets) {
38
25
  for (const [name, bag] of Object.entries(presets)) {
39
- if (!bag || typeof bag !== "object" || Object.keys(bag).length === 0) throw new Error(`[ogygia] presets.${name} is empty — a preset with nothing is a mistake.`);
40
- for (const k of Object.keys(bag)) if (!REGION_PRESET_KEYS.has(k)) throw new Error(`[ogygia] presets.${name}: unknown key \`${k}\`. A preset takes ${[...REGION_PRESET_KEYS].join(", ")}.`);
26
+ if (!bag || typeof bag !== "object" || Object.keys(bag).length === 0) throw new OgygiaError("preset_empty", { name });
27
+ for (const k of Object.keys(bag)) if (!REGION_PRESET_KEYS.has(k)) throw new OgygiaError("preset_key_unknown", {
28
+ name,
29
+ key: k,
30
+ allowed: [...REGION_PRESET_KEYS].join(", ")
31
+ });
41
32
  }
42
33
  }
43
34
  /**
@@ -57,7 +48,7 @@ function resolve_options(options, defaultRegionTtl, defaultRenewWindow = 691200)
57
48
  const session_cookie = typeof options.sessionCookie === "string" && options.sessionCookie.length > 0 ? options.sessionCookie : "";
58
49
  const region_ttl = Math.min(86400, Math.max(60, Math.floor(options.ttl ?? defaultRegionTtl)));
59
50
  const renew_raw = options.maxPageAge;
60
- if (renew_raw !== void 0 && !(typeof renew_raw === "number" && Number.isFinite(renew_raw) && renew_raw >= 0)) throw new Error(`[ogygia] maxPageAge must be a number of seconds ≥ 0 (got ${String(renew_raw)}). Write e.g. \`30 * 24 * 3600\` for 30 days, or \`0\` to turn renewal off.`);
51
+ if (renew_raw !== void 0 && !(typeof renew_raw === "number" && Number.isFinite(renew_raw) && renew_raw >= 0)) throw new OgygiaError("max_page_age_invalid", { value: String(renew_raw) });
61
52
  const renew_window = Math.min(31536e4, Math.floor(renew_raw ?? defaultRenewWindow));
62
53
  const router_enabled = options.router !== false;
63
54
  return {
@@ -1,4 +1,5 @@
1
1
  import { OWN_PACKAGE_NAMES } from "../own-package.js";
2
+ import { OgygiaError, warn } from "../messages/build.js";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { globSync } from "tinyglobby";
@@ -37,7 +38,10 @@ function discover_package_files(app_root) {
37
38
  const pkg_dir = path.join(app_root, "node_modules", ...name.split("/"));
38
39
  const declared = (read_manifest(pkg_dir)?.ogygia)?.files;
39
40
  if (declared == null) continue;
40
- if (!Array.isArray(declared) || !declared.every((p) => typeof p === "string")) throw new Error(`[ogygia] ${name}: \`"ogygia": { "files": […] }\` must be an array of package-relative paths/globs (like npm's "files") — got ${JSON.stringify(declared)}.`);
41
+ if (!Array.isArray(declared) || !declared.every((p) => typeof p === "string")) throw new OgygiaError("package_files_not_array", {
42
+ package: name,
43
+ got: JSON.stringify(declared)
44
+ });
41
45
  let root;
42
46
  try {
43
47
  root = fs.realpathSync(pkg_dir);
@@ -61,12 +65,18 @@ function discover_package_files(app_root) {
61
65
  continue;
62
66
  }
63
67
  const abs = path.resolve(root, entry);
64
- if (abs !== root && !abs.startsWith(root + path.sep)) throw new Error(`[ogygia] ${name}: ogygia.files entry '${entry}' escapes the package.`);
68
+ if (abs !== root && !abs.startsWith(root + path.sep)) throw new OgygiaError("package_files_escapes", {
69
+ package: name,
70
+ entry
71
+ });
65
72
  let stat;
66
73
  try {
67
74
  stat = fs.statSync(abs);
68
75
  } catch {
69
- console.warn(`[ogygia] ${name}: ogygia.files entry '${entry}' does not exist — skipped.`);
76
+ warn("package_files_missing", {
77
+ package: name,
78
+ entry
79
+ });
70
80
  continue;
71
81
  }
72
82
  if (stat.isDirectory()) dirs.push(posix(fs.realpathSync(abs)));