@ogygia/core 0.11.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +1 -1
  2. package/dist/KeepHost.svelte +1 -1
  3. package/dist/LiveHost.svelte +1 -1
  4. package/dist/NestedProvider.svelte +1 -1
  5. package/dist/Region.svelte +28 -21
  6. package/dist/Region.svelte.d.ts +0 -2
  7. package/dist/SlotBoundary.svelte +1 -1
  8. package/dist/compiler/bom.js +1 -1
  9. package/dist/compiler/browser.d.ts +1 -1
  10. package/dist/compiler/browser.js +1 -1
  11. package/dist/compiler/ctx.d.ts +1 -1
  12. package/dist/compiler/ctx.js +3 -3
  13. package/dist/compiler/{macros/dedent.d.ts → dedent.d.ts} +12 -1
  14. package/dist/compiler/dedent.js +43 -0
  15. package/dist/compiler/driver.d.ts +9 -6
  16. package/dist/compiler/driver.js +69 -35
  17. package/dist/compiler/ids.d.ts +3 -0
  18. package/dist/compiler/ids.js +4 -1
  19. package/dist/compiler/index.d.ts +3 -3
  20. package/dist/compiler/index.js +3 -3
  21. package/dist/compiler/kit.d.ts +7 -0
  22. package/dist/compiler/kit.js +22 -10
  23. package/dist/compiler/link/caps.js +59 -9
  24. package/dist/compiler/link/island-deps.js +225 -22
  25. package/dist/compiler/link/location-map.d.ts +10 -0
  26. package/dist/compiler/link/location-map.js +53 -0
  27. package/dist/compiler/link/manifest.js +5 -1
  28. package/dist/compiler/link/runtime-entry.d.ts +19 -2
  29. package/dist/compiler/link/runtime-entry.js +34 -1
  30. package/dist/compiler/link/server-manifest.js +43 -7
  31. package/dist/compiler/link/transport.js +39 -7
  32. package/dist/compiler/macros/source.js +2 -2
  33. package/dist/compiler/program.d.ts +4 -0
  34. package/dist/compiler/program.js +5 -1
  35. package/dist/compiler/region/transform.d.ts +13 -4
  36. package/dist/compiler/region/transform.js +15 -6
  37. package/dist/context.d.ts +3 -12
  38. package/dist/context.js +1 -16
  39. package/dist/hooks.d.ts +16 -0
  40. package/dist/hooks.js +4 -2
  41. package/dist/insights/page-insights.js +2 -2
  42. package/dist/internal.d.ts +2 -1
  43. package/dist/internal.js +2 -1
  44. package/dist/nested.d.ts +11 -0
  45. package/dist/nested.js +19 -0
  46. package/dist/ref-sink.d.ts +15 -0
  47. package/dist/ref-sink.js +19 -0
  48. package/dist/ref.d.ts +2 -2
  49. package/dist/ref.js +1 -11
  50. package/dist/runtime/beacon.js +1 -1
  51. package/dist/runtime/boot-link.js +4 -1
  52. package/dist/runtime/core.js +10 -7
  53. package/dist/runtime/entry-locations.d.ts +10 -3
  54. package/dist/runtime/entry-locations.js +132 -21
  55. package/dist/runtime/full.js +5 -1
  56. package/dist/runtime/hydrate-core.d.ts +4 -4
  57. package/dist/runtime/hydrate-core.js +8 -5
  58. package/dist/runtime/morph.d.ts +7 -0
  59. package/dist/runtime/morph.js +21 -15
  60. package/dist/runtime/nav-handle.d.ts +3 -0
  61. package/dist/runtime/nav-handle.js +12 -5
  62. package/dist/runtime/ownership.d.ts +10 -1
  63. package/dist/runtime/ownership.js +19 -1
  64. package/dist/runtime/reconcile.d.ts +5 -2
  65. package/dist/runtime/reconcile.js +52 -16
  66. package/dist/runtime/region-endpoint-url.js +1 -0
  67. package/dist/runtime/region-ids.js +2 -2
  68. package/dist/runtime/remote-seeds.js +14 -3
  69. package/dist/runtime/router-nav.js +15 -0
  70. package/dist/runtime/router.js +3 -0
  71. package/dist/runtime/seeds.d.ts +10 -5
  72. package/dist/runtime/seeds.js +16 -12
  73. package/dist/runtime/slots.d.ts +24 -2
  74. package/dist/runtime/slots.js +2 -0
  75. package/dist/runtime/wire-classes.d.ts +2 -0
  76. package/dist/runtime/wire-classes.js +10 -1
  77. package/dist/runtime/wire-format.d.ts +4 -1
  78. package/dist/runtime/wire-format.js +6 -1
  79. package/dist/seed-ref-reviver.d.ts +24 -0
  80. package/dist/seed-ref-reviver.js +44 -0
  81. package/dist/seed-refs.d.ts +2 -16
  82. package/dist/seed-refs.js +1 -33
  83. package/dist/server/document-tail.d.ts +3 -2
  84. package/dist/server/document-tail.js +4 -3
  85. package/dist/server/entry-location.js +11 -8
  86. package/dist/server/props-wire.js +4 -3
  87. package/dist/server/signing-key.d.ts +10 -2
  88. package/dist/server/signing-key.js +49 -15
  89. package/dist/vite/index.js +5 -4
  90. package/dist/vite/kit-dirs.js +1 -1
  91. package/package.json +3 -3
  92. package/dist/compiler/link/entry-shim.d.ts +0 -4
  93. package/dist/compiler/link/entry-shim.js +0 -29
  94. package/dist/compiler/macros/dedent.js +0 -36
