@ogygia/core 0.11.5 → 0.12.1
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.
- package/dist/Region.svelte +28 -21
- package/dist/Region.svelte.d.ts +0 -2
- package/dist/compiler/bom.js +1 -1
- package/dist/compiler/browser.d.ts +1 -1
- package/dist/compiler/browser.js +1 -1
- package/dist/compiler/ctx.d.ts +1 -1
- package/dist/compiler/ctx.js +3 -3
- package/dist/compiler/{macros/dedent.d.ts → dedent.d.ts} +12 -1
- package/dist/compiler/dedent.js +43 -0
- package/dist/compiler/driver.d.ts +9 -6
- package/dist/compiler/driver.js +16 -24
- package/dist/compiler/index.d.ts +2 -2
- package/dist/compiler/index.js +2 -2
- package/dist/compiler/link/caps.js +59 -9
- package/dist/compiler/link/island-deps.js +225 -22
- package/dist/compiler/link/location-map.d.ts +10 -0
- package/dist/compiler/link/location-map.js +53 -0
- package/dist/compiler/link/manifest.js +5 -1
- package/dist/compiler/link/server-manifest.js +43 -7
- package/dist/compiler/link/transport.js +39 -7
- package/dist/compiler/macros/source.js +2 -2
- package/dist/compiler/region/transform.d.ts +13 -4
- package/dist/compiler/region/transform.js +25 -9
- package/dist/hooks.d.ts +16 -0
- package/dist/hooks.js +4 -2
- package/dist/insights/page-insights.js +2 -2
- package/dist/runtime/beacon.js +1 -1
- package/dist/runtime/boot-link.js +4 -1
- package/dist/runtime/core.js +3 -2
- package/dist/runtime/entry-locations.d.ts +10 -3
- package/dist/runtime/entry-locations.js +132 -21
- package/dist/runtime/morph.d.ts +7 -0
- package/dist/runtime/morph.js +21 -15
- package/dist/runtime/ownership.d.ts +10 -1
- package/dist/runtime/ownership.js +19 -1
- package/dist/runtime/reconcile.d.ts +5 -2
- package/dist/runtime/reconcile.js +52 -16
- package/dist/runtime/region-endpoint-url.js +1 -0
- package/dist/runtime/router-nav.js +15 -0
- package/dist/runtime/router.js +3 -0
- package/dist/runtime/slots.d.ts +8 -2
- package/dist/server/document-tail.d.ts +3 -2
- package/dist/server/document-tail.js +4 -3
- package/dist/server/entry-location.js +11 -8
- package/dist/server/props-wire.js +2 -2
- package/dist/server/signing-key.d.ts +10 -2
- package/dist/server/signing-key.js +49 -15
- package/dist/vite/index.js +5 -4
- package/dist/vite/kit-dirs.js +1 -1
- package/package.json +1 -1
- package/dist/compiler/link/entry-shim.d.ts +0 -4
- package/dist/compiler/link/entry-shim.js +0 -29
- package/dist/compiler/macros/dedent.js +0 -36
package/dist/hooks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REGION_BRAND } from "./region-brand.js";
|
|
2
|
-
import { signing_key } from "./server/signing-key.js";
|
|
2
|
+
import { configure_signing_secret, signing_key } from "./server/signing-key.js";
|
|
3
3
|
import { mpaSpeculationRules } from "./compiler/link/speculation.js";
|
|
4
4
|
import { set_page_route_config } from "./plugin/routes.js";
|
|
5
5
|
import { kit_page_facts_tail, kit_page_ready, kit_render_context, set_kit_event_reader, set_kit_page_reader, set_render_page_port } from "./server/kit-context.js";
|
|
@@ -9,7 +9,7 @@ import { absolutize_hole_html } from "./server/hole-urls.js";
|
|
|
9
9
|
import { is_keep_fallback } from "./keep-fallback.js";
|
|
10
10
|
import { set_source_recorder } from "./source.js";
|
|
11
11
|
import { kit_asset } from "./kit-paths.js";
|
|
12
|
-
import { region_mac_message, sign, verify } from "./server/hmac.js";
|
|
12
|
+
import { region_mac_message, secret_has_min_entropy, sign, verify } from "./server/hmac.js";
|
|
13
13
|
import { expiry_refusal, note_cache_control } from "./server/shared-cache.js";
|
|
14
14
|
import { app_lifetime } from "./server/region-endpoint.js";
|
|
15
15
|
import { cached_render, render_cache_key } from "./server/render-cache.js";
|
|
@@ -435,6 +435,8 @@ var OgygiaHandle = class {
|
|
|
435
435
|
this.#transform = options.transform;
|
|
436
436
|
configure_islands_endpoint(options.endpoint);
|
|
437
437
|
this.#endpoint = islands_endpoint();
|
|
438
|
+
if (options.secret?.trim() && !import.meta.env.DEV && !secret_has_min_entropy(options.secret)) throw new Error(`[ogygia] handle({ secret }) is too short for production (need ≥16 UTF-8 bytes).`);
|
|
439
|
+
configure_signing_secret(options.secret);
|
|
438
440
|
this.#speculation_rules = enabled ? "" : mpaSpeculationRules(this.#endpoint);
|
|
439
441
|
this.render_rate = new RateLimiter({
|
|
440
442
|
max: rateLimit.max,
|
|
@@ -1267,8 +1267,8 @@ function analyze_page(page, regions, failures = [], now = Infinity, cpu = null)
|
|
|
1267
1267
|
const woke = gone.filter((f) => f.recovered);
|
|
1268
1268
|
const dead = gone.filter((f) => !f.recovered);
|
|
1269
1269
|
const parts = [];
|
|
1270
|
-
if (woke.length) parts.push(`${list(woke.map(name))} woke on the current build's code
|
|
1271
|
-
if (dead.length) parts.push(`${list(dead.map(name))} stayed asleep: ${dead.length === 1 ? "
|
|
1270
|
+
if (woke.length) parts.push(`${list(woke.map(name))} woke on the current build's code, found in its \`_app/ogygia.json\`: the page's HTML and ${woke.length === 1 ? "that island" : "those islands"} now come from different builds`);
|
|
1271
|
+
if (dead.length) parts.push(`${list(dead.map(name))} stayed asleep: the current build's file failed too, or \`_app/ogygia.json\` did not name ${dead.length === 1 ? "it" : "them"}`);
|
|
1272
1272
|
findings.push({
|
|
1273
1273
|
code: "island-file-gone",
|
|
1274
1274
|
severity: dead.length ? "error" : "warn",
|
package/dist/runtime/beacon.js
CHANGED
|
@@ -108,7 +108,7 @@ function tag_attrs(tag) {
|
|
|
108
108
|
}
|
|
109
109
|
return out;
|
|
110
110
|
}
|
|
111
|
-
/** The tag's name, lowercase (`<
|
|
111
|
+
/** The tag's name, lowercase (`<ds-button …>` → `ds-button`). */
|
|
112
112
|
function tag_name(tag) {
|
|
113
113
|
let i = 1;
|
|
114
114
|
while (i < tag.length && tag[i] !== " " && tag[i] !== ">" && tag[i] !== "/" && tag[i] !== "\n") i++;
|
|
@@ -9,7 +9,7 @@ import { runtime_session } from "./session.js";
|
|
|
9
9
|
import { regions_in_shadow } from "./connected.js";
|
|
10
10
|
import { yield_task } from "./schedule.js";
|
|
11
11
|
import { beacon_nav } from "./beacon.js";
|
|
12
|
-
import { is_self_owned, walk_enters } from "./ownership.js";
|
|
12
|
+
import { is_self_owned, record_server_body, server_body_attributes, server_body_child, walk_enters } from "./ownership.js";
|
|
13
13
|
import { load_slot } from "./load-scheduler.js";
|
|
14
14
|
// src/runtime/boot-link.ts
|
|
15
15
|
/**
|
|
@@ -38,6 +38,9 @@ function link_boot() {
|
|
|
38
38
|
beacon_nav: BEACON ? beacon_nav : no_beacon_nav,
|
|
39
39
|
walk_enters,
|
|
40
40
|
is_self_owned,
|
|
41
|
+
record_server_body,
|
|
42
|
+
server_body_attributes,
|
|
43
|
+
server_body_child,
|
|
41
44
|
load_slot
|
|
42
45
|
};
|
|
43
46
|
}
|
package/dist/runtime/core.js
CHANGED
|
@@ -4,7 +4,7 @@ import { init_shadow_registries, parse_region_html, restore_adopt_sheets, restor
|
|
|
4
4
|
import { mark_region_changed, region_changed, unwatch_region, watch_region } from "./drift-watch.js";
|
|
5
5
|
import { runtime_session } from "./session.js";
|
|
6
6
|
import { RegionAnswerRefused, capability_expired, deny_of, is_allowed_region_endpoint, is_document_answer, is_redirected_answer, is_same_origin_response, island_module_url, renewal_url } from "./region-endpoint-url.js";
|
|
7
|
-
import { import_entry, island_entry_of, on_entry_fallback, pin_region_urls } from "./entry-locations.js";
|
|
7
|
+
import { import_entry, island_entry_of, on_entry_fallback, pin_build_map, pin_region_urls } from "./entry-locations.js";
|
|
8
8
|
import { is_awake, is_deferred, is_frozen, ours_on_kit_document, phase2_hydrate_schedule, region_hydrate_schedule, region_schedule } from "./region-attrs.js";
|
|
9
9
|
import { slots } from "./slots.js";
|
|
10
10
|
import { KEEP_FALLBACK_HTML } from "../keep-fallback-marker.js";
|
|
@@ -94,7 +94,7 @@ const ON_DEMAND_EVENTS = [
|
|
|
94
94
|
const ON_DEMAND_SELECTOR = "ogygia-region[render=\"defer\"][when=\"interaction\"]";
|
|
95
95
|
/**
|
|
96
96
|
* ON-DEMAND holes, delegated: ONE set of capture listeners on the document for every armed hole
|
|
97
|
-
* (five listeners per hole used to be five hundred on a page of
|
|
97
|
+
* (five listeners per hole used to be five hundred on a page of nav-menus). Intent lands anywhere
|
|
98
98
|
* inside a hole; the nearest ARMED hole above the target fires once and is forgotten.
|
|
99
99
|
*/
|
|
100
100
|
const on_demand_armed = /* @__PURE__ */ new Map();
|
|
@@ -1128,6 +1128,7 @@ function boot(installers = [], client_plugins = []) {
|
|
|
1128
1128
|
runtime_session.adopt_document_head();
|
|
1129
1129
|
link_boot();
|
|
1130
1130
|
for (const install of installers) install();
|
|
1131
|
+
pin_build_map();
|
|
1131
1132
|
on_entry_fallback((report) => {
|
|
1132
1133
|
if (DEVTOOLS) emit({
|
|
1133
1134
|
domain: "runtime",
|
|
@@ -11,6 +11,13 @@ export declare function on_entry_fallback(reporter: (report: EntryFallbackReport
|
|
|
11
11
|
export declare function note_entry_location(entry: string | null | undefined, src: string | null | undefined, base?: string): void;
|
|
12
12
|
/** The known location of an entry, or undefined. */
|
|
13
13
|
export declare function entry_location(entry: string, base?: string): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Pin the build map's address to the page that wrote it, as `pin_region_urls` pins a region's: with
|
|
16
|
+
* Kit's default `paths.relative` it is written relative to that page, and it is read lazily — after a
|
|
17
|
+
* client navigation to another depth, resolved against the new address, it named a file that is not
|
|
18
|
+
* there. The runtime calls this at boot, before any navigation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function pin_build_map(): void;
|
|
14
21
|
/**
|
|
15
22
|
* PIN A REGION'S URLS TO THE PAGE THAT WROTE THEM. With Kit's default `paths.relative`, the server
|
|
16
23
|
* writes `entry` / `src` / `endpoint` relative to the page it rendered (`../../_app/immutable/…`,
|
|
@@ -26,8 +33,8 @@ export declare function pin_region_urls(el: Element, base?: string): void;
|
|
|
26
33
|
/** An island element's identity, its location noted on the way (the one read every loader uses). */
|
|
27
34
|
export declare function island_entry_of(el: Element): string | null;
|
|
28
35
|
/**
|
|
29
|
-
* Import an entry: its location when known
|
|
30
|
-
*
|
|
31
|
-
*
|
|
36
|
+
* Import an entry: its location when known (else this build's map names it, once read); failing that,
|
|
37
|
+
* or with none anywhere, the CURRENT build's location — so an island on a page that outlived its build
|
|
38
|
+
* still wakes, on the current code. `url`: what the caller resolved for the identity.
|
|
32
39
|
*/
|
|
33
40
|
export declare function import_entry<T>(entry: string, url?: string): Promise<T>;
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* fingerprint, the devtools' names). The file to import is its LOCATION — content-hashed, so an
|
|
7
7
|
* `immutable` cache never serves a stale one. The server writes it beside the identity: the region
|
|
8
8
|
* element's `src`, and the island graph's `s` map for an entry no element carries (a portable
|
|
9
|
-
* snippet's)
|
|
10
|
-
* here first: the location once one
|
|
11
|
-
*
|
|
9
|
+
* snippet's), and the build's location maps for the rest (`import_entry`). Every loader resolves
|
|
10
|
+
* through `island_module_url` (region-endpoint-url.ts), which asks here first: the location once one
|
|
11
|
+
* is known, else the identity (on the dev server, the module itself; in a build, a name, no file).
|
|
12
12
|
*/
|
|
13
13
|
/** absolute identity → absolute location */
|
|
14
14
|
const locations = /* @__PURE__ */ new Map();
|
|
@@ -43,6 +43,48 @@ function note_entry_location(entry, src, base) {
|
|
|
43
43
|
function entry_location(entry, base) {
|
|
44
44
|
return locations.size ? locations.get(absolute(entry, base)) : void 0;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* THIS BUILD'S LOCATION MAP (compiler/link/location-map.ts, the content-hashed one): every island entry's
|
|
48
|
+
* location by identity, for a load that arrives with an identity alone — a region the browser
|
|
49
|
+
* rendered, a held region from the wire, a snippet whose host carried no graph. The runtime's script
|
|
50
|
+
* names it (`data-og-locations`); it is read once, on the first such load. Content-hashed, so no
|
|
51
|
+
* cache serves it stale. It only fills gaps: a location a page wrote (a later build's, after a
|
|
52
|
+
* navigation) always wins over the map of the build the runtime came from. No map, or it failed
|
|
53
|
+
* (the dev server, a page that outlived its build): the identity is loaded, as before. A failed read
|
|
54
|
+
* is not kept: the next identity-only load asks again (a blip must not cost the page every later one).
|
|
55
|
+
*/
|
|
56
|
+
const BUILD_MAP_ATTR = "data-og-locations";
|
|
57
|
+
let build_map = null;
|
|
58
|
+
/**
|
|
59
|
+
* Pin the build map's address to the page that wrote it, as `pin_region_urls` pins a region's: with
|
|
60
|
+
* Kit's default `paths.relative` it is written relative to that page, and it is read lazily — after a
|
|
61
|
+
* client navigation to another depth, resolved against the new address, it named a file that is not
|
|
62
|
+
* there. The runtime calls this at boot, before any navigation.
|
|
63
|
+
*/
|
|
64
|
+
function pin_build_map() {
|
|
65
|
+
if (typeof document === "undefined") return;
|
|
66
|
+
const script = document.querySelector(`script[${BUILD_MAP_ATTR}]`);
|
|
67
|
+
const href = script?.getAttribute(BUILD_MAP_ATTR);
|
|
68
|
+
if (script && href) script.setAttribute(BUILD_MAP_ATTR, absolute(href));
|
|
69
|
+
}
|
|
70
|
+
function read_build_map() {
|
|
71
|
+
if (build_map) return build_map;
|
|
72
|
+
if (typeof document === "undefined") return null;
|
|
73
|
+
const href = document.querySelector(`script[${BUILD_MAP_ATTR}]`)?.getAttribute(BUILD_MAP_ATTR);
|
|
74
|
+
if (!href) return null;
|
|
75
|
+
const url = absolute(href);
|
|
76
|
+
const read = fetch(url).then((res) => res.ok ? res.json() : null).catch(() => null).then((map) => {
|
|
77
|
+
if (!map) {
|
|
78
|
+
if (build_map === read) build_map = null;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const stable in map) {
|
|
82
|
+
const identity = absolute(stable, url);
|
|
83
|
+
if (!locations.has(identity)) locations.set(identity, absolute(map[stable], url));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return build_map = read;
|
|
87
|
+
}
|
|
46
88
|
/** The region attributes that hold a URL: its identity, its location, its hole's endpoint. */
|
|
47
89
|
const REGION_URL_ATTRS = [
|
|
48
90
|
"entry",
|
|
@@ -80,32 +122,101 @@ function island_entry_of(el) {
|
|
|
80
122
|
return entry;
|
|
81
123
|
}
|
|
82
124
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
125
|
+
* THE CURRENT BUILD'S MAP (compiler/link/location-map.ts): the one file at a fixed name, in the app
|
|
126
|
+
* dir of the identities it maps (`<appDir>/ogygia.json`), so any app's is found from an identity alone.
|
|
127
|
+
* A page that outlived its build reads it when its own files are gone (or it never named a map) and
|
|
128
|
+
* wakes on the current code. Revalidated (a host serves it as any changing file; `no-cache` past one
|
|
129
|
+
* that kept it anyway), read once per document per app. Only an ogygia identity has one: the dev
|
|
130
|
+
* server's module URLs never ask.
|
|
87
131
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
132
|
+
const current_maps = /* @__PURE__ */ new Map();
|
|
133
|
+
/** A build's island identity (`…/<appDir>/ogygia/og-region.<id>.js`): a name, no file. */
|
|
134
|
+
function is_build_identity(entry) {
|
|
135
|
+
return absolute(entry).includes("/ogygia/og-region.");
|
|
92
136
|
}
|
|
137
|
+
/** The current map's file for an identity (`<appDir>/ogygia.json`), or null: not a build's identity. */
|
|
138
|
+
function current_map_file(identity) {
|
|
139
|
+
const dir = identity.lastIndexOf("/ogygia/og-region.");
|
|
140
|
+
return dir === -1 ? null : identity.slice(0, dir) + "/ogygia.json";
|
|
141
|
+
}
|
|
142
|
+
/** The current build's location for `entry`: null when its map has none, undefined when the map
|
|
143
|
+
* could not be read (that read is not kept: the next ask reads again). */
|
|
144
|
+
async function current_location(entry) {
|
|
145
|
+
const identity = absolute(entry);
|
|
146
|
+
const file = current_map_file(identity);
|
|
147
|
+
if (!file) return null;
|
|
148
|
+
let map = current_maps.get(file);
|
|
149
|
+
if (!map) {
|
|
150
|
+
map = fetch(file, { cache: "no-cache" }).then((res) => res.ok ? res.json() : null).catch(() => null);
|
|
151
|
+
current_maps.set(file, map);
|
|
152
|
+
}
|
|
153
|
+
const pairs = await map;
|
|
154
|
+
if (!pairs) {
|
|
155
|
+
forget_current_map(file, map);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const name in pairs) if (absolute(name, file) === identity) return absolute(pairs[name], file);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function forget_current_map(file, map = current_maps.get(file)) {
|
|
162
|
+
if (current_maps.get(file) === map) current_maps.delete(file);
|
|
163
|
+
}
|
|
164
|
+
/** The host says this file is not there (it does not say so of a file that failed another way). */
|
|
165
|
+
function is_gone(url) {
|
|
166
|
+
return fetch(url, {
|
|
167
|
+
method: "HEAD",
|
|
168
|
+
cache: "no-store"
|
|
169
|
+
}).then((res) => res.status === 404 || res.status === 410, () => false);
|
|
170
|
+
}
|
|
171
|
+
/** How long a recovery waits before each ask of the current map: a rollover settles in seconds. */
|
|
172
|
+
const RECOVERY_WAITS = [
|
|
173
|
+
0,
|
|
174
|
+
1e3,
|
|
175
|
+
3e3,
|
|
176
|
+
9e3
|
|
177
|
+
];
|
|
93
178
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
179
|
+
* The current build's file for a build's identity whose file just failed to load. A deploy's rollover
|
|
180
|
+
* is exactly when this is asked, so an answer that cannot be the current one is asked again while it
|
|
181
|
+
* settles: a map that could not be read (a 404, a 5xx), and a map that names the very file that just
|
|
182
|
+
* failed when the host says that file is gone — an edge still answering with the old build's map. A
|
|
183
|
+
* file that failed some other way (the network, a broken module) is not asked about again.
|
|
184
|
+
*/
|
|
185
|
+
async function recover_location(entry, failed) {
|
|
186
|
+
let current;
|
|
187
|
+
for (const wait of RECOVERY_WAITS) {
|
|
188
|
+
if (wait) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
189
|
+
current = await current_location(entry);
|
|
190
|
+
if (current === void 0) continue;
|
|
191
|
+
if (current !== failed || !await is_gone(failed)) break;
|
|
192
|
+
forget_current_map(current_map_file(absolute(entry)));
|
|
193
|
+
}
|
|
194
|
+
return current ?? null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Import an entry: its location when known (else this build's map names it, once read); failing that,
|
|
198
|
+
* or with none anywhere, the CURRENT build's location — so an island on a page that outlived its build
|
|
199
|
+
* still wakes, on the current code. `url`: what the caller resolved for the identity.
|
|
97
200
|
*/
|
|
98
201
|
function import_entry(entry, url = entry_location(entry) ?? entry) {
|
|
99
|
-
|
|
202
|
+
if (entry_location(entry)) return load_entry(entry, url);
|
|
203
|
+
return (read_build_map() ?? Promise.resolve()).then(async () => {
|
|
204
|
+
const located = entry_location(entry);
|
|
205
|
+
if (located) return load_entry(entry, located);
|
|
206
|
+
return load_entry(entry, await current_location(entry) || url);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function load_entry(entry, url) {
|
|
210
|
+
return import(
|
|
100
211
|
/* @vite-ignore */
|
|
101
212
|
url
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
213
|
+
).catch(async (error) => {
|
|
214
|
+
const failed = absolute(url);
|
|
215
|
+
const current = is_build_identity(entry) ? await recover_location(entry, failed) : absolute(entry);
|
|
216
|
+
if (!current || current === failed) throw error;
|
|
106
217
|
const retry = import(
|
|
107
218
|
/* @vite-ignore */
|
|
108
|
-
|
|
219
|
+
current
|
|
109
220
|
);
|
|
110
221
|
const report = (recovered) => fallback_reporter?.({
|
|
111
222
|
entry,
|
|
@@ -116,4 +227,4 @@ function import_entry(entry, url = entry_location(entry) ?? entry) {
|
|
|
116
227
|
return retry;
|
|
117
228
|
});
|
|
118
229
|
}
|
|
119
|
-
export { entry_location, import_entry, island_entry_of, note_entry_location, on_entry_fallback, pin_region_urls };
|
|
230
|
+
export { entry_location, import_entry, island_entry_of, note_entry_location, on_entry_fallback, pin_build_map, pin_region_urls };
|
package/dist/runtime/morph.d.ts
CHANGED
|
@@ -35,6 +35,13 @@ export declare function morph_children(parent: Element, new_nodes: ArrayLike<Nod
|
|
|
35
35
|
*/
|
|
36
36
|
export interface MorphOptions {
|
|
37
37
|
target?: "live" | "walk";
|
|
38
|
+
/**
|
|
39
|
+
* A child of the ROOT that is not the render's (the router's body reconcile: a node a script put
|
|
40
|
+
* in `<body>`, such as a portal's mount anchor; ownership.ts `server_body_child`). The morph steps
|
|
41
|
+
* over it: never matched, never moved, never removed. The render's children around it reconcile
|
|
42
|
+
* as usual. Root level only; below the root the ownership rules decide.
|
|
43
|
+
*/
|
|
44
|
+
stays?: (node: Node) => boolean;
|
|
38
45
|
}
|
|
39
46
|
/** Add + update + remove attributes so `from` matches `to` exactly. Boolean attrs are attr presence.
|
|
40
47
|
* `keep_extra` (a self-owned element, {@link is_self_owned}): add + update only — an attribute the
|
package/dist/runtime/morph.js
CHANGED
|
@@ -95,7 +95,7 @@ function morph_children(parent, new_nodes, options) {
|
|
|
95
95
|
const owner = owner_of(parent);
|
|
96
96
|
if (owner !== "walk" && owner !== "region") return;
|
|
97
97
|
}
|
|
98
|
-
reconcile(parent, new_nodes, build_id_sets(parent, new_nodes), walk);
|
|
98
|
+
reconcile(parent, new_nodes, build_id_sets(parent, new_nodes), walk, options?.stays);
|
|
99
99
|
clear_aria_hidden_over_focus(parent);
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
@@ -191,18 +191,24 @@ function reserved_for_later(sets, new_inner, cursor, idx) {
|
|
|
191
191
|
* ({@link reconcile_by_identity}). Toward Svelte's walk, positions are always the render's (the walk
|
|
192
192
|
* binds by position).
|
|
193
193
|
*/
|
|
194
|
-
function reconcile(parent, new_nodes, sets, walk) {
|
|
194
|
+
function reconcile(parent, new_nodes, sets, walk, stays) {
|
|
195
195
|
if (!walk && order_owner(parent) === "element") reconcile_by_identity(parent, new_nodes, sets);
|
|
196
|
-
else reconcile_children(parent, new_nodes, sets, walk);
|
|
196
|
+
else reconcile_children(parent, new_nodes, sets, walk, stays);
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
199
|
* The render's sequence, by position.
|
|
200
200
|
* @param walk the {@link MorphOptions.target} is Svelte's walk (the repair), threaded unchanged down
|
|
201
201
|
* the recursion — a subtree the walk never enters is skipped.
|
|
202
|
+
* @param stays the root's children that are not the render's ({@link MorphOptions.stays}): the
|
|
203
|
+
* cursor steps over them, so they are never matched, moved or removed. Not threaded down.
|
|
202
204
|
*/
|
|
203
|
-
function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
205
|
+
function reconcile_children(parent, new_nodes, sets, walk = false, stays) {
|
|
204
206
|
const count = new_nodes.length;
|
|
205
|
-
|
|
207
|
+
const step = (n) => {
|
|
208
|
+
if (stays) while (n && stays(n)) n = n.nextSibling;
|
|
209
|
+
return n;
|
|
210
|
+
};
|
|
211
|
+
let cursor = step(parent.firstChild);
|
|
206
212
|
let idx = 0;
|
|
207
213
|
while (idx < count && cursor) {
|
|
208
214
|
const next = new_nodes[idx];
|
|
@@ -213,11 +219,11 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
213
219
|
if (!same_node(cursor, next)) break;
|
|
214
220
|
if (sets !== null && !id_sets_agree(sets, cursor, next)) break;
|
|
215
221
|
const here = cursor;
|
|
216
|
-
cursor = cursor.nextSibling;
|
|
222
|
+
cursor = step(cursor.nextSibling);
|
|
217
223
|
morph_same(here, next, sets, walk);
|
|
218
224
|
} else {
|
|
219
225
|
const here = cursor;
|
|
220
|
-
cursor = cursor.nextSibling;
|
|
226
|
+
cursor = step(cursor.nextSibling);
|
|
221
227
|
morph_node(here, next, sets, walk);
|
|
222
228
|
}
|
|
223
229
|
idx++;
|
|
@@ -225,7 +231,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
225
231
|
if (idx >= count) {
|
|
226
232
|
while (cursor) {
|
|
227
233
|
const gone = cursor;
|
|
228
|
-
cursor = cursor.nextSibling;
|
|
234
|
+
cursor = step(cursor.nextSibling);
|
|
229
235
|
parent.removeChild(gone);
|
|
230
236
|
}
|
|
231
237
|
return;
|
|
@@ -236,7 +242,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
236
242
|
}
|
|
237
243
|
let old_keys = null;
|
|
238
244
|
let old_inner = null;
|
|
239
|
-
for (let n = cursor; n; n = n.nextSibling) {
|
|
245
|
+
for (let n = cursor; n; n = step(n.nextSibling)) {
|
|
240
246
|
const k = key_of(n);
|
|
241
247
|
if (k !== null) (old_keys ??= /* @__PURE__ */ new Map()).set(k, n);
|
|
242
248
|
else if (sets !== null && n.nodeType === ELEMENT) {
|
|
@@ -260,7 +266,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
260
266
|
const next = new_nodes[idx];
|
|
261
267
|
if (cursor && key_of(next) === null && same_node(cursor, next)) {
|
|
262
268
|
const here = cursor;
|
|
263
|
-
cursor = cursor.nextSibling;
|
|
269
|
+
cursor = step(cursor.nextSibling);
|
|
264
270
|
morph_same(here, next, sets, walk);
|
|
265
271
|
} else parent.insertBefore(clone(next), cursor);
|
|
266
272
|
}
|
|
@@ -271,7 +277,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
271
277
|
const matched = old_keys?.get(next_key);
|
|
272
278
|
if (matched !== void 0) {
|
|
273
279
|
old_keys.delete(next_key);
|
|
274
|
-
if (matched === cursor) cursor = cursor.nextSibling;
|
|
280
|
+
if (matched === cursor) cursor = step(cursor.nextSibling);
|
|
275
281
|
else parent.insertBefore(matched, cursor);
|
|
276
282
|
morph_node(matched, next, sets, walk);
|
|
277
283
|
continue;
|
|
@@ -281,7 +287,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
281
287
|
const holder = wanted === void 0 ? null : find_holder(old_inner, wanted, next);
|
|
282
288
|
if (holder !== null) {
|
|
283
289
|
release_holder(old_inner, sets, holder);
|
|
284
|
-
if (holder === cursor) cursor = cursor.nextSibling;
|
|
290
|
+
if (holder === cursor) cursor = step(cursor.nextSibling);
|
|
285
291
|
else parent.insertBefore(holder, cursor);
|
|
286
292
|
morph_same(holder, next, sets, walk);
|
|
287
293
|
continue;
|
|
@@ -291,11 +297,11 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
291
297
|
while (cursor) {
|
|
292
298
|
cur_key = key_of(cursor);
|
|
293
299
|
if (cur_key === null || old_keys === null || !old_keys.has(cur_key)) break;
|
|
294
|
-
cursor = cursor.nextSibling;
|
|
300
|
+
cursor = step(cursor.nextSibling);
|
|
295
301
|
}
|
|
296
302
|
if (cursor && next_key === null && cur_key === null && same_node(cursor, next) && (old_inner === null || !reserved_for_later(sets, new_inner, cursor, idx))) {
|
|
297
303
|
const here = cursor;
|
|
298
|
-
cursor = cursor.nextSibling;
|
|
304
|
+
cursor = step(cursor.nextSibling);
|
|
299
305
|
if (old_inner !== null) release_holder(old_inner, sets, here);
|
|
300
306
|
morph_same(here, next, sets, walk);
|
|
301
307
|
continue;
|
|
@@ -304,7 +310,7 @@ function reconcile_children(parent, new_nodes, sets, walk = false) {
|
|
|
304
310
|
}
|
|
305
311
|
while (cursor) {
|
|
306
312
|
const gone = cursor;
|
|
307
|
-
cursor = cursor.nextSibling;
|
|
313
|
+
cursor = step(cursor.nextSibling);
|
|
308
314
|
parent.removeChild(gone);
|
|
309
315
|
}
|
|
310
316
|
if (old_keys) {
|
|
@@ -98,4 +98,13 @@ export declare function mark_render_made(node: Node): void;
|
|
|
98
98
|
* produces at this level (`render_tags`) is render output — a runtime does not make the render's
|
|
99
99
|
* elements. Anything else is the element's own: never matched, never removed.
|
|
100
100
|
*/
|
|
101
|
-
export declare function render_made(node: Node, render_tags: ReadonlySet<string>): boolean;
|
|
101
|
+
export declare function render_made(node: Node, render_tags: ReadonlySet<string>): boolean;
|
|
102
|
+
/** Record what the server sent `root` (`<body>`): the attribute names on `sent` (the root as the
|
|
103
|
+
* server rendered it), and `children` (the live root's nodes that are the server's) as render output. */
|
|
104
|
+
export declare function record_server_body(root: Element, sent: Element, children: Iterable<Node>): void;
|
|
105
|
+
/** The attribute names the server last sent `root`, or null for a root with no record yet (all of
|
|
106
|
+
* it is then taken to be the server's). */
|
|
107
|
+
export declare function server_body_attributes(root: Element): ReadonlySet<string> | null;
|
|
108
|
+
/** Is `node`, a child of `<body>`, the server's? Recorded, or an `<ogygia-region>`. Anything else
|
|
109
|
+
* the page's scripts put there: a navigation neither matches nor removes it. */
|
|
110
|
+
export declare function server_body_child(node: Node): boolean;
|
|
@@ -112,4 +112,22 @@ function render_made(node, render_tags) {
|
|
|
112
112
|
const name = node.localName;
|
|
113
113
|
return name === REGION || render_tags.has(name);
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
const NO_TAGS = /* @__PURE__ */ new Set();
|
|
116
|
+
const server_attrs = /* @__PURE__ */ new WeakMap();
|
|
117
|
+
/** Record what the server sent `root` (`<body>`): the attribute names on `sent` (the root as the
|
|
118
|
+
* server rendered it), and `children` (the live root's nodes that are the server's) as render output. */
|
|
119
|
+
function record_server_body(root, sent, children) {
|
|
120
|
+
server_attrs.set(root, new Set(Array.from(sent.attributes, (a) => a.name)));
|
|
121
|
+
for (const n of children) mark_render_made(n);
|
|
122
|
+
}
|
|
123
|
+
/** The attribute names the server last sent `root`, or null for a root with no record yet (all of
|
|
124
|
+
* it is then taken to be the server's). */
|
|
125
|
+
function server_body_attributes(root) {
|
|
126
|
+
return server_attrs.get(root) ?? null;
|
|
127
|
+
}
|
|
128
|
+
/** Is `node`, a child of `<body>`, the server's? Recorded, or an `<ogygia-region>`. Anything else
|
|
129
|
+
* the page's scripts put there: a navigation neither matches nor removes it. */
|
|
130
|
+
function server_body_child(node) {
|
|
131
|
+
return render_made(node, NO_TAGS);
|
|
132
|
+
}
|
|
133
|
+
export { BAKED_ATTR, HTML_PARENT_ATTR, OPAQUE_ATTR, REGION_RENDER_ATTRS, attributes_writer, is_declared_foreign, is_region_owned, is_self_owned, is_upgraded_ce, mark_render_made, order_owner, owner_of, record_server_body, render_made, server_body_attributes, server_body_child, slots_light_dom, walk_enters };
|
|
@@ -27,7 +27,9 @@ export declare function stamp_region_keys(root: ParentNode): void;
|
|
|
27
27
|
* here, on either document.
|
|
28
28
|
*/
|
|
29
29
|
export declare const NO_RECONCILE_SELECTOR = "[data-og-no-reconcile]";
|
|
30
|
-
export declare function reconcile_body(live: Element, next: Element, morph: (parent: Element, nodes: ArrayLike<Node
|
|
30
|
+
export declare function reconcile_body(live: Element, next: Element, morph: (parent: Element, nodes: ArrayLike<Node>, options?: {
|
|
31
|
+
stays?: (node: Node) => boolean;
|
|
32
|
+
}) => void): void;
|
|
31
33
|
/**
|
|
32
34
|
* outerSync body swap — the FALLBACK path (reconcile off, or a region trapped in a shadow root the
|
|
33
35
|
* morph can't pierce). The old code did `document.body.replaceWith(doc.body)`, which THROWS AWAY the
|
|
@@ -35,7 +37,8 @@ export declare function reconcile_body(live: Element, next: Element, morph: (par
|
|
|
35
37
|
* analytics, a scroll library) attached to `document.body`. htmx 4 boosts land the same way and use
|
|
36
38
|
* `outerSync` for exactly this reason: keep the element, sync its attributes, replace its children.
|
|
37
39
|
* So we do too — the live body node survives, only its guts change. Incoming children are moved out
|
|
38
|
-
* of the parsed document (insertion adopts them across documents).
|
|
40
|
+
* of the parsed document (insertion adopts them across documents). The page's own children (a
|
|
41
|
+
* portal's anchor) stay, as in the reconcile; the server's are replaced.
|
|
39
42
|
*/
|
|
40
43
|
export declare function swap_body(live: HTMLElement, next: HTMLElement): void;
|
|
41
44
|
export { endpoint_key, fingerprint_of, fnv1a, signature_of };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dispose_ids } from "../ref.js";
|
|
2
|
-
import { boot_link, morph_sync_attributes } from "./slots.js";
|
|
2
|
+
import { boot_link, morph_sync_attributes, slots } from "./slots.js";
|
|
3
3
|
import { region_owned_ids } from "./region-ids.js";
|
|
4
4
|
import { emit } from "../events/bus.js";
|
|
5
5
|
import { endpoint_key, fingerprint_of, fnv1a, signature_of } from "./hash.js";
|
|
@@ -27,22 +27,48 @@ import { endpoint_key, fingerprint_of, fnv1a, signature_of } from "./hash.js";
|
|
|
27
27
|
const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
|
|
28
28
|
const props_sidecar_of = (region) => boot_link().props_sidecar_of(region);
|
|
29
29
|
/**
|
|
30
|
-
* `<body>`
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
30
|
+
* `<body>` has two owners (runtime/ownership.ts): the server, page by page (a theme class, a layout
|
|
31
|
+
* flag, the content), and the scripts living on the page (a scroll lock, a consent banner's state; a
|
|
32
|
+
* component mounted into the body, such as a popover's portal). A navigation moves the server's part
|
|
33
|
+
* to the new page's and leaves the page's part alone:
|
|
34
|
+
* - ATTRIBUTES: it removes only those the PREVIOUS page's server sent that the new one doesn't;
|
|
35
|
+
* - CHILDREN: the morph steps over a child the server never sent (a portal's mount anchor: taking
|
|
36
|
+
* it away leaves the portal inserting its content beside a detached node, so a popover in a kept
|
|
37
|
+
* island never opens again).
|
|
38
|
+
* ONE record per reconciled root (ownership.ts `record_server_body`), reached through the boot
|
|
39
|
+
* registry like every boot helper this lazy chunk uses: the router takes the body's when it starts,
|
|
40
|
+
* before any island wakes; each reconcile renews its root's (a harness driving it on its own root
|
|
41
|
+
* gets one from its first call). A root with no record yet is all the server's: every child is
|
|
42
|
+
* matched, and no attribute is taken away.
|
|
37
43
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
/** The record of what the server last sent `live`, or null while there is none. */
|
|
45
|
+
function body_record(live) {
|
|
46
|
+
const link = slots.boot;
|
|
47
|
+
return link && link.server_body_attributes(live) !== null ? link : null;
|
|
48
|
+
}
|
|
40
49
|
function sync_body_attributes(live, next) {
|
|
41
50
|
morph_sync_attributes()(live, next, true);
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
const sent = body_record(live)?.server_body_attributes(live);
|
|
52
|
+
if (sent) {
|
|
53
|
+
for (const name of sent) if (!next.hasAttribute(name)) live.removeAttribute(name);
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
}
|
|
56
|
+
/** The live body's children that are the page's own, not the server's (none without a record). */
|
|
57
|
+
function page_own_children(live) {
|
|
58
|
+
const own = /* @__PURE__ */ new Set();
|
|
59
|
+
const record = body_record(live);
|
|
60
|
+
if (record === null) return own;
|
|
61
|
+
for (let n = live.firstChild; n; n = n.nextSibling) if (!record.server_body_child(n)) own.add(n);
|
|
62
|
+
return own;
|
|
63
|
+
}
|
|
64
|
+
/** After a navigation the server's body is the new page's: its attributes, and every live child that
|
|
65
|
+
* is not the page's own. */
|
|
66
|
+
function renew_server_body(live, next, own) {
|
|
67
|
+
const link = slots.boot;
|
|
68
|
+
if (!link) return;
|
|
69
|
+
const children = [];
|
|
70
|
+
for (let n = live.firstChild; n; n = n.nextSibling) if (!own.has(n)) children.push(n);
|
|
71
|
+
link.record_server_body(live, next, children);
|
|
46
72
|
}
|
|
47
73
|
/** The `<script data-ogygia-props>` seed text of a region — keyed at the end of the body or the
|
|
48
74
|
* adjacent sibling (runtime/sidecar.ts) — or '' — the same lookup the hydrator uses, read-only.
|
|
@@ -143,6 +169,7 @@ function dt_emit_reconcile(live, next) {
|
|
|
143
169
|
function reconcile_body(live, next, morph) {
|
|
144
170
|
stamp_region_keys(live);
|
|
145
171
|
stamp_region_keys(next);
|
|
172
|
+
const own = page_own_children(live);
|
|
146
173
|
sync_body_attributes(live, next);
|
|
147
174
|
if (DEVTOOLS) dt_emit_reconcile(live, next);
|
|
148
175
|
const next_keys = /* @__PURE__ */ new Set();
|
|
@@ -158,7 +185,8 @@ function reconcile_body(live, next, morph) {
|
|
|
158
185
|
if (set !== void 0) for (const id of set) removed_ids.push(id);
|
|
159
186
|
}
|
|
160
187
|
absorb_kept_props(live, next);
|
|
161
|
-
morph(live, next.childNodes);
|
|
188
|
+
morph(live, next.childNodes, own.size > 0 ? { stays: (n) => own.has(n) } : void 0);
|
|
189
|
+
renew_server_body(live, next, own);
|
|
162
190
|
if (removed_ids.length > 0) dispose_ids(removed_ids);
|
|
163
191
|
}
|
|
164
192
|
/**
|
|
@@ -168,12 +196,20 @@ function reconcile_body(live, next, morph) {
|
|
|
168
196
|
* analytics, a scroll library) attached to `document.body`. htmx 4 boosts land the same way and use
|
|
169
197
|
* `outerSync` for exactly this reason: keep the element, sync its attributes, replace its children.
|
|
170
198
|
* So we do too — the live body node survives, only its guts change. Incoming children are moved out
|
|
171
|
-
* of the parsed document (insertion adopts them across documents).
|
|
199
|
+
* of the parsed document (insertion adopts them across documents). The page's own children (a
|
|
200
|
+
* portal's anchor) stay, as in the reconcile; the server's are replaced.
|
|
172
201
|
*/
|
|
173
202
|
function swap_body(live, next) {
|
|
203
|
+
const own = page_own_children(live);
|
|
174
204
|
sync_body_attributes(live, next);
|
|
175
|
-
live.replaceChildren();
|
|
205
|
+
if (own.size === 0) live.replaceChildren();
|
|
206
|
+
else for (let n = live.firstChild; n;) {
|
|
207
|
+
const here = n;
|
|
208
|
+
n = n.nextSibling;
|
|
209
|
+
if (!own.has(here)) live.removeChild(here);
|
|
210
|
+
}
|
|
176
211
|
while (next.firstChild) live.appendChild(next.firstChild);
|
|
212
|
+
renew_server_body(live, next, own);
|
|
177
213
|
}
|
|
178
214
|
/** For each live region whose reconcile key also appears in `next`, push the incoming region's
|
|
179
215
|
* props into the live island (kept regions only — a re-mounting region gets its props on mount). */
|
|
@@ -143,6 +143,7 @@ function warm_key(url) {
|
|
|
143
143
|
function warm_island_module(entry, base, cls = "speculative") {
|
|
144
144
|
const url = island_module_url(entry, base);
|
|
145
145
|
if (!url) return;
|
|
146
|
+
if (!entry_location(entry, base) && url.includes("/ogygia/og-region.")) return;
|
|
146
147
|
const key = warm_key(url);
|
|
147
148
|
if (warmed_modules.has(key)) return;
|
|
148
149
|
warmed_modules.add(key);
|