@markless/router 0.8.0 → 0.9.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-BsYTWdxi.js → create-server-entry-CVhSjR45.js} +29 -4
- package/dist/index.js +1 -1
- package/dist/{request-locals-BDAhNpEa.js → request-locals-BPfJaoM_.js} +3 -2
- package/dist/vite/runtime/create-server-entry.js +1 -1
- package/dist/vite/runtime/mdx-route.d.ts +2 -1
- package/dist/vite/runtime/mdx-route.js +1 -1
- package/dist/vite.js +34 -7
- package/package.json +5 -5
- package/src/vite/entries/fragment-entry.ts +257 -104
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ROUTE_SCRIPT_TYPE,
|
|
1
|
+
import { a as ROUTE_SCRIPT_TYPE, d as PREFETCH_ATTRIBUTE, f as REPLACE_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, l as servedHtmlAttributesScript, n as requestLocals, o as SERVED_BODY_ATTRIBUTES_PROPERTY, p as SCROLL_ATTRIBUTE, r as takeMiddlewareHeaders, s as SERVED_HTML_ATTRIBUTES_PROPERTY, u as LINK_ATTRIBUTE } from "./request-locals-BPfJaoM_.js";
|
|
2
2
|
import { i as matchRouteManifest, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId } from "./route-manifest-DmoxXanW.js";
|
|
3
3
|
import { n as __marklessDebugBootstrapSource } from "./debug-channel-Di6SppZv.js";
|
|
4
4
|
import { CONTAINER_ATTRIBUTE, PROTOCOL_CONTAINER_END_COMMENT, PROTOCOL_CONTAINER_START_COMMENT, PROTOCOL_LOCATOR_PATHS_ATTRIBUTE } from "@markless/web/fns/container-region";
|
|
@@ -252,13 +252,27 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
|
|
|
252
252
|
d.head.appendChild(link);
|
|
253
253
|
}
|
|
254
254
|
};
|
|
255
|
+
const parseAsLoaded = (html) => {
|
|
256
|
+
const raw = [];
|
|
257
|
+
const emptied = html.replace(/<!--[\s\S]*?-->|<(script|style|textarea|title)\b[^>]*>[\s\S]*?<\/\1\s*>|<noscript\b([^>]*)>([\s\S]*?)<\/noscript\s*>/gi, (match, _rawText, attributes, content) => {
|
|
258
|
+
if (content === void 0) return match;
|
|
259
|
+
raw.push(content);
|
|
260
|
+
return `<noscript${attributes ?? ""}></noscript>`;
|
|
261
|
+
});
|
|
262
|
+
if (!raw.length) return new DOMParser().parseFromString(html, "text/html");
|
|
263
|
+
const doc = new DOMParser().parseFromString(emptied, "text/html");
|
|
264
|
+
const tags = doc.getElementsByTagName("noscript");
|
|
265
|
+
if (tags.length !== raw.length) return new DOMParser().parseFromString(html, "text/html");
|
|
266
|
+
for (let index = 0; index < tags.length; index++) tags[index].textContent = raw[index];
|
|
267
|
+
return doc;
|
|
268
|
+
};
|
|
255
269
|
const parse = (url, text) => {
|
|
256
270
|
const at = text.indexOf(config.regionEndMarker);
|
|
257
271
|
const prefix = at === -1 ? text : text.slice(0, at);
|
|
258
272
|
return {
|
|
259
273
|
url,
|
|
260
274
|
prefix,
|
|
261
|
-
doc:
|
|
275
|
+
doc: parseAsLoaded(prefix),
|
|
262
276
|
tail: at === -1 ? "" : text.slice(at + config.regionEndMarker.length)
|
|
263
277
|
};
|
|
264
278
|
};
|
|
@@ -876,7 +890,7 @@ function createServerEntry(options) {
|
|
|
876
890
|
resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
|
|
877
891
|
modulePreloads: (html, resumes) => landingModulePreloads(resumes ? options.routeSsrModulePreloads?.[file] : void 0, linkBridges?.[html.includes("href=\"#/") ? 1 : 0])
|
|
878
892
|
};
|
|
879
|
-
const headStart = (
|
|
893
|
+
const headStart = (fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (fragment ? "" : options.importMap ?? "");
|
|
880
894
|
if (options.render === "blocking") {
|
|
881
895
|
const pageHtml = splitPageHtml(await renderToString(routedArtifact, renderOptions));
|
|
882
896
|
privateToVisitor();
|
|
@@ -1014,7 +1028,17 @@ function landingModulePreloads(routePreloads, linkBridgePath) {
|
|
|
1014
1028
|
const preloads = [];
|
|
1015
1029
|
addModulePreloads(preloads, /* @__PURE__ */ new Set(), routePreloads);
|
|
1016
1030
|
if (linkBridgePath) preloads.push(linkBridgePath);
|
|
1017
|
-
return preloads.length > 0 ? preloads : void 0;
|
|
1031
|
+
return preloads.length > 0 ? preloads.map(lowFetchPriority) : void 0;
|
|
1032
|
+
}
|
|
1033
|
+
function lowFetchPriority(preload) {
|
|
1034
|
+
if (typeof preload === "string") return {
|
|
1035
|
+
href: preload,
|
|
1036
|
+
fetchPriority: "low"
|
|
1037
|
+
};
|
|
1038
|
+
return preload.fetchPriority ? preload : {
|
|
1039
|
+
...preload,
|
|
1040
|
+
fetchPriority: "low"
|
|
1041
|
+
};
|
|
1018
1042
|
}
|
|
1019
1043
|
function routerLinkHrefs(html) {
|
|
1020
1044
|
return [...html.matchAll(/<a\b(?=[^>]*\bdata-markless-router-link(?:[\s=>]|$))(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
|
|
@@ -1290,6 +1314,7 @@ function fragmentConfig(fragment, documentLoads) {
|
|
|
1290
1314
|
scrollAttribute: SCROLL_ATTRIBUTE,
|
|
1291
1315
|
routeScriptType: ROUTE_SCRIPT_TYPE,
|
|
1292
1316
|
servedHtmlAttributesProperty: SERVED_HTML_ATTRIBUTES_PROPERTY,
|
|
1317
|
+
servedBodyAttributesProperty: SERVED_BODY_ATTRIBUTES_PROPERTY,
|
|
1293
1318
|
retireProperty: CONTAINER_RETIRE_PROPERTY,
|
|
1294
1319
|
streamExecutorGlobal: STREAM_ARM_EXECUTOR_GLOBAL,
|
|
1295
1320
|
streamedArmSelector: STREAMED_ARM_SELECTOR,
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as routeMountTarget, d as PREFETCH_ATTRIBUTE, f as REPLACE_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, p as SCROLL_ATTRIBUTE, t as rememberRequestLocals, u as LINK_ATTRIBUTE } from "./request-locals-BPfJaoM_.js";
|
|
2
2
|
import { a as normalizeRequestPathname, c as parseRequestFile, i as matchRouteManifest, l as transformRequestFileSource, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId, s as normalizeRequestFileId } from "./route-manifest-DmoxXanW.js";
|
|
3
3
|
import { createExternalDelegateStructureTokens, createExternalDelegateView } from "@markless/web/fns/external-delegate";
|
|
4
4
|
import { render } from "@markless/web/render";
|
|
@@ -9,8 +9,9 @@ const PREFETCH_ATTRIBUTE = "data-markless-router-prefetch";
|
|
|
9
9
|
const ROUTE_SCRIPT_TYPE = "@markless/core/route";
|
|
10
10
|
const HTML_ATTRIBUTES_SLOT = "__marklessRouterHtmlAttributes";
|
|
11
11
|
const SERVED_HTML_ATTRIBUTES_PROPERTY = "__marklessServedHtmlAttributes";
|
|
12
|
+
const SERVED_BODY_ATTRIBUTES_PROPERTY = "__marklessServedBodyAttributes";
|
|
12
13
|
function servedHtmlAttributesScript() {
|
|
13
|
-
return `<script>document.documentElement.${
|
|
14
|
+
return `<script>(()=>{const r=document.documentElement,p=(e)=>Array.from(e.attributes,(a)=>[a.name,a.value]),b=()=>!!document.body&&!!(r.${SERVED_BODY_ATTRIBUTES_PROPERTY}=p(document.body));r.${SERVED_HTML_ATTRIBUTES_PROPERTY}=p(r);b()||new MutationObserver((_,o)=>{b()&&o.disconnect()}).observe(r,{childList:true})})()<\/script>`;
|
|
14
15
|
}
|
|
15
16
|
function routeMountTarget(document, currentRoot) {
|
|
16
17
|
const root = currentRoot ?? document.querySelector(`script[type="@markless/core/route"]`)?.parentElement;
|
|
@@ -91,4 +92,4 @@ function takeMiddlewareHeaders(request) {
|
|
|
91
92
|
return record.taken;
|
|
92
93
|
}
|
|
93
94
|
//#endregion
|
|
94
|
-
export { ROUTE_SCRIPT_TYPE as a,
|
|
95
|
+
export { ROUTE_SCRIPT_TYPE as a, routeMountTarget as c, PREFETCH_ATTRIBUTE as d, REPLACE_ATTRIBUTE as f, HTML_ATTRIBUTES_SLOT as i, servedHtmlAttributesScript as l, requestLocals as n, SERVED_BODY_ATTRIBUTES_PROPERTY as o, SCROLL_ATTRIBUTE as p, takeMiddlewareHeaders as r, SERVED_HTML_ATTRIBUTES_PROPERTY as s, rememberRequestLocals as t, LINK_ATTRIBUTE as u };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as createServerEntry, n as routerLinkBridgeModules } from "../../create-server-entry-
|
|
1
|
+
export { t as createServerEntry, n as routerLinkBridgeModules } from "../../create-server-entry-CVhSjR45.js";
|
|
@@ -93,6 +93,7 @@ type ProtocolStatePayload = {
|
|
|
93
93
|
readonly key: unknown;
|
|
94
94
|
readonly error: unknown;
|
|
95
95
|
};
|
|
96
|
+
readonly hostScope?: string;
|
|
96
97
|
}>;
|
|
97
98
|
readonly sharedSeeds?: ReadonlyArray<{
|
|
98
99
|
readonly graphNodeId: string;
|
|
@@ -866,7 +867,7 @@ type ResumeDomElement = ResumeDomNode & {
|
|
|
866
867
|
readonly dispatchEvent?: (event: ResumeSharedPatchEvent) => boolean;
|
|
867
868
|
readonly setAttribute?: (name: string, value: string) => void;
|
|
868
869
|
readonly removeAttribute?: (name: string) => void;
|
|
869
|
-
__marklessDelegatedDispatch?: boolean;
|
|
870
|
+
__marklessDelegatedDispatch?: boolean | string;
|
|
870
871
|
__marklessListen?: (eventType: string) => void;
|
|
871
872
|
__marklessDispatch?: (input: {
|
|
872
873
|
readonly root: ResumeDomElement;
|
|
@@ -4,7 +4,7 @@ import { i as protocolStateVersion, n as protocolInstanceQualifies, t as protoco
|
|
|
4
4
|
/** The field names each templated record list may spell, in the order its templates index them. */
|
|
5
5
|
const PROTOCOL_RECORD_KEYS = {
|
|
6
6
|
"markless/state": {
|
|
7
|
-
computed: "graphNodeId name async deriveSymbolId value version root $type $ref source flags records id type fields items entries values bytes arrayType buffer byteOffset length byteLength directValue dependencies path snapshot status key error",
|
|
7
|
+
computed: "graphNodeId name async deriveSymbolId value version root $type $ref source flags records id type fields items entries values bytes arrayType buffer byteOffset length byteLength directValue dependencies path snapshot status key error hostScope",
|
|
8
8
|
sharedSeeds: "graphNodeId deriveSymbolId dependencies path reads",
|
|
9
9
|
sharedDefinitions: "id name exportedName scope version graphNodeIds projectionIds dependencies definitionId definitionName returnProperties kind graphNodeId path"
|
|
10
10
|
},
|
package/dist/vite.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { d as
|
|
1
|
+
import { d as PREFETCH_ATTRIBUTE, f as REPLACE_ATTRIBUTE, p as SCROLL_ATTRIBUTE, u as LINK_ATTRIBUTE } from "./request-locals-BPfJaoM_.js";
|
|
2
2
|
import { i as matchRouteManifest, l as transformRequestFileSource, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId, r as isPageModuleFile, t as assertNoEndpointConflicts } from "./route-manifest-DmoxXanW.js";
|
|
3
3
|
import { t as __marklessRouteHref } from "./route-href-CGooO_g6.js";
|
|
4
4
|
import { r as protocolIslandSegment } from "./protocol-constants-B4WxrZAS.js";
|
|
5
|
-
import { a as runFragmentNavigation, c as listenForLinkIntent, i as FRAGMENT_PATH_PREFIX, n as routerLinkBridgeModules, o as startFragmentNavigation, s as documentHandlersMessage } from "./create-server-entry-
|
|
5
|
+
import { a as runFragmentNavigation, c as listenForLinkIntent, i as FRAGMENT_PATH_PREFIX, n as routerLinkBridgeModules, o as startFragmentNavigation, s as documentHandlersMessage } from "./create-server-entry-CVhSjR45.js";
|
|
6
6
|
import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
|
|
7
7
|
import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutBase, withoutLeadingSlash } from "ufo";
|
|
8
8
|
import { existsSync, readdirSync } from "node:fs";
|
|
@@ -1527,6 +1527,30 @@ function fragmentRequestModule() {
|
|
|
1527
1527
|
};
|
|
1528
1528
|
}
|
|
1529
1529
|
//#endregion
|
|
1530
|
+
//#region src/vite/nitro-dev-close.ts
|
|
1531
|
+
function devNitroState() {
|
|
1532
|
+
return { nitro: void 0 };
|
|
1533
|
+
}
|
|
1534
|
+
function devNitroModule(state) {
|
|
1535
|
+
return {
|
|
1536
|
+
name: "markless-router:dev-nitro",
|
|
1537
|
+
setup(nitro) {
|
|
1538
|
+
state.nitro = nitro;
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function devNitroClosePlugin(state) {
|
|
1543
|
+
return {
|
|
1544
|
+
name: "markless-router:dev-nitro-close",
|
|
1545
|
+
apply: "serve",
|
|
1546
|
+
async closeBundle() {
|
|
1547
|
+
const nitro = state.nitro;
|
|
1548
|
+
state.nitro = void 0;
|
|
1549
|
+
await nitro?.close();
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
//#endregion
|
|
1530
1554
|
//#region src/vite/nitro-prerender.ts
|
|
1531
1555
|
function serverBuildState() {
|
|
1532
1556
|
return { entry: void 0 };
|
|
@@ -1621,8 +1645,10 @@ function router(options = {}) {
|
|
|
1621
1645
|
const navigationEntry = resumeEntryState();
|
|
1622
1646
|
const fragmentEntry = resumeEntryState();
|
|
1623
1647
|
const routePreloads = routePreloadState();
|
|
1648
|
+
const serverBuild = serverBuildState();
|
|
1649
|
+
const devNitro = devNitroState();
|
|
1624
1650
|
return [
|
|
1625
|
-
routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry,
|
|
1651
|
+
routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuild, devNitro),
|
|
1626
1652
|
devSourceModuleRequestPlugin(),
|
|
1627
1653
|
devPageForUrlPlugin(options),
|
|
1628
1654
|
pageServerOnlyPropsPlugin(),
|
|
@@ -1633,6 +1659,7 @@ function router(options = {}) {
|
|
|
1633
1659
|
htmlTransformPlugin(),
|
|
1634
1660
|
virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, options, fragmentEntry),
|
|
1635
1661
|
linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options),
|
|
1662
|
+
devNitroClosePlugin(devNitro),
|
|
1636
1663
|
nitroPlugins
|
|
1637
1664
|
];
|
|
1638
1665
|
}
|
|
@@ -1697,7 +1724,7 @@ function devSourceModuleRequestPlugin() {
|
|
|
1697
1724
|
}
|
|
1698
1725
|
};
|
|
1699
1726
|
}
|
|
1700
|
-
function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState()) {
|
|
1727
|
+
function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState(), devNitro = devNitroState()) {
|
|
1701
1728
|
let clientOutDir = "";
|
|
1702
1729
|
let clientEnvironmentName = "";
|
|
1703
1730
|
let clientEnvironmentConfig;
|
|
@@ -1731,7 +1758,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
|
|
|
1731
1758
|
ssrEnvironment.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, config.root);
|
|
1732
1759
|
const serverWatch = withWatchIgnores(config.server?.watch);
|
|
1733
1760
|
return {
|
|
1734
|
-
nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild),
|
|
1761
|
+
nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild, devNitro),
|
|
1735
1762
|
server: {
|
|
1736
1763
|
...config.server,
|
|
1737
1764
|
watch: serverWatch
|
|
@@ -1927,7 +1954,7 @@ function requestFileBuildTransformPlugin(root) {
|
|
|
1927
1954
|
}
|
|
1928
1955
|
};
|
|
1929
1956
|
}
|
|
1930
|
-
function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState()) {
|
|
1957
|
+
function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState(), devNitro = devNitroState()) {
|
|
1931
1958
|
const scanDirs = Array.isArray(nitroConfig?.scanDirs) ? nitroConfig.scanDirs.filter((dir) => typeof dir === "string") : [];
|
|
1932
1959
|
const watchOptions = withWatchIgnores(nitroConfig?.watchOptions, serverWatchIgnored);
|
|
1933
1960
|
const publicAssets = Array.isArray(nitroConfig?.publicAssets) ? nitroConfig.publicAssets : [];
|
|
@@ -1950,7 +1977,7 @@ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isB
|
|
|
1950
1977
|
modules: [
|
|
1951
1978
|
...nitroConfig?.modules ?? [],
|
|
1952
1979
|
fragmentRequestModule(),
|
|
1953
|
-
...isBuild ? [prerenderRendererModule(serverBuild)] : []
|
|
1980
|
+
...isBuild ? [prerenderRendererModule(serverBuild)] : [devNitroModule(devNitro)]
|
|
1954
1981
|
],
|
|
1955
1982
|
...isBuild && { routeRules: {
|
|
1956
1983
|
[`/${MARKLESS_BUILD_PREFIX}**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markless/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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,12 +64,12 @@
|
|
|
64
64
|
"pathe": "^2.0.3",
|
|
65
65
|
"satteri": "0.6.3",
|
|
66
66
|
"ufo": "^1.6.4",
|
|
67
|
-
"@markless/
|
|
68
|
-
"@markless/
|
|
69
|
-
"@markless/
|
|
67
|
+
"@markless/compiler": "0.9.0",
|
|
68
|
+
"@markless/bundler": "0.9.0",
|
|
69
|
+
"@markless/web": "0.9.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@markless/analyzer": "0.
|
|
72
|
+
"@markless/analyzer": "0.9.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"typescript": "^5.9.3",
|
|
@@ -10,6 +10,7 @@ export interface FragmentNavigationConfig {
|
|
|
10
10
|
readonly scrollAttribute: string;
|
|
11
11
|
readonly routeScriptType: string;
|
|
12
12
|
readonly servedHtmlAttributesProperty: string;
|
|
13
|
+
readonly servedBodyAttributesProperty: string;
|
|
13
14
|
readonly retireProperty: string;
|
|
14
15
|
readonly streamExecutorGlobal: string;
|
|
15
16
|
readonly streamedArmSelector: string;
|
|
@@ -407,21 +408,126 @@ async function mergeHead(d: Document, next: Document) {
|
|
|
407
408
|
return { stale, scripts };
|
|
408
409
|
}
|
|
409
410
|
|
|
410
|
-
//
|
|
411
|
-
|
|
411
|
+
// A swap applies only what the server changed between its documents; a script's own changes (a theme class) stay.
|
|
412
|
+
type ServedAttributes = ReadonlyMap<string, string>;
|
|
413
|
+
const servedAttributes = {
|
|
414
|
+
html: new WeakMap<Document, ServedAttributes>(),
|
|
415
|
+
body: new WeakMap<Document, ServedAttributes>(),
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const attributePairs = (element: Element): ServedAttributes =>
|
|
419
|
+
new Map(Array.from(element.attributes, (attribute) => [attribute.name, attribute.value]));
|
|
420
|
+
|
|
421
|
+
// What the served document gave <html> or <body>: the last swap's incoming, else the landing's own record.
|
|
422
|
+
function servedBefore(
|
|
423
|
+
d: Document,
|
|
424
|
+
which: 'html' | 'body',
|
|
425
|
+
servedProperty: string,
|
|
426
|
+
): ServedAttributes {
|
|
427
|
+
return (
|
|
428
|
+
servedAttributes[which].get(d) ??
|
|
429
|
+
new Map(
|
|
430
|
+
(
|
|
431
|
+
d.documentElement as unknown as Record<
|
|
432
|
+
string,
|
|
433
|
+
readonly [string, string][] | undefined
|
|
434
|
+
>
|
|
435
|
+
)[servedProperty],
|
|
436
|
+
)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function applyServedChanges(
|
|
441
|
+
d: Document,
|
|
442
|
+
target: HTMLElement,
|
|
443
|
+
served: ServedAttributes,
|
|
444
|
+
incoming: ServedAttributes,
|
|
445
|
+
): void {
|
|
446
|
+
for (const name of new Set([...served.keys(), ...incoming.keys()])) {
|
|
447
|
+
const before = served.get(name) ?? null;
|
|
448
|
+
const after = incoming.get(name) ?? null;
|
|
449
|
+
if (before !== after) applyServedChange(d, target, name, before, after);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
412
452
|
|
|
413
453
|
function syncHtmlAttributes(d: Document, next: Document, servedProperty: string): void {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
454
|
+
const incoming = attributePairs(next.documentElement);
|
|
455
|
+
applyServedChanges(d, d.documentElement, servedBefore(d, 'html', servedProperty), incoming);
|
|
456
|
+
servedAttributes.html.set(d, incoming);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// A shell <body> the swap morphs in place takes only the server's changes to its attributes.
|
|
460
|
+
function syncBodyAttributes(d: Document, next: Document, servedProperty: string): void {
|
|
461
|
+
const incoming = attributePairs(next.body);
|
|
462
|
+
applyServedChanges(d, d.body, servedBefore(d, 'body', servedProperty), incoming);
|
|
463
|
+
servedAttributes.body.set(d, incoming);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// A container <body> is replaced whole: the incoming one takes the outgoing's attributes plus the server's changes, its own container attributes as served.
|
|
467
|
+
function carryBodyAttributes(
|
|
468
|
+
d: Document,
|
|
469
|
+
from: HTMLElement,
|
|
470
|
+
to: HTMLElement,
|
|
471
|
+
own: readonly string[],
|
|
472
|
+
servedProperty: string,
|
|
473
|
+
): void {
|
|
474
|
+
const incoming = attributePairs(to);
|
|
475
|
+
const merged = d.createElement(from.localName);
|
|
476
|
+
for (const attribute of from.attributes) merged.setAttribute(attribute.name, attribute.value);
|
|
477
|
+
applyServedChanges(d, merged, servedBefore(d, 'body', servedProperty), incoming);
|
|
478
|
+
for (const name of to.getAttributeNames())
|
|
479
|
+
if (!own.includes(name) && !merged.hasAttribute(name)) to.removeAttribute(name);
|
|
480
|
+
for (const attribute of merged.attributes)
|
|
481
|
+
if (!own.includes(attribute.name) && to.getAttribute(attribute.name) !== attribute.value)
|
|
482
|
+
to.setAttribute(attribute.name, attribute.value);
|
|
483
|
+
servedAttributes.body.set(d, incoming);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// class and style are merged a token and a declaration at a time, so a script's own tokens are never dropped.
|
|
487
|
+
function applyServedChange(
|
|
488
|
+
d: Document,
|
|
489
|
+
root: HTMLElement,
|
|
490
|
+
name: string,
|
|
491
|
+
before: string | null,
|
|
492
|
+
after: string | null,
|
|
493
|
+
): void {
|
|
494
|
+
if (name === 'class') {
|
|
495
|
+
const was = classTokens(before);
|
|
496
|
+
const will = classTokens(after);
|
|
497
|
+
const removed = [...was].filter((token) => !will.has(token));
|
|
498
|
+
const added = [...will].filter((token) => !was.has(token));
|
|
499
|
+
if (removed.length) root.classList.remove(...removed);
|
|
500
|
+
if (added.length) root.classList.add(...added);
|
|
501
|
+
} else if (name === 'style') {
|
|
502
|
+
const was = declarations(d, before);
|
|
503
|
+
const will = declarations(d, after);
|
|
504
|
+
for (const property of was.keys())
|
|
505
|
+
if (!will.has(property)) root.style.removeProperty(property);
|
|
506
|
+
for (const [property, [value, priority]] of will) {
|
|
507
|
+
const old = was.get(property);
|
|
508
|
+
if (old?.[0] !== value || old[1] !== priority)
|
|
509
|
+
root.style.setProperty(property, value, priority);
|
|
510
|
+
}
|
|
511
|
+
} else if (after === null) root.removeAttribute(name);
|
|
512
|
+
else root.setAttribute(name, after);
|
|
513
|
+
if (after === null && !root.getAttribute(name)?.trim()) root.removeAttribute(name);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const classTokens = (value: string | null): Set<string> =>
|
|
517
|
+
new Set(value?.split(/\s+/).filter(Boolean));
|
|
518
|
+
|
|
519
|
+
function declarations(d: Document, value: string | null): Map<string, [string, string]> {
|
|
520
|
+
const style = d.createElement('div').style;
|
|
521
|
+
style.cssText = value ?? '';
|
|
522
|
+
const result = new Map<string, [string, string]>();
|
|
523
|
+
for (let index = 0; index < style.length; index++) {
|
|
524
|
+
const property = style.item(index);
|
|
525
|
+
result.set(property, [
|
|
526
|
+
style.getPropertyValue(property),
|
|
527
|
+
style.getPropertyPriority(property),
|
|
528
|
+
]);
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
425
531
|
}
|
|
426
532
|
|
|
427
533
|
// Set from the page's config at the start of every swap.
|
|
@@ -621,14 +727,14 @@ async function streamAppends(
|
|
|
621
727
|
return signal.aborted ? undefined : all;
|
|
622
728
|
}
|
|
623
729
|
|
|
624
|
-
// Inserting a big page whole lays it out in one long
|
|
625
|
-
const
|
|
730
|
+
// Inserting a big page whole lays it out in one long task; the swap lays it out this many elements at a time and yields between.
|
|
731
|
+
const CHUNK_ELEMENTS = 800;
|
|
626
732
|
|
|
627
733
|
type HeldPage = [parent: Node, before: Node | null, nodes: ChildNode[]][];
|
|
628
734
|
|
|
629
735
|
function holdPage(parent: Element, after: Node | undefined, before: Node | null): HeldPage {
|
|
630
736
|
const nodes: ChildNode[] = [];
|
|
631
|
-
for (let node = after ? after.nextSibling : parent.firstChild; node && node !== before;
|
|
737
|
+
for (let node = after ? after.nextSibling : parent.firstChild; node && node !== before;) {
|
|
632
738
|
const next: ChildNode | null = node.nextSibling;
|
|
633
739
|
nodes.push(node);
|
|
634
740
|
node.remove();
|
|
@@ -640,32 +746,40 @@ function holdPage(parent: Element, after: Node | undefined, before: Node | null)
|
|
|
640
746
|
const elementsIn = (node: Node) =>
|
|
641
747
|
node.nodeType === 1 ? (node as Element).getElementsByTagName('*').length + 1 : 0;
|
|
642
748
|
|
|
643
|
-
//
|
|
749
|
+
// Runs while the leaving page is held on screen; each chunk is laid out before yielding and an element that fits in one goes in whole.
|
|
644
750
|
function fillPage(
|
|
645
751
|
w: Window,
|
|
646
752
|
held: HeldPage,
|
|
647
753
|
budget: number,
|
|
648
754
|
current: () => boolean,
|
|
649
755
|
): true | Promise<boolean> {
|
|
756
|
+
const laidOut = () => {
|
|
757
|
+
void w.document.documentElement.offsetHeight;
|
|
758
|
+
return new Promise<boolean>((resolve) => {
|
|
759
|
+
const channel = new MessageChannel();
|
|
760
|
+
channel.port1.onmessage = () => resolve(current());
|
|
761
|
+
channel.port2.postMessage(0);
|
|
762
|
+
});
|
|
763
|
+
};
|
|
764
|
+
const nextChunk = () => laidOut().then((still) => still && fillPage(w, held, budget, current));
|
|
650
765
|
let used = 0;
|
|
651
766
|
while (held.length) {
|
|
652
767
|
const [parent, before, nodes] = held[0]!;
|
|
768
|
+
const size = elementsIn(nodes[0]!);
|
|
769
|
+
if (used > 0 && used + size > budget && size <= budget) return nextChunk();
|
|
653
770
|
const node = nodes.shift()!;
|
|
654
771
|
if (!nodes.length) held.shift();
|
|
655
|
-
|
|
656
|
-
if (used + size > budget && node.firstChild) {
|
|
772
|
+
if (size > budget && node.firstChild) {
|
|
657
773
|
const children = [...node.childNodes];
|
|
658
774
|
(node as Element).replaceChildren();
|
|
659
775
|
held.unshift([node, null, children]);
|
|
660
776
|
used += 1;
|
|
661
777
|
} else used += size;
|
|
662
778
|
parent.insertBefore(node, before);
|
|
663
|
-
if (used >= budget && held.length)
|
|
664
|
-
return new Promise<void>((resolve) =>
|
|
665
|
-
w.requestAnimationFrame(() => w.setTimeout(resolve, 0)),
|
|
666
|
-
).then(() => current() && fillPage(w, held, budget, current));
|
|
779
|
+
if (used >= budget && held.length) return nextChunk();
|
|
667
780
|
}
|
|
668
|
-
|
|
781
|
+
// The last chunk is laid out in its own task too, so what runs once the page is in starts on a laid-out page.
|
|
782
|
+
return budget === Infinity ? true : laidOut();
|
|
669
783
|
}
|
|
670
784
|
|
|
671
785
|
export async function navigate(
|
|
@@ -739,89 +853,128 @@ export async function navigate(
|
|
|
739
853
|
if (mode === 'push') w.history.pushState(entry, '', landed.href);
|
|
740
854
|
else w.history.replaceState(entry, '', landed.href);
|
|
741
855
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
region.appendChild(d.createComment(state.config.pageEndComment));
|
|
779
|
-
// The document's own nodes after the page stay; a document app morphs them to its next shell.
|
|
780
|
-
const focused = d.activeElement as HTMLElement | null;
|
|
781
|
-
const kept = nodesAfter(d, leavingEnd);
|
|
782
|
-
const arriving = nodesAfter(d, end);
|
|
856
|
+
// A scroll target or a restored position needs the whole page in place before it is applied; a page that fits in a chunk goes in at once.
|
|
857
|
+
const progressive =
|
|
858
|
+
restore === 'reset' &&
|
|
859
|
+
!url.hash &&
|
|
860
|
+
d.visibilityState !== 'hidden' &&
|
|
861
|
+
typeof d.startViewTransition === 'function' &&
|
|
862
|
+
incoming.getElementsByTagName('*').length > CHUNK_ELEMENTS;
|
|
863
|
+
let region!: Element;
|
|
864
|
+
let end!: Comment;
|
|
865
|
+
let scripts: Element[] = [];
|
|
866
|
+
const commit = async (): Promise<boolean> => {
|
|
867
|
+
if (id !== session.navigation) return false;
|
|
868
|
+
const offsets: ScrolledBox[] = [];
|
|
869
|
+
for (const element of state.scrolled) {
|
|
870
|
+
if (!element.isConnected || !scope.contains(element)) continue;
|
|
871
|
+
if (element.scrollTop || element.scrollLeft)
|
|
872
|
+
offsets.push({
|
|
873
|
+
path: elementPath(scope, element),
|
|
874
|
+
id: element.id,
|
|
875
|
+
kind: boxKind(element),
|
|
876
|
+
top: element.scrollTop,
|
|
877
|
+
left: element.scrollLeft,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
state.scrolled.clear();
|
|
881
|
+
retire(state, outgoing);
|
|
882
|
+
(w as unknown as Record<string, unknown>)[state.config.streamExecutorGlobal] = undefined;
|
|
883
|
+
for (const element of d.querySelectorAll(state.config.streamedArmSelector))
|
|
884
|
+
element.remove();
|
|
885
|
+
state.shown = pageKey(new URL(w.location.href));
|
|
886
|
+
if (documentLoads) syncHtmlAttributes(d, next, state.config.servedHtmlAttributesProperty);
|
|
887
|
+
if (documentLoads && !isContainer(d.body))
|
|
888
|
+
syncBodyAttributes(d, next, state.config.servedBodyAttributesProperty);
|
|
889
|
+
let start: Comment | undefined;
|
|
890
|
+
let held: HeldPage | undefined;
|
|
891
|
+
if (documentLoads && !isContainer(d.body)) {
|
|
783
892
|
const fresh: Element[] = [];
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
893
|
+
region = d.body;
|
|
894
|
+
morphChildren(d, region, incoming, fresh);
|
|
895
|
+
scripts = fresh.flatMap(scriptsIn);
|
|
896
|
+
region.append((end = d.createComment('')));
|
|
897
|
+
} else {
|
|
898
|
+
region = d.adoptNode(incoming);
|
|
899
|
+
if (outgoing === d.body)
|
|
900
|
+
carryBodyAttributes(
|
|
901
|
+
d,
|
|
902
|
+
outgoing as HTMLElement,
|
|
903
|
+
region as HTMLElement,
|
|
904
|
+
state.config.containerAttributes,
|
|
905
|
+
state.config.servedBodyAttributesProperty,
|
|
791
906
|
);
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
907
|
+
const leavingEnd = pageEnd(outgoing, state.config.pageEndComment);
|
|
908
|
+
if (leavingEnd) {
|
|
909
|
+
if (outgoing !== d.body)
|
|
910
|
+
carryHostAttributes(d, outgoing, region, state.config.containerAttributes);
|
|
911
|
+
end =
|
|
912
|
+
pageEnd(region, state.config.pageEndComment) ??
|
|
913
|
+
region.appendChild(d.createComment(state.config.pageEndComment));
|
|
914
|
+
// The document's own nodes after the page stay; a document app morphs them to its next shell.
|
|
915
|
+
const focused = d.activeElement as HTMLElement | null;
|
|
916
|
+
const kept = nodesAfter(d, leavingEnd);
|
|
917
|
+
const arriving = nodesAfter(d, end);
|
|
918
|
+
const fresh: Element[] = [];
|
|
919
|
+
if (documentLoads) morphChildren(d, kept, arriving, fresh);
|
|
920
|
+
const leavingStart = pageStart(outgoing, state.config.pageStartComment);
|
|
921
|
+
start = pageStart(region, state.config.pageStartComment);
|
|
922
|
+
if (leavingStart || start) {
|
|
923
|
+
start ??= region.insertBefore(
|
|
924
|
+
d.createComment(state.config.pageStartComment),
|
|
925
|
+
region.firstChild,
|
|
926
|
+
);
|
|
927
|
+
const keptBefore = leavingStart
|
|
928
|
+
? nodesBefore(d, leavingStart)
|
|
929
|
+
: d.createDocumentFragment();
|
|
930
|
+
const arrivingBefore = nodesBefore(d, start);
|
|
931
|
+
if (documentLoads) morphChildren(d, keptBefore, arrivingBefore, fresh);
|
|
932
|
+
region.insertBefore(keptBefore, start);
|
|
933
|
+
}
|
|
934
|
+
scripts = [...scriptsIn(region), ...fresh.flatMap(scriptsIn)];
|
|
935
|
+
region.append(kept);
|
|
936
|
+
held = holdPage(region, start, end);
|
|
937
|
+
outgoing.replaceWith(region);
|
|
938
|
+
// The document's own nodes moved to the new element; focus inside them stays put.
|
|
939
|
+
if (focused && focused !== d.activeElement && region.contains(focused))
|
|
940
|
+
focused.focus({ preventScroll: true });
|
|
941
|
+
} else {
|
|
942
|
+
scripts = scriptsIn(region);
|
|
943
|
+
held = holdPage(region, undefined, null);
|
|
944
|
+
outgoing.replaceWith(region);
|
|
945
|
+
region.after((end = d.createComment('')));
|
|
798
946
|
}
|
|
799
|
-
scripts = [...scriptsIn(region), ...fresh.flatMap(scriptsIn)];
|
|
800
|
-
region.append(kept);
|
|
801
|
-
held = holdPage(region, start, end);
|
|
802
|
-
outgoing.replaceWith(region);
|
|
803
|
-
// The document's own nodes moved to the new element; focus inside them stays put.
|
|
804
|
-
if (focused && focused !== d.activeElement && region.contains(focused))
|
|
805
|
-
focused.focus({ preventScroll: true });
|
|
806
|
-
} else {
|
|
807
|
-
scripts = scriptsIn(region);
|
|
808
|
-
held = holdPage(region, undefined, null);
|
|
809
|
-
outgoing.replaceWith(region);
|
|
810
|
-
region.after((end = d.createComment('')));
|
|
811
947
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
948
|
+
const filled =
|
|
949
|
+
held &&
|
|
950
|
+
fillPage(
|
|
951
|
+
w,
|
|
952
|
+
held,
|
|
953
|
+
progressive ? CHUNK_ELEMENTS : Infinity,
|
|
954
|
+
() => id === session.navigation,
|
|
955
|
+
);
|
|
956
|
+
if (filled instanceof Promise && !(await filled)) return false;
|
|
957
|
+
if (progressive) w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
|
958
|
+
// 'instant': a `scroll-behavior: smooth` page would otherwise animate every landing.
|
|
959
|
+
for (const box of offsets)
|
|
960
|
+
sameBox(scope, box)?.scrollTo({ top: box.top, left: box.left, behavior: 'instant' });
|
|
961
|
+
for (const element of head.stale) element.remove();
|
|
962
|
+
if (next.title && d.title !== next.title) d.title = next.title;
|
|
963
|
+
return true;
|
|
964
|
+
};
|
|
965
|
+
let committed: boolean;
|
|
966
|
+
if (progressive) {
|
|
967
|
+
// The leaving page stays on screen while the next one is laid out a chunk at a time; it then paints in one frame, with no animation.
|
|
968
|
+
let result = false;
|
|
969
|
+
const transition = d.startViewTransition(async () => {
|
|
970
|
+
result = await commit();
|
|
971
|
+
});
|
|
972
|
+
transition.ready.catch(() => {});
|
|
973
|
+
transition.finished.catch(() => {});
|
|
974
|
+
await transition.updateCallbackDone.then(() => transition.skipTransition(), reportAsync);
|
|
975
|
+
committed = result;
|
|
976
|
+
} else committed = await commit();
|
|
977
|
+
if (!committed) return;
|
|
825
978
|
const ran = runScripts(
|
|
826
979
|
d,
|
|
827
980
|
[...head.scripts, ...(scripts as HTMLScriptElement[])],
|
|
@@ -834,7 +987,7 @@ export async function navigate(
|
|
|
834
987
|
if (restore === 'reset') {
|
|
835
988
|
const target = url.hash ? d.getElementById(decodeURIComponent(url.hash.slice(1))) : null;
|
|
836
989
|
if (target) target.scrollIntoView({ behavior: 'instant' });
|
|
837
|
-
else if (!
|
|
990
|
+
else if (!progressive) w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
|
838
991
|
} else if (restore !== 'manual')
|
|
839
992
|
w.scrollTo({ top: restore.y, left: restore.x, behavior: 'instant' });
|
|
840
993
|
d.dispatchEvent(new CustomEvent('marklessrouterrendered'));
|