@@ -1,17 +1,19 @@
1
1
  import { runtime_bootstrap_tags } from "./document-tail.js";
2
2
  import runtime_url from "virtual:ogygia/runtime-url";
3
- import { entryLocation, islandDeps } from "virtual:ogygia/island-deps";
3
+ import { entryLocation, islandDeps, locationMap } from "virtual:ogygia/island-deps";
4
4
  // src/server/entry-location.ts
5
5
  /**
6
6
  * ENTRY IDENTITY → LOCATION, on the server. A hydrate island's entry and the runtime each have an
7
- * IDENTITY — the stable URL the compiler bakes (`/<appDir>/immutable/og-region.<iid>.js`, the
7
+ * IDENTITY — the stable URL the compiler bakes (`/<appDir>/ogygia/og-region.<iid>.js`, the
8
8
  * runtime's stable name) and every map keys by — and a LOCATION: the content-hashed file the build
9
- * emitted, the one a browser loads. A stable name served `immutable` would be kept for a year after
10
- * its content changed; a hashed name cannot go stale. The server bundle is built before the client
11
- * bundle, so the location is looked up at render time through the build handoff (`entryLocation`).
9
+ * emitted under `immutable/`, the one a browser loads. A hashed name cannot go stale; the stable name
10
+ * lives outside `immutable/` (compiler/region/transform.ts `STABLE_NAME_DIR`), so a host revalidates
11
+ * it. The server bundle is built before the client bundle, so the location is looked up at render
12
+ * time through the build handoff (`entryLocation`).
12
13
  *
13
- * No location (the dev server, a handoff from before hashing, an island from another app): the
14
- * identity itself is served — in a build, the stable-name shim that re-exports the current file.
14
+ * In dev the identity is the location (the dev server serves the module at it). No location in a
15
+ * build (a handoff from before hashing, an island from another app): a region writes no `src`, and the
16
+ * browser finds the current file through the build's location maps (compiler/link/location-map.ts).
15
17
  */
16
18
  /** The file an entry is loaded from: its content-hashed location, else the identity itself. */
