@markless/router 0.5.0 → 0.7.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.
- package/dist/create-server-entry-yboa-2m6.js +1333 -0
- package/dist/index-BjxpxRL7.d.ts +248 -0
- package/dist/index.d.ts +2 -233
- package/dist/index.js +64 -15
- package/dist/request-locals-BDAhNpEa.js +94 -0
- package/dist/route-manifest-DMizh42B.js +456 -0
- package/dist/typescript-plugin.cjs +1 -1
- package/dist/typescript-plugin.js +1 -1
- package/dist/vite/runtime/create-route-discovery.js +1 -1
- package/dist/vite/runtime/create-server-entry.d.ts +16 -0
- package/dist/vite/runtime/create-server-entry.js +2 -658
- package/dist/vite/runtime/mdx-route.d.ts +21 -2
- package/dist/vite/runtime/mdx-route.js +65 -3
- package/dist/vite.js +239 -39
- package/package.json +4 -4
- package/src/vite/entries/fragment-entry.ts +290 -32
- package/dist/fragment-navigation-CIiNnFFc.js +0 -323
- package/dist/link-attributes-nY1Qkmx2.js +0 -7
- package/dist/request-files-Ca8E0FOo.js +0 -233
- package/dist/request-locals-BFPF6Vdm.js +0 -28
- package/dist/route-manifest-Bw_nFohL.js +0 -189
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markless/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Markless router: client-side navigation with SSR streaming and build-time preload maps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
"pathe": "^2.0.3",
|
|
65
65
|
"satteri": "0.6.3",
|
|
66
66
|
"ufo": "^1.6.4",
|
|
67
|
-
"@markless/
|
|
68
|
-
"@markless/
|
|
67
|
+
"@markless/web": "0.7.0",
|
|
68
|
+
"@markless/bundler": "0.7.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@markless/analyzer": "0.
|
|
71
|
+
"@markless/analyzer": "0.7.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"typescript": "^5.9.3",
|
|
@@ -3,17 +3,24 @@ import type { ModulePreloadInput } from '@markless/web/render-to-string';
|
|
|
3
3
|
|
|
4
4
|
export interface FragmentNavigationConfig {
|
|
5
5
|
readonly entry: string;
|
|
6
|
+
readonly entryImports: readonly string[];
|
|
6
7
|
readonly linkAttribute: string;
|
|
7
8
|
readonly prefetchAttribute: string;
|
|
8
9
|
readonly replaceAttribute: string;
|
|
9
10
|
readonly scrollAttribute: string;
|
|
10
11
|
readonly routeScriptType: string;
|
|
12
|
+
readonly servedHtmlAttributesProperty: string;
|
|
11
13
|
readonly retireProperty: string;
|
|
12
14
|
readonly streamExecutorGlobal: string;
|
|
13
15
|
readonly streamedArmSelector: string;
|
|
14
|
-
readonly
|
|
16
|
+
readonly requestPrefix: string;
|
|
15
17
|
readonly regionEndMarker: string;
|
|
16
18
|
readonly appendMarker: string;
|
|
19
|
+
// Close and open the page's nodes when the document's own element is the container; what is outside is the document's.
|
|
20
|
+
readonly pageEndComment: string;
|
|
21
|
+
readonly pageStartComment: string;
|
|
22
|
+
// The container's own attributes, never carried from the leaving page's element to the next one.
|
|
23
|
+
readonly containerAttributes: readonly [container: string, paths: string];
|
|
17
24
|
// 'document' apps swap the whole body: their shell renders from the request URL.
|
|
18
25
|
readonly documentLoads: boolean;
|
|
19
26
|
// A mouse fetches after resting this long on a link; press, focus and touch fetch at once.
|
|
@@ -23,7 +30,7 @@ export interface FragmentNavigationConfig {
|
|
|
23
30
|
readonly prefetchConnections: number;
|
|
24
31
|
// Paths the server answers with endpoints, not pages: never fetched before a click or as a fragment.
|
|
25
32
|
readonly endpointPrefixes: readonly string[];
|
|
26
|
-
// At most this many visible-link fragments are fetched at idle on fast connections,
|
|
33
|
+
// At most this many visible-link fragments are fetched at idle on fast connections, in document order; 0 keeps HTML to intent only.
|
|
27
34
|
readonly idleFragments: number;
|
|
28
35
|
readonly idleWithoutConnectionInfo: boolean;
|
|
29
36
|
// false: nothing is fetched before a click.
|
|
@@ -64,9 +71,11 @@ export type FragmentNavigationState = {
|
|
|
64
71
|
readonly prefetch: (url: URL) => void;
|
|
65
72
|
readonly parse: (url: string, text: string) => FragmentFetched;
|
|
66
73
|
readonly idleDownloads: boolean;
|
|
67
|
-
// Schedules the idle tier: at idle,
|
|
74
|
+
// Schedules the idle tier: at idle, fetch a few page fragments of the links in view; their code waits for intent.
|
|
68
75
|
readonly rescan: () => void;
|
|
69
76
|
readonly scrolled: Set<Element>;
|
|
77
|
+
// Code the intent started preloading, by absolute href, settling once each file has arrived.
|
|
78
|
+
readonly arriving: Map<string, Promise<unknown>>;
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
export type FragmentScroll =
|
|
@@ -167,12 +176,15 @@ function regionOf(state: FragmentNavigationState, doc: Document): Element | null
|
|
|
167
176
|
(
|
|
168
177
|
doc.querySelector(`script[type="${state.config.routeScriptType}"]`) ??
|
|
169
178
|
doc.querySelector('script[data-markless-router-link-resumer]')
|
|
170
|
-
)?.closest(
|
|
179
|
+
)?.closest(`[${state.config.containerAttributes[0]}]`) ?? null
|
|
171
180
|
);
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
function retire(state: FragmentNavigationState, root: Element): void {
|
|
175
|
-
for (const container of [
|
|
184
|
+
for (const container of [
|
|
185
|
+
root,
|
|
186
|
+
...root.querySelectorAll(`[${state.config.containerAttributes[0]}]`),
|
|
187
|
+
]) {
|
|
176
188
|
const retireContainer = (container as unknown as Record<string, unknown>)[
|
|
177
189
|
state.config.retireProperty
|
|
178
190
|
];
|
|
@@ -191,13 +203,157 @@ function reportAsync(error: unknown): void {
|
|
|
191
203
|
});
|
|
192
204
|
}
|
|
193
205
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
206
|
+
// The JavaScript MIME types of the HTML spec, which a browser runs as classic scripts.
|
|
207
|
+
const JAVASCRIPT_TYPE =
|
|
208
|
+
/^(?:|(?:text|application)\/(?:x-)?(?:java|ecma)script|text\/(?:javascript1\.[0-5]|jscript|livescript))$/;
|
|
209
|
+
|
|
210
|
+
function scriptKind(script: HTMLScriptElement): 'classic' | 'module' | undefined {
|
|
211
|
+
const type = script.getAttribute('type');
|
|
212
|
+
const language = script.getAttribute('language');
|
|
213
|
+
const essence = (type ?? (language ? `text/${language}` : ''))
|
|
214
|
+
.split(';')[0]!
|
|
215
|
+
.trim()
|
|
216
|
+
.toLowerCase();
|
|
217
|
+
if (essence === 'module') return 'module';
|
|
218
|
+
if (!JAVASCRIPT_TYPE.test(essence) || script.hasAttribute('nomodule')) return undefined;
|
|
219
|
+
return 'classic';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// A parsed or adopted script never runs; a fresh element with its attributes and text does.
|
|
223
|
+
function runnable(d: Document, script: HTMLScriptElement): HTMLScriptElement {
|
|
197
224
|
const clone = d.createElement('script');
|
|
198
225
|
for (const attribute of script.attributes) clone.setAttribute(attribute.name, attribute.value);
|
|
199
226
|
clone.textContent = script.textContent;
|
|
200
|
-
|
|
227
|
+
return clone;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const settles = (script: HTMLScriptElement) =>
|
|
231
|
+
new Promise<unknown>((done) => {
|
|
232
|
+
script.addEventListener('load', done, { once: true });
|
|
233
|
+
script.addEventListener('error', done, { once: true });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// External files this document already ran: a page that includes one again keeps the loaded copy.
|
|
237
|
+
const ranFiles = new WeakMap<Document, Set<string>>();
|
|
238
|
+
|
|
239
|
+
function filesRan(d: Document): Set<string> {
|
|
240
|
+
let files = ranFiles.get(d);
|
|
241
|
+
if (!files) ranFiles.set(d, (files = new Set()));
|
|
242
|
+
for (const script of d.scripts) if (script.src && scriptKind(script)) files.add(script.src);
|
|
243
|
+
return files;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
type LoadListener = {
|
|
247
|
+
readonly target: EventTarget;
|
|
248
|
+
readonly type: string;
|
|
249
|
+
readonly listener: EventListenerOrEventListenerObject;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// The document fired DOMContentLoaded and load already: listeners the page's scripts add are called once those scripts ran.
|
|
253
|
+
function captureLoadListeners(d: Document, w: Window, into: LoadListener[]): () => void {
|
|
254
|
+
const targets: Array<readonly [EventTarget, string]> = [];
|
|
255
|
+
if (d.readyState !== 'loading') targets.push([d, 'DOMContentLoaded']);
|
|
256
|
+
if (d.readyState === 'complete') targets.push([w, 'load']);
|
|
257
|
+
const restores = targets.map(([target, type]) => {
|
|
258
|
+
const own = Object.getOwnPropertyDescriptor(target, 'addEventListener');
|
|
259
|
+
const add = target.addEventListener;
|
|
260
|
+
target.addEventListener = function (
|
|
261
|
+
this: EventTarget,
|
|
262
|
+
kind: string,
|
|
263
|
+
listener: EventListenerOrEventListenerObject | null,
|
|
264
|
+
options?: boolean | AddEventListenerOptions,
|
|
265
|
+
) {
|
|
266
|
+
if (kind === type && listener && this === target) into.push({ target, type, listener });
|
|
267
|
+
return add.call(this, kind, listener, options);
|
|
268
|
+
};
|
|
269
|
+
return () => {
|
|
270
|
+
if (own) Object.defineProperty(target, 'addEventListener', own);
|
|
271
|
+
else delete (target as { addEventListener?: unknown }).addEventListener;
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
return () => {
|
|
275
|
+
for (const restore of restores) restore();
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function callLoadListeners(listeners: readonly LoadListener[], type: string): void {
|
|
280
|
+
const event = new Event(type);
|
|
281
|
+
for (const entry of listeners) {
|
|
282
|
+
if (entry.type !== type) continue;
|
|
283
|
+
try {
|
|
284
|
+
if (typeof entry.listener === 'function') entry.listener.call(entry.target, event);
|
|
285
|
+
else entry.listener.handleEvent(event);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
reportAsync(error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A landing's order: blocking classic scripts one by one, async as they arrive, then deferred and module scripts, then load listeners.
|
|
293
|
+
async function runScripts(
|
|
294
|
+
d: Document,
|
|
295
|
+
scripts: readonly HTMLScriptElement[],
|
|
296
|
+
ran: Set<string>,
|
|
297
|
+
current: () => boolean,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const w = d.defaultView!;
|
|
300
|
+
const listeners: LoadListener[] = [];
|
|
301
|
+
const release = captureLoadListeners(d, w, listeners);
|
|
302
|
+
try {
|
|
303
|
+
const deferred: HTMLScriptElement[] = [];
|
|
304
|
+
for (const script of scripts) {
|
|
305
|
+
const kind = scriptKind(script);
|
|
306
|
+
if (!kind || !script.isConnected) continue;
|
|
307
|
+
const src = script.hasAttribute('src') ? script.src : '';
|
|
308
|
+
if (src && ran.has(src)) continue;
|
|
309
|
+
if (src) ran.add(src);
|
|
310
|
+
const async = script.hasAttribute('async') && (src !== '' || kind === 'module');
|
|
311
|
+
if (!async && (kind === 'module' || (src && script.hasAttribute('defer')))) {
|
|
312
|
+
deferred.push(script);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const clone = runnable(d, script);
|
|
316
|
+
if (async || !src) {
|
|
317
|
+
script.replaceWith(clone);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
clone.async = false;
|
|
321
|
+
const done = settles(clone);
|
|
322
|
+
script.replaceWith(clone);
|
|
323
|
+
await done;
|
|
324
|
+
if (!current()) return;
|
|
325
|
+
}
|
|
326
|
+
let last: Promise<unknown> | undefined;
|
|
327
|
+
let tail: HTMLScriptElement | undefined;
|
|
328
|
+
for (const script of deferred) {
|
|
329
|
+
if (!script.isConnected) continue;
|
|
330
|
+
const clone = runnable(d, script);
|
|
331
|
+
clone.async = false;
|
|
332
|
+
last = clone.src ? settles(clone) : undefined;
|
|
333
|
+
tail = clone;
|
|
334
|
+
script.replaceWith(clone);
|
|
335
|
+
}
|
|
336
|
+
// An inline module fires no load event, so a marker queued after the last one says when it ran.
|
|
337
|
+
if (tail && !last) {
|
|
338
|
+
const key = `__marklessScriptsRan${Math.random().toString(36).slice(2)}`;
|
|
339
|
+
const host = w as unknown as Record<string, unknown>;
|
|
340
|
+
last = new Promise((done) => (host[key] = done));
|
|
341
|
+
const marker = d.createElement('script');
|
|
342
|
+
marker.type = 'module';
|
|
343
|
+
marker.async = false;
|
|
344
|
+
if (tail.nonce) marker.nonce = tail.nonce;
|
|
345
|
+
marker.textContent = `window.${key}();`;
|
|
346
|
+
d.head.append(marker);
|
|
347
|
+
await last;
|
|
348
|
+
delete host[key];
|
|
349
|
+
marker.remove();
|
|
350
|
+
} else await last;
|
|
351
|
+
if (!current()) return;
|
|
352
|
+
callLoadListeners(listeners, 'DOMContentLoaded');
|
|
353
|
+
callLoadListeners(listeners, 'load');
|
|
354
|
+
} finally {
|
|
355
|
+
release();
|
|
356
|
+
}
|
|
201
357
|
}
|
|
202
358
|
|
|
203
359
|
function loaded(w: Window, links: readonly HTMLLinkElement[]): Promise<unknown> {
|
|
@@ -235,9 +391,8 @@ async function mergeHead(d: Document, next: Document) {
|
|
|
235
391
|
else d.head.prepend(node);
|
|
236
392
|
if (node.localName === 'link' && (node as HTMLLinkElement).rel === 'stylesheet')
|
|
237
393
|
stylesheets.push(node as HTMLLinkElement);
|
|
238
|
-
// A kept
|
|
239
|
-
if (node.localName === 'script'
|
|
240
|
-
scripts.push(node as HTMLScriptElement);
|
|
394
|
+
// A kept head script already ran on this document; only one the destination adds runs.
|
|
395
|
+
if (node.localName === 'script') scripts.push(node as HTMLScriptElement);
|
|
241
396
|
}
|
|
242
397
|
cursor = node;
|
|
243
398
|
}
|
|
@@ -255,10 +410,13 @@ async function mergeHead(d: Document, next: Document) {
|
|
|
255
410
|
// Only attributes a server document set are the server's to remove: scripts own the rest (a theme class).
|
|
256
411
|
const serverHtmlAttributes = new WeakMap<Document, Set<string>>();
|
|
257
412
|
|
|
258
|
-
function syncHtmlAttributes(d: Document, next: Document): void {
|
|
413
|
+
function syncHtmlAttributes(d: Document, next: Document, servedProperty: string): void {
|
|
259
414
|
const root = d.documentElement;
|
|
260
415
|
const incoming = next.documentElement;
|
|
261
|
-
|
|
416
|
+
const served =
|
|
417
|
+
serverHtmlAttributes.get(d) ??
|
|
418
|
+
((root as unknown as Record<string, readonly string[] | undefined>)[servedProperty] || []);
|
|
419
|
+
for (const name of served)
|
|
262
420
|
if (!incoming.hasAttribute(name)) root.removeAttribute(name);
|
|
263
421
|
for (const attribute of incoming.attributes)
|
|
264
422
|
if (root.getAttribute(attribute.name) !== attribute.value)
|
|
@@ -266,12 +424,62 @@ function syncHtmlAttributes(d: Document, next: Document): void {
|
|
|
266
424
|
serverHtmlAttributes.set(d, new Set(incoming.getAttributeNames()));
|
|
267
425
|
}
|
|
268
426
|
|
|
427
|
+
// Set from the page's config at the start of every swap.
|
|
428
|
+
let containerAttribute = '';
|
|
429
|
+
|
|
269
430
|
const isContainer = (node: Node): boolean =>
|
|
270
|
-
node.nodeType === 1 && (node as Element).hasAttribute(
|
|
431
|
+
node.nodeType === 1 && (node as Element).hasAttribute(containerAttribute);
|
|
432
|
+
|
|
433
|
+
function pageEnd(host: Node, data: string): Comment | undefined {
|
|
434
|
+
for (let node = host.lastChild; node; node = node.previousSibling)
|
|
435
|
+
if (node.nodeType === 8 && (node as Comment).data === data) return node as Comment;
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function pageStart(host: Node, data: string): Comment | undefined {
|
|
440
|
+
for (let node = host.firstChild; node; node = node.nextSibling)
|
|
441
|
+
if (node.nodeType === 8 && (node as Comment).data === data) return node as Comment;
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function nodesBefore(d: Document, node: Node): DocumentFragment {
|
|
446
|
+
const nodes = d.createDocumentFragment();
|
|
447
|
+
while (node.previousSibling) nodes.prepend(node.previousSibling);
|
|
448
|
+
return nodes;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function nodesAfter(d: Document, node: Node): DocumentFragment {
|
|
452
|
+
const nodes = d.createDocumentFragment();
|
|
453
|
+
while (node.nextSibling) nodes.append(node.nextSibling);
|
|
454
|
+
return nodes;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const scriptsIn = (element: Element): Element[] =>
|
|
458
|
+
element.localName === 'script' ? [element] : [...element.querySelectorAll('script')];
|
|
459
|
+
|
|
460
|
+
const serverHostAttributes = new WeakMap<Document, Set<string>>();
|
|
461
|
+
|
|
462
|
+
// The incoming container is a fresh element; attributes a script set on the outgoing one stay.
|
|
463
|
+
function carryHostAttributes(
|
|
464
|
+
d: Document,
|
|
465
|
+
from: Element,
|
|
466
|
+
to: Element,
|
|
467
|
+
own: readonly string[],
|
|
468
|
+
): void {
|
|
469
|
+
const server = serverHostAttributes.get(d);
|
|
470
|
+
serverHostAttributes.set(d, new Set(to.getAttributeNames()));
|
|
471
|
+
for (const attribute of from.attributes)
|
|
472
|
+
if (
|
|
473
|
+
!to.hasAttribute(attribute.name) &&
|
|
474
|
+
!server?.has(attribute.name) &&
|
|
475
|
+
!own.includes(attribute.name)
|
|
476
|
+
)
|
|
477
|
+
to.setAttribute(attribute.name, attribute.value);
|
|
478
|
+
}
|
|
271
479
|
|
|
272
480
|
// A document app's shell has no runtime, so it is morphed in place: unchanged chrome keeps its nodes.
|
|
273
481
|
// Containers are always replaced whole: their DOM is the new page's resume payload's to locate.
|
|
274
|
-
function morphChildren(d: Document, from:
|
|
482
|
+
function morphChildren(d: Document, from: Node, to: Node, fresh: Element[]): void {
|
|
275
483
|
let cursor = from.firstChild;
|
|
276
484
|
const skipContainers = () => {
|
|
277
485
|
while (cursor && isContainer(cursor)) {
|
|
@@ -409,7 +617,7 @@ async function streamAppends(
|
|
|
409
617
|
}
|
|
410
618
|
}
|
|
411
619
|
flush(true);
|
|
412
|
-
end.parentNode?.removeChild(end);
|
|
620
|
+
if ((end as Comment).data !== state.config.pageEndComment) end.parentNode?.removeChild(end);
|
|
413
621
|
return signal.aborted ? undefined : all;
|
|
414
622
|
}
|
|
415
623
|
|
|
@@ -422,6 +630,7 @@ export async function navigate(
|
|
|
422
630
|
const state = (d as FragmentHost).__marklessFragmentNav;
|
|
423
631
|
const w = d.defaultView;
|
|
424
632
|
if (!state || !w) return;
|
|
633
|
+
containerAttribute = state.config.containerAttributes[0];
|
|
425
634
|
const session = sessionFor(d);
|
|
426
635
|
const id = ++session.navigation;
|
|
427
636
|
session.streaming?.abort();
|
|
@@ -442,12 +651,24 @@ export async function navigate(
|
|
|
442
651
|
const documentLoads = state.config.documentLoads;
|
|
443
652
|
const incoming = documentLoads ? next.body : regionOf(state, next);
|
|
444
653
|
const outgoing = documentLoads ? d.body : regionOf(state, d);
|
|
445
|
-
const scope = documentLoads ? d.body : outgoing?.parentElement;
|
|
654
|
+
const scope = documentLoads && !isContainer(d.body) ? d.body : outgoing?.parentElement;
|
|
446
655
|
if (!incoming || !outgoing || !scope) {
|
|
447
656
|
fallback(fetched.url);
|
|
448
657
|
return;
|
|
449
658
|
}
|
|
659
|
+
// The destination shows once its own code has arrived, so its first interaction never waits on a fetch.
|
|
660
|
+
const code = [...next.head.querySelectorAll<HTMLLinkElement>('link[rel="modulepreload"]')].map(
|
|
661
|
+
(link) => state.arriving.get(new URL(link.getAttribute('href') ?? '', d.baseURI).href),
|
|
662
|
+
);
|
|
663
|
+
for (const item of state.routes[url.pathname] ?? [])
|
|
664
|
+
code.push(
|
|
665
|
+
state.arriving.get(
|
|
666
|
+
new URL(typeof item === 'string' ? item : item.href, d.baseURI).href,
|
|
667
|
+
),
|
|
668
|
+
);
|
|
669
|
+
const ranFiles = filesRan(d);
|
|
450
670
|
const head = await mergeHead(d, next);
|
|
671
|
+
await Promise.race([Promise.all(code), new Promise((resolve) => w.setTimeout(resolve, 3000))]);
|
|
451
672
|
if (id !== session.navigation) return taken.abort();
|
|
452
673
|
let restore = scroll;
|
|
453
674
|
if (mode === 'traverse') {
|
|
@@ -485,31 +706,67 @@ export async function navigate(
|
|
|
485
706
|
(w as unknown as Record<string, unknown>)[state.config.streamExecutorGlobal] = undefined;
|
|
486
707
|
for (const element of d.querySelectorAll(state.config.streamedArmSelector)) element.remove();
|
|
487
708
|
state.shown = pageKey(new URL(w.location.href));
|
|
488
|
-
if (documentLoads) syncHtmlAttributes(d, next);
|
|
489
|
-
const fresh: Element[] = [];
|
|
709
|
+
if (documentLoads) syncHtmlAttributes(d, next, state.config.servedHtmlAttributesProperty);
|
|
490
710
|
let region: Element;
|
|
491
|
-
|
|
711
|
+
let end: Comment;
|
|
712
|
+
let scripts: Element[];
|
|
713
|
+
if (documentLoads && !isContainer(d.body)) {
|
|
714
|
+
const fresh: Element[] = [];
|
|
492
715
|
region = d.body;
|
|
493
716
|
morphChildren(d, region, incoming, fresh);
|
|
717
|
+
scripts = fresh.flatMap(scriptsIn);
|
|
718
|
+
region.append((end = d.createComment('')));
|
|
494
719
|
} else {
|
|
495
720
|
region = d.adoptNode(incoming);
|
|
496
|
-
outgoing.
|
|
497
|
-
|
|
721
|
+
const leavingEnd = pageEnd(outgoing, state.config.pageEndComment);
|
|
722
|
+
if (leavingEnd) {
|
|
723
|
+
carryHostAttributes(d, outgoing, region, state.config.containerAttributes);
|
|
724
|
+
end =
|
|
725
|
+
pageEnd(region, state.config.pageEndComment) ??
|
|
726
|
+
region.appendChild(d.createComment(state.config.pageEndComment));
|
|
727
|
+
// The document's own nodes after the page stay; a document app morphs them to its next shell.
|
|
728
|
+
const focused = d.activeElement as HTMLElement | null;
|
|
729
|
+
const kept = nodesAfter(d, leavingEnd);
|
|
730
|
+
const arriving = nodesAfter(d, end);
|
|
731
|
+
const fresh: Element[] = [];
|
|
732
|
+
if (documentLoads) morphChildren(d, kept, arriving, fresh);
|
|
733
|
+
const leavingStart = pageStart(outgoing, state.config.pageStartComment);
|
|
734
|
+
let start = pageStart(region, state.config.pageStartComment);
|
|
735
|
+
if (leavingStart || start) {
|
|
736
|
+
start ??= region.insertBefore(
|
|
737
|
+
d.createComment(state.config.pageStartComment),
|
|
738
|
+
region.firstChild,
|
|
739
|
+
);
|
|
740
|
+
const keptBefore = leavingStart
|
|
741
|
+
? nodesBefore(d, leavingStart)
|
|
742
|
+
: d.createDocumentFragment();
|
|
743
|
+
const arrivingBefore = nodesBefore(d, start);
|
|
744
|
+
if (documentLoads) morphChildren(d, keptBefore, arrivingBefore, fresh);
|
|
745
|
+
region.insertBefore(keptBefore, start);
|
|
746
|
+
}
|
|
747
|
+
scripts = [...scriptsIn(region), ...fresh.flatMap(scriptsIn)];
|
|
748
|
+
region.append(kept);
|
|
749
|
+
outgoing.replaceWith(region);
|
|
750
|
+
// The document's own nodes moved to the new element; focus inside them stays put.
|
|
751
|
+
if (focused && focused !== d.activeElement && region.contains(focused))
|
|
752
|
+
focused.focus({ preventScroll: true });
|
|
753
|
+
} else {
|
|
754
|
+
scripts = scriptsIn(region);
|
|
755
|
+
outgoing.replaceWith(region);
|
|
756
|
+
region.after((end = d.createComment('')));
|
|
757
|
+
}
|
|
498
758
|
}
|
|
499
759
|
// 'instant': a `scroll-behavior: smooth` page would otherwise animate every landing.
|
|
500
760
|
for (const box of offsets)
|
|
501
761
|
sameBox(scope, box)?.scrollTo({ top: box.top, left: box.left, behavior: 'instant' });
|
|
502
|
-
const end = d.createComment('');
|
|
503
|
-
if (documentLoads) region.append(end);
|
|
504
|
-
else region.after(end);
|
|
505
762
|
for (const element of head.stale) element.remove();
|
|
506
763
|
if (next.title && d.title !== next.title) d.title = next.title;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
764
|
+
const ran = runScripts(
|
|
765
|
+
d,
|
|
766
|
+
[...head.scripts, ...(scripts as HTMLScriptElement[])],
|
|
767
|
+
ranFiles,
|
|
768
|
+
() => id === session.navigation,
|
|
769
|
+
);
|
|
513
770
|
const active = d.activeElement as HTMLElement | null;
|
|
514
771
|
if (active && active !== d.body && !region.contains(active)) active.blur?.();
|
|
515
772
|
(region.querySelector('[autofocus]') as HTMLElement | null)?.focus();
|
|
@@ -540,4 +797,5 @@ export async function navigate(
|
|
|
540
797
|
if (w.requestIdleCallback) w.requestIdleCallback(idle, { timeout: 2000 });
|
|
541
798
|
else w.setTimeout(idle, 200);
|
|
542
799
|
}, reportAsync);
|
|
800
|
+
await ran;
|
|
543
801
|
}
|