17
19
  function entry_src(identity) {
@@ -24,6 +26,7 @@ function entry_src(identity) {
24
26
  */
25
27
  function runtime_bootstrap(resolve = (url) => url) {
26
28
  if (!runtime_url) return "";
27
- return runtime_bootstrap_tags(resolve(entry_src(runtime_url)), islandDeps(runtime_url).map(resolve));
29
+ const map = locationMap();
30
+ return runtime_bootstrap_tags(resolve(entry_src(runtime_url)), islandDeps(runtime_url).map(resolve), map ? resolve(map) : null);
28
31
  }
29
32
  export { entry_src, runtime_bootstrap };
@@ -1,10 +1,11 @@
1
1
  import { escape_script_text } from "../escape.js";
2
+ import { SEED_REF_KEY } from "../seed-ref-reviver.js";
2
3
  import { REF_WIRE_KEY, ref_reducer } from "../ref.js";
3
4
  import { register_wire_kind } from "../live-transport.js";
4
5
  import { register_derived_kind, register_store_kind } from "../store-transport.js";
5
6
  import { register_snippet_kind } from "../region-snippet.js";
6
7
  import { register_fn_kind } from "../fn-transport.js";
7
- import { SEED_REF_KEY, analyze, json_culprit, plan_seed_refs } from "../seed-refs.js";
8
+ import { analyze, json_culprit, plan_seed_refs } from "../seed-refs.js";
8
9
  import { stringify } from "devalue";
9
10
  // src/server/props-wire.ts
10
11
  /**
@@ -152,8 +153,8 @@ function plan_props_wire(value, entry) {
152
153
  culprit: () => culprit === void 0 ? culprit = json_culprit(value) : culprit
153
154
  };
154
155
  }
155
- const LIVE_ENTRY_MARK = "/immutable/og-region.";
156
- const LIVE_ENTRY_RE = /^\/[^"\s]+\/immutable\/og-region\.[0-9a-f]+\.js$/;
156
+ const LIVE_ENTRY_MARK = "/ogygia/og-region.";
157
+ const LIVE_ENTRY_RE = /^\/[^"\s]+\/ogygia\/og-region\.[0-9a-f]+\.js$/;
157
158
  function live_entries_in(text) {
158
159
  const out = [];
159
160
  let at = text.indexOf(LIVE_ENTRY_MARK);
@@ -1,11 +1,19 @@
1
1
  export interface SigningKey {
2
2
  secret: string;
3
- /** env-provided: survives redeploys and is shared by every deployment that sets the same value */
3
+ /** the app's or the env's: survives redeploys and is shared by every deployment that sets the same value */
4
4
  stable: boolean;
5
5
  /** 8-char key fingerprint (`&k=` on capabilities, `x-ogygia-key` on refusals) */
6
6
  fp: string;
7
+ /** where the secret came from: `handle({ secret })`, `OGYGIA_SECRET`, or this build's random key */
8
+ source: "option" | "env" | "build";
7
9
  }
8
10
  export declare function signing_key(): SigningKey;
11
+ /**
12
+ * Record the app's `handle({ secret })`. A blank one counts as none (an app can pass a value that may
13
+ * be unset). Once a key is decided it never changes in the process: capabilities minted under the
14
+ * first would stop verifying, so a different secret arriving later is an error (thrown in dev).
15
+ */
16
+ export declare function configure_signing_secret(secret: string | undefined): void;
9
17
  export declare function key_fingerprint(secret: string): string;
10
- /** Tests: forget the decided key. */
18
+ /** Tests: forget the decided key and the app's secret. */
11
19
  export declare function reset_signing_key(): void;
@@ -5,7 +5,12 @@ import { building } from "$app/env";
5
5
  /**
6
6
  * THE region signing key, decided once at first use (the first mint or verify), not at module eval:
7
7
  * a host that fills `process.env` after the server's modules load (an adapter loading a `.env` late)
8
- * still counts. The runtime's `OGYGIA_SECRET` wins; else this build's random key.
8
+ * still counts. The app's `handle({ secret })` wins, then the runtime's `OGYGIA_SECRET`, else this
9
+ * build's random key.
10
+ *
11
+ * Process-wide on `globalThis`, not module state (like the islands endpoint, server/endpoint.ts):
12
+ * one server can load this file as two copies (pnpm gives a package one folder per set of peer
13
+ * versions), and a copy that never saw the handle's secret would mint with the build's key.
9
14
  *
10
15
  * A capability carries its minter's key FINGERPRINT (`&k=`): an HMAC of a constant under the key —
11
16
  * the same on every instance sharing the key, and it reveals nothing of the key. A deployment that
@@ -13,18 +18,47 @@ import { building } from "$app/env";
13
18
  * instead of an opaque 403: the page and its islands endpoint came from different deployments (a CDN
14
19
  * routing them to two backends, HTML cached across a key change).
15
20
  */
16
- let key;
21
+ const SLOT = Symbol.for("ogygia.signing-key");
22
+ const SOURCE_NAME = {
23
+ option: "handle({ secret })",
24
+ env: "OGYGIA_SECRET",
25
+ build: "this build's random key"
26
+ };
27
+ function slot() {
28
+ const g = globalThis;
29
+ return g[SLOT] ??= {};
30
+ }
17
31
  function signing_key() {
18
- if (key) return key;
32
+ const s = slot();
33
+ if (s.key) return s.key;
19
34
  const from_env = typeof process !== "undefined" && process.env?.OGYGIA_SECRET || "";
20
- const secret = from_env || buildKey;
21
- key = {
35
+ const [secret, source] = s.option ? [s.option, "option"] : from_env ? [from_env, "env"] : [buildKey, "build"];
36
+ s.key = {
22
37
  secret,
23
- stable: from_env !== "",
24
- fp: key_fingerprint(secret)
38
+ stable: source !== "build",
39
+ fp: key_fingerprint(secret),
40
+ source
25
41
  };
26
- if (!key.stable && !import.meta.env.DEV) warn_build_key();
27
- return key;
42
+ if (!s.key.stable && !import.meta.env.DEV) warn_build_key();
43
+ return s.key;
44
+ }
45
+ /**
46
+ * Record the app's `handle({ secret })`. A blank one counts as none (an app can pass a value that may
47
+ * be unset). Once a key is decided it never changes in the process: capabilities minted under the
48
+ * first would stop verifying, so a different secret arriving later is an error (thrown in dev).
49
+ */
50
+ function configure_signing_secret(secret) {
51
+ if (!secret?.trim()) return;
52
+ const s = slot();
53
+ const decided = s.key?.secret ?? s.option;
54
+ if (decided === void 0 || decided === secret) {
55
+ s.option = secret;
56
+ return;
57
+ }
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);
28
62
  }
29
63
  function key_fingerprint(secret) {
30
64
  return sign(secret, "ogygia:key-fp").slice(0, 8);
@@ -36,12 +70,12 @@ function warn_build_key() {
36
70
  const K = Symbol.for("ogygia.build-key-warned");
37
71
  if (g[K]) return;
38
72
  g[K] = true;
39
- 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 so prerendered holes survive redeploys.");
40
- 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.");
41
- 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.");
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 }).");
42
76
  }
43
- /** Tests: forget the decided key. */
77
+ /** Tests: forget the decided key and the app's secret. */
44
78
  function reset_signing_key() {
45
- key = void 0;
79
+ delete globalThis[SLOT];
46
80
  }
47
- export { key_fingerprint, reset_signing_key, signing_key };
81
+ export { configure_signing_secret, key_fingerprint, reset_signing_key, signing_key };
@@ -701,10 +701,10 @@ function ogygia(options = {}) {
701
701
  },
702
702
  generateBundle() {
703
703
  if (!is_build || server_leg_of(this)) return;
704
- for (const shim of compiler.entry_shims((ref) => this.getFileName(ref))) this.emitFile({
704
+ for (const file of compiler.location_maps((ref) => this.getFileName(ref))) this.emitFile({
705
705
  type: "asset",
706
- fileName: shim.fileName,
707
- source: shim.source
706
+ fileName: file.fileName,
707
+ source: file.source
708
708
  });
709
709
  },
710
710
  closeBundle: {
@@ -805,7 +805,8 @@ function ogygia(options = {}) {
805
805
  content_css,
806
806
  css_inline,
807
807
  fn_manifest: Object.fromEntries(compiler.dollar_hoists),
808
- entries: entries.locations
808
+ entries: entries.locations,
809
+ locations: compiler.location_map_url
809
810
  });
810
811
  emit_island_deps_handoff(root, json, kit_dirs(root).out_dir);
811
812
  }
@@ -8,7 +8,7 @@ import { pathToFileURL } from "node:url";
8
8
  * `src/routes`) and `outDir` (default `.svelte-kit`). ogygia scans the routes tree (csr worlds, the
9
9
  * client-build keepalive, route options) and writes/reads its island-deps handoff under Kit's output
10
10
  * dir — every one of those must follow the app's configuration, not the defaults (an app building
11
- * two route trees from one source, `PES_MODE=v2 → src/routes-v2` + `.svelte-kit-v2`, had its
11
+ * two route trees from one source, `CATALOG_MODE=v2 → src/routes-v2` + `.svelte-kit-v2`, had its
12
12
  * all-csr=false tree read as "no routes": Kit skipped the client build, the runtime chunk was never
13
13
  * emitted, every island 404'd).
14
14
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygia/core",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
4
4
  "description": "The core of ogygia (SSR islands for SvelteKit): apps install `ogygia`, plugins build on this",
5
5
  "homepage": "https://ogygiajs.dev",
6
6
  "bugs": {
@@ -141,6 +141,7 @@
141
141
  "tinyglobby": "^0.2.17"
142
142
  },
143
143
  "devDependencies": {
144
+ "@ogygia/tsdown-plugin-svelte": "1.1.1",
144
145
  "@sveltejs/kit": "^2.70.2",
145
146
  "@sveltejs/vite-plugin-svelte": "^7.2.0",
146
147
  "@types/node": "^26.1.2",
@@ -156,8 +157,7 @@
156
157
  "typescript": "^6.0.3",
157
158
  "valibot": "^1.4.2",
158
159
  "vite": "^8.3.5",
159
- "vitest": "^4.1.12",
160
- "@ogygia/tsdown-plugin-svelte": "1.1.0"
160
+ "vitest": "^4.1.12"
161
161
  },
162
162
  "peerDependencies": {
163
163
  "@sveltejs/kit": ">=2.70.2 <4",
@@ -1,4 +0,0 @@
1
- /** An island entry's shim: every export of its hashed file (a component entry has a default). */
2
- export declare function island_shim_source(shim_file: string, entry_file: string): string;
3
- /** The runtime's shim: the hashed runtime, run (it has no exports; loading it boots). */
4
- export declare function runtime_shim_source(shim_file: string, runtime_file: string): string;
@@ -1,29 +0,0 @@
1
- import { path } from "../host.js";
2
- // src/compiler/link/entry-shim.ts
3
- /**
4
- * THE STABLE-NAME SHIMS. An island entry and the runtime are content-hashed files; each one's old
5
- * stable name is ALSO written, as a shim of the hashed file, so that a page cached before content
6
- * hashing (whose islands import their stable names), and a load whose location failed (the runtime
7
- * re-fetches the stable name fresh), still reach the CURRENT build.
8
- *
9
- * A shim is written as a plain ASSET at the bundle's end, once every hashed name is final — never as
10
- * a bundled chunk. Its name never changes, so the bundler has nothing to hash, and a chunk would make
11
- * the island's module the target of two entries: the bundler would split its code behind two facades
12
- * and every island load would cost two requests. As an asset, the island has one entry, its location
13
- * IS its code, and the shim is a static re-export by relative path.
14
- */
15
- /** The specifier from the shim's file to the hashed file (both output-relative paths). */
16
- function relative_specifier(from_file, to_file) {
17
- const rel = path.posix.relative(path.posix.dirname(from_file), to_file);
18
- return rel.startsWith(".") ? rel : "./" + rel;
19
- }
20
- /** An island entry's shim: every export of its hashed file (a component entry has a default). */
21
- function island_shim_source(shim_file, entry_file) {
22
- const spec = JSON.stringify(relative_specifier(shim_file, entry_file));
23
- return `export * from ${spec};\nexport { default } from ${spec};\n`;
24
- }
25
- /** The runtime's shim: the hashed runtime, run (it has no exports; loading it boots). */
26
- function runtime_shim_source(shim_file, runtime_file) {
27
- return `import ${JSON.stringify(relative_specifier(shim_file, runtime_file))};\n`;
28
- }
29
- export { island_shim_source, runtime_shim_source };
@@ -1,36 +0,0 @@
1
- // src/compiler/macros/dedent.ts
2
- const CRLF_G = /\r\n?/g;
3
- const LEADING_INDENT_RE = /^[\t ]*/;
4
- /**
5
- * Strip the common leading indentation from a block of source, and trim leading/trailing blank
6
- * lines. Source embedded in a call (`import.meta.og.code(\`…\`, …)`) is indented to match where the
7
- * call sits; that indentation is an artifact of the surrounding code, not part of the snippet. This
8
- * normalizes it so the rendered `<pre>` shows the snippet's OWN logical indentation, flush-left.
9
- *
10
- * Blank lines (whitespace-only) don't count toward the common indent; tabs and spaces are treated by
11
- * their literal column contribution to the prefix (the common prefix is computed on raw characters,
12
- * so mixed tabs/spaces still de-indent correctly as long as the leading run is consistent — which it
13
- * is for machine-indented source).
14
- */
15
- function dedent(text) {
16
- const lines = text.replace(CRLF_G, "\n").split("\n");
17
- while (lines.length && lines[0].trim() === "") lines.shift();
18
- while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
19
- if (!lines.length) return "";
20
- let common = null;
21
- for (const line of lines) {
22
- if (line.trim() === "") continue;
23
- const lead = LEADING_INDENT_RE.exec(line)[0];
24
- if (common === null) common = lead;
25
- else {
26
- let i = 0;
27
- const max = Math.min(common.length, lead.length);
28
- while (i < max && common[i] === lead[i]) i++;
29
- common = common.slice(0, i);
30
- }
31
- if (common === "") break;
32
- }
33
- const n = common?.length ?? 0;
34
- return lines.map((line) => line.trim() === "" ? "" : line.slice(n)).join("\n");
35
- }
36
- export { dedent };