@markless/router 0.7.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-yboa-2m6.js → create-server-entry-CVhSjR45.js} +52 -8
- package/dist/debug-channel-dB6CJyHa.js +1 -0
- package/dist/index.js +4 -4
- package/dist/{request-locals-BDAhNpEa.js → request-locals-BPfJaoM_.js} +3 -2
- package/dist/{route-manifest-DMizh42B.js → route-manifest-DmoxXanW.js} +2 -2
- package/dist/vite/route-href.js +1 -2
- package/dist/vite/runtime/create-route-discovery.js +1 -1
- package/dist/vite/runtime/create-server-entry.js +1 -2
- package/dist/vite/runtime/mdx-route.d.ts +4 -2
- package/dist/vite/runtime/mdx-route.js +1 -1
- package/dist/vite.js +100 -21
- package/package.json +8 -7
- package/src/vite/entries/fragment-entry.ts +300 -86
- package/dist/debug-channel-CewnC9fH.js +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as ROUTE_SCRIPT_TYPE,
|
|
2
|
-
import { i as matchRouteManifest, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId } from "./route-manifest-
|
|
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
|
+
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";
|
|
5
5
|
import { CONTAINER_RETIRE_PROPERTY, renderToString, splitSsrContainer } from "@markless/web/render-to-string";
|
|
@@ -49,6 +49,11 @@ function appendModulePreloads(document, items) {
|
|
|
49
49
|
return appended;
|
|
50
50
|
}
|
|
51
51
|
//#endregion
|
|
52
|
+
//#region src/vite/runtime/document-handlers.ts
|
|
53
|
+
function documentHandlersMessage(what) {
|
|
54
|
+
return `MARKLESS_ROUTER_DOCUMENT_HANDLERS_UNSUPPORTED: ${what}, but the router serves only the document's HTML and never wires it in the browser, so the handler would never run. Put the handler on an element a page renders, for example in a layout component each page wraps its content in.`;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
52
57
|
//#region src/vite/fragment-navigation.ts
|
|
53
58
|
function startFragmentNavigation(d, linkAttributeOf, routes, importModule, loadRun, intentOf) {
|
|
54
59
|
const w = d.defaultView;
|
|
@@ -192,7 +197,14 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
|
|
|
192
197
|
if (endpoint && !under) return false;
|
|
193
198
|
const rest = page.indexOf("**");
|
|
194
199
|
if (rest === -1 ? page.length !== parts.length : parts.length <= rest) return false;
|
|
195
|
-
return page.every((segment, index) =>
|
|
200
|
+
return page.every((segment, index) => {
|
|
201
|
+
if (segment === "**" || segment === ":") return true;
|
|
202
|
+
try {
|
|
203
|
+
return segment === decodeURIComponent(parts[index]);
|
|
204
|
+
} catch {
|
|
205
|
+
return segment === parts[index];
|
|
206
|
+
}
|
|
207
|
+
});
|
|
196
208
|
});
|
|
197
209
|
};
|
|
198
210
|
const eligible = (anchor, forClick = false) => {
|
|
@@ -240,13 +252,27 @@ function runFragmentNavigation(d, config, routes, importModule, scrolled, arrivi
|
|
|
240
252
|
d.head.appendChild(link);
|
|
241
253
|
}
|
|
242
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
|
+
};
|
|
243
269
|
const parse = (url, text) => {
|
|
244
270
|
const at = text.indexOf(config.regionEndMarker);
|
|
245
271
|
const prefix = at === -1 ? text : text.slice(0, at);
|
|
246
272
|
return {
|
|
247
273
|
url,
|
|
248
274
|
prefix,
|
|
249
|
-
doc:
|
|
275
|
+
doc: parseAsLoaded(prefix),
|
|
250
276
|
tail: at === -1 ? "" : text.slice(at + config.regionEndMarker.length)
|
|
251
277
|
};
|
|
252
278
|
};
|
|
@@ -836,7 +862,7 @@ function createServerEntry(options) {
|
|
|
836
862
|
const renderSsr = baseArtifact?.renderSsr ?? pageModule.marklessRenderSsr;
|
|
837
863
|
if (!renderSsr) {
|
|
838
864
|
const shell = await renderDocumentShell(documentModule, pageProps, "");
|
|
839
|
-
const message = `Page module must export
|
|
865
|
+
const message = `Page module must export a Markless compiled artifact: ${escapeHtml(file)}`;
|
|
840
866
|
return new Response(fillDocumentChildren(shell, message), http.commit(status, headers));
|
|
841
867
|
}
|
|
842
868
|
const documentLoads = options.documentNavigation === "document";
|
|
@@ -864,7 +890,7 @@ function createServerEntry(options) {
|
|
|
864
890
|
resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
|
|
865
891
|
modulePreloads: (html, resumes) => landingModulePreloads(resumes ? options.routeSsrModulePreloads?.[file] : void 0, linkBridges?.[html.includes("href=\"#/") ? 1 : 0])
|
|
866
892
|
};
|
|
867
|
-
const headStart = (
|
|
893
|
+
const headStart = (fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (fragment ? "" : options.importMap ?? "");
|
|
868
894
|
if (options.render === "blocking") {
|
|
869
895
|
const pageHtml = splitPageHtml(await renderToString(routedArtifact, renderOptions));
|
|
870
896
|
privateToVisitor();
|
|
@@ -1002,7 +1028,17 @@ function landingModulePreloads(routePreloads, linkBridgePath) {
|
|
|
1002
1028
|
const preloads = [];
|
|
1003
1029
|
addModulePreloads(preloads, /* @__PURE__ */ new Set(), routePreloads);
|
|
1004
1030
|
if (linkBridgePath) preloads.push(linkBridgePath);
|
|
1005
|
-
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
|
+
};
|
|
1006
1042
|
}
|
|
1007
1043
|
function routerLinkHrefs(html) {
|
|
1008
1044
|
return [...html.matchAll(/<a\b(?=[^>]*\bdata-markless-router-link(?:[\s=>]|$))(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
|
|
@@ -1104,8 +1140,15 @@ function wholeDocument(shell, page, fragment, documentLoads) {
|
|
|
1104
1140
|
async function renderDocumentModule(documentModule, props, htmlAttributes) {
|
|
1105
1141
|
const output = await documentModule?.default?.renderSsr?.(props, { [HTML_ATTRIBUTES_SLOT]: htmlAttributes });
|
|
1106
1142
|
assertDocumentStorageFree(output);
|
|
1143
|
+
assertDocumentHandlerFree(output);
|
|
1107
1144
|
return output?.html;
|
|
1108
1145
|
}
|
|
1146
|
+
function assertDocumentHandlerFree(output) {
|
|
1147
|
+
const view = output?.view;
|
|
1148
|
+
const names = [...new Set((view?.events ?? []).map((event) => event.eventName))];
|
|
1149
|
+
if (names.length === 0) return;
|
|
1150
|
+
throw new Error(documentHandlersMessage(`the document renders ${names.join(", ")} handlers`));
|
|
1151
|
+
}
|
|
1109
1152
|
function assertDocumentStorageFree(output) {
|
|
1110
1153
|
const storage = documentStorageRecords(output?.state);
|
|
1111
1154
|
if (storage.length === 0) return;
|
|
@@ -1271,6 +1314,7 @@ function fragmentConfig(fragment, documentLoads) {
|
|
|
1271
1314
|
scrollAttribute: SCROLL_ATTRIBUTE,
|
|
1272
1315
|
routeScriptType: ROUTE_SCRIPT_TYPE,
|
|
1273
1316
|
servedHtmlAttributesProperty: SERVED_HTML_ATTRIBUTES_PROPERTY,
|
|
1317
|
+
servedBodyAttributesProperty: SERVED_BODY_ATTRIBUTES_PROPERTY,
|
|
1274
1318
|
retireProperty: CONTAINER_RETIRE_PROPERTY,
|
|
1275
1319
|
streamExecutorGlobal: STREAM_ARM_EXECUTOR_GLOBAL,
|
|
1276
1320
|
streamedArmSelector: STREAMED_ARM_SELECTOR,
|
|
@@ -1330,4 +1374,4 @@ function insertImportMap(html, importMap) {
|
|
|
1330
1374
|
return `${html.slice(0, at)}${importMap}${html.slice(at)}`;
|
|
1331
1375
|
}
|
|
1332
1376
|
//#endregion
|
|
1333
|
-
export { runFragmentNavigation as a, FRAGMENT_PATH_PREFIX as i, routerLinkBridgeModules as n, startFragmentNavigation as o, LINK_BRIDGE_DIR as r,
|
|
1377
|
+
export { runFragmentNavigation as a, listenForLinkIntent as c, FRAGMENT_PATH_PREFIX as i, routerLinkBridgeModules as n, startFragmentNavigation as o, LINK_BRIDGE_DIR as r, documentHandlersMessage as s, createServerEntry as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as __marklessDebugActivateContainer, n as __marklessDebugBootstrapSource, r as __marklessDebugChannelForTest, i as __marklessDebugDisposeContainer, a as __marklessDebugInvalidateElement, o as __marklessDebugRecordInteraction, s as __marklessDebugRecordViolation, c as __marklessDebugRegisterDelegatedEvents, l as __marklessDebugRegisterRouter, u as __marklessDebugResetForTest, d as __marklessDebugSetBoundaries, f as __marklessDebugStartContainer } from "./debug-channel-Di6SppZv.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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
|
+
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";
|
|
5
5
|
import { MARKLESS_PENDING_MIN_VISIBLE_MS as MARKLESS_NAV_PENDING_MIN_MS, settleOrPendingDeadline } from "@markless/web/fns/pending-timing";
|
|
@@ -152,8 +152,8 @@ async function __marklessRouterStartSpaNavigation(options) {
|
|
|
152
152
|
navigation.addEventListener("navigate", (event) => {
|
|
153
153
|
handleNavigateEvent(event, context);
|
|
154
154
|
});
|
|
155
|
-
if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-
|
|
156
|
-
if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-
|
|
155
|
+
if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-dB6CJyHa.js").then((debug) => debug.__marklessDebugRegisterRouter(void 0, "spa-click-listener")).catch(() => {});
|
|
156
|
+
if (typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__) await import("./debug-channel-dB6CJyHa.js").then((debug) => debug.__marklessDebugRegisterRouter(void 0, "navigation-event")).catch(() => {});
|
|
157
157
|
{
|
|
158
158
|
const bootUrl = parseSameOriginUrl(runtimeWindow.location.href, runtimeWindow.location.href);
|
|
159
159
|
const bootHashPath = bootUrl ? hashRoutePath(bootUrl) : void 0;
|
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
import { basename, extname, join, normalize, relative } from "pathe";
|
|
2
|
-
import { joinURL, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
|
|
2
|
+
import { decode, joinURL, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
|
|
3
3
|
const MIDDLEWARE_DIR = "middleware";
|
|
4
4
|
const HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
5
5
|
"connect",
|
|
@@ -432,7 +432,7 @@ function matchRoutePathname(route, requestPathname) {
|
|
|
432
432
|
params[routeSegment.slice(1)] = requestSegment;
|
|
433
433
|
continue;
|
|
434
434
|
}
|
|
435
|
-
if (routeSegment !== requestSegment) return;
|
|
435
|
+
if (routeSegment !== decode(requestSegment)) return;
|
|
436
436
|
}
|
|
437
437
|
return params;
|
|
438
438
|
}
|
package/dist/vite/route-href.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { __marklessRouteHref };
|
|
1
|
+
export { t as __marklessRouteHref } from "../route-href-CGooO_g6.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as normalizeRouteFileId } from "../../route-manifest-
|
|
1
|
+
import { o as normalizeRouteFileId } from "../../route-manifest-DmoxXanW.js";
|
|
2
2
|
//#region src/vite/runtime/create-route-discovery.ts
|
|
3
3
|
function createRouteDiscovery(discoveredPageModuleLoaders) {
|
|
4
4
|
const pageModuleLoaders = Object.fromEntries(Object.entries(discoveredPageModuleLoaders).map(([file, loader]) => [normalizeRouteFileId(file), loader]));
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createServerEntry, routerLinkBridgeModules };
|
|
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;
|
|
@@ -1186,4 +1187,5 @@ export declare function renderMdxChild(children: MdxChild[], component: MdxCompo
|
|
|
1186
1187
|
export declare function composeMdxState(children: readonly MdxChild[]): MdxStatePayload | undefined;
|
|
1187
1188
|
export declare function composeMdxView(parts: readonly MdxRoutePart[], children: readonly MdxChild[], initialElementOffset: number, initialCommentOffset?: number): MdxViewPayload | undefined;
|
|
1188
1189
|
export declare function loadMdxSymbol(symbolId: string, children: readonly MdxChild[], loaders: readonly MdxSymbolLoader[]): unknown;
|
|
1189
|
-
//#endregion
|
|
1190
|
+
//#endregion
|
|
1191
|
+
export {};
|
|
@@ -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
|
|
2
|
-
import { i as matchRouteManifest, l as transformRequestFileSource, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId, r as isPageModuleFile, t as assertNoEndpointConflicts } from "./route-manifest-
|
|
1
|
+
import { d as PREFETCH_ATTRIBUTE, f as REPLACE_ATTRIBUTE, p as SCROLL_ATTRIBUTE, u as LINK_ATTRIBUTE } from "./request-locals-BPfJaoM_.js";
|
|
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, i as FRAGMENT_PATH_PREFIX, n as routerLinkBridgeModules, o as startFragmentNavigation, s as
|
|
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";
|
|
@@ -11,10 +11,20 @@ import { nitro } from "nitro/vite";
|
|
|
11
11
|
import { minifySync, sortUserPlugins } from "vite";
|
|
12
12
|
import { isClientPrimarySourceRequest, isExecutionLogChunk, isMarklessNavigationPack, isRenderDataSourceRequest, isResumeSourceRequest, isRouteNavigationSourceRequest, isSymbolOnlySourceRequest, lazyStartupPackName, marklessDeferredChunkFileNames, optimizedDepsToInclude, plannedLandingFiles, resolverVirtualModuleSourceFile, startupPackOfLazy, symbolVirtualModuleSourceFile, warmEvaluatedImports } from "@markless/bundler/preload";
|
|
13
13
|
import { MARKLESS_BUILD_PREFIX, MARKLESS_CHUNK_SPECIFIER_PREFIX, MARKLESS_IMPORT_MAP_ASSET, MARKLESS_PUBLIC_BUILD_METADATA_FILES, importMapScript, marklessBuildManifestDir } from "@markless/bundler/rolldown";
|
|
14
|
-
import {
|
|
14
|
+
import { parse } from "@tsrx/yuku";
|
|
15
15
|
import { toHtml } from "hast-util-to-html";
|
|
16
16
|
import { HastReader, createMdxHastHandle, defineHastPlugin, dropHandle, materializeHastTree, resolveHastSubscriptions, serializeHandle, visitHastHandle } from "satteri";
|
|
17
17
|
import "@markless/web/render-to-string";
|
|
18
|
+
import { isEventAttribute } from "@markless/compiler";
|
|
19
|
+
//#region src/vite/tsrx-ast.ts
|
|
20
|
+
/** A `.tsrx` module's tree; throws a `SyntaxError` naming the first error, as a module that does not parse has no tree to read. */
|
|
21
|
+
function parseTsrxModule(source) {
|
|
22
|
+
const { program, diagnostics } = parse(source, { preserveParens: true });
|
|
23
|
+
const error = diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
24
|
+
if (error) throw new SyntaxError(`${error.message} (${error.start}:${error.end})`);
|
|
25
|
+
return program;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
18
28
|
//#region src/route-types.ts
|
|
19
29
|
const routeTypesEnvPath = "markless-router-env.d.ts";
|
|
20
30
|
const routeTypesOutputPath = ".output/markless/router/types/routes.d.ts";
|
|
@@ -300,7 +310,7 @@ function lowerRouterLinks(code, ast) {
|
|
|
300
310
|
return applyEdits(code, edits);
|
|
301
311
|
}
|
|
302
312
|
function parseTransformAst(code, id, parse) {
|
|
303
|
-
if (id.includes(".tsrx")) return
|
|
313
|
+
if (id.includes(".tsrx")) return parseTsrxModule(code);
|
|
304
314
|
return parse(code, {
|
|
305
315
|
astType: "ts",
|
|
306
316
|
lang: id.includes(".jsx") ? "jsx" : "tsx",
|
|
@@ -825,6 +835,8 @@ function scalarPlanSourceReference(source) {
|
|
|
825
835
|
[MARKLESS_SCALAR_PLAN_SOURCE_QUERY]: null
|
|
826
836
|
});
|
|
827
837
|
}
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region ../bundler/src/source-module.ts
|
|
828
840
|
/**
|
|
829
841
|
* The same one line for the live roster, gated on the same fact the reconcile
|
|
830
842
|
* plane is gated on: a payload with no computed nodes can hold no roster
|
|
@@ -844,6 +856,7 @@ function emitQueuedResumeContainerEvent(source, { censusRepair = true } = {}) {
|
|
|
844
856
|
implementation,
|
|
845
857
|
"export function resumeContainerEvent(input) {",
|
|
846
858
|
" const root = input.root;",
|
|
859
|
+
" if (input.eventIndex >= 0) input = { ...input, eventRecord: (root.__mE ||= JSON.parse(root.querySelector('script[type=\"markless/view\"]').textContent).events)[input.eventIndex] };",
|
|
847
860
|
censusRepair ? " if (root.__marklessShifted) return import('@markless/web/fns/served-census-repair').then((census) => (census.marklessRepairCensus(root), resumeContainerEvent(input)));" : "",
|
|
848
861
|
" if (!root.__marklessDispatch) {",
|
|
849
862
|
" let handler = marklessResumeContainerEvent, tail = Promise.resolve(), done = tail, lastEvent = null, lastEventTime = undefined, seen = new WeakMap();",
|
|
@@ -1389,11 +1402,11 @@ const NAVIGATION_POLYFILL_MODULE = "@virtualstate/navigation";
|
|
|
1389
1402
|
//#endregion
|
|
1390
1403
|
//#region src/vite/document-navigation.ts
|
|
1391
1404
|
const ROUTE_INVARIANT_DOCUMENT_PROPS = /* @__PURE__ */ new Set(["children"]);
|
|
1392
|
-
function documentNavigationFor(source
|
|
1405
|
+
function documentNavigationFor(source) {
|
|
1393
1406
|
if (source === void 0) return "client";
|
|
1394
1407
|
let program;
|
|
1395
1408
|
try {
|
|
1396
|
-
program =
|
|
1409
|
+
program = parseTsrxModule(source);
|
|
1397
1410
|
} catch {
|
|
1398
1411
|
return "document";
|
|
1399
1412
|
}
|
|
@@ -1403,6 +1416,43 @@ function documentNavigationFor(source, id) {
|
|
|
1403
1416
|
if (!component) return "document";
|
|
1404
1417
|
return readsOnlyRouteInvariantProps(nodes(component.params)) ? "client" : "document";
|
|
1405
1418
|
}
|
|
1419
|
+
function documentHandlerError(source, id) {
|
|
1420
|
+
if (source === void 0) return void 0;
|
|
1421
|
+
let program;
|
|
1422
|
+
try {
|
|
1423
|
+
program = parseTsrxModule(source);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const found = firstHandlerAttribute(program);
|
|
1428
|
+
if (!found) return void 0;
|
|
1429
|
+
return documentHandlersMessage(`${id} sets ${found.attribute} on <${found.tag}>`);
|
|
1430
|
+
}
|
|
1431
|
+
function firstHandlerAttribute(value, tag = "") {
|
|
1432
|
+
if (Array.isArray(value)) {
|
|
1433
|
+
for (const item of value) {
|
|
1434
|
+
const found = firstHandlerAttribute(item, tag);
|
|
1435
|
+
if (found) return found;
|
|
1436
|
+
}
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const current = node(value);
|
|
1440
|
+
if (!current) return void 0;
|
|
1441
|
+
if (current.type === "JSXAttribute") {
|
|
1442
|
+
const name = node(current.name)?.name;
|
|
1443
|
+
return typeof name === "string" && (isEventAttribute(name) || name === "attach") ? {
|
|
1444
|
+
attribute: name,
|
|
1445
|
+
tag
|
|
1446
|
+
} : void 0;
|
|
1447
|
+
}
|
|
1448
|
+
const elementName = node(node(current.openingElement)?.name)?.name;
|
|
1449
|
+
const nextTag = typeof elementName === "string" ? elementName : tag;
|
|
1450
|
+
for (const [key, child] of Object.entries(current)) {
|
|
1451
|
+
if (key === "loc" || key === "range" || key === "metadata") continue;
|
|
1452
|
+
const found = firstHandlerAttribute(child, nextTag);
|
|
1453
|
+
if (found) return found;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1406
1456
|
function readsOnlyRouteInvariantProps(params) {
|
|
1407
1457
|
if (params.length === 0) return true;
|
|
1408
1458
|
if (params.length > 1) return false;
|
|
@@ -1477,6 +1527,30 @@ function fragmentRequestModule() {
|
|
|
1477
1527
|
};
|
|
1478
1528
|
}
|
|
1479
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
|
|
1480
1554
|
//#region src/vite/nitro-prerender.ts
|
|
1481
1555
|
function serverBuildState() {
|
|
1482
1556
|
return { entry: void 0 };
|
|
@@ -1571,8 +1645,10 @@ function router(options = {}) {
|
|
|
1571
1645
|
const navigationEntry = resumeEntryState();
|
|
1572
1646
|
const fragmentEntry = resumeEntryState();
|
|
1573
1647
|
const routePreloads = routePreloadState();
|
|
1648
|
+
const serverBuild = serverBuildState();
|
|
1649
|
+
const devNitro = devNitroState();
|
|
1574
1650
|
return [
|
|
1575
|
-
routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry,
|
|
1651
|
+
routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuild, devNitro),
|
|
1576
1652
|
devSourceModuleRequestPlugin(),
|
|
1577
1653
|
devPageForUrlPlugin(options),
|
|
1578
1654
|
pageServerOnlyPropsPlugin(),
|
|
@@ -1583,6 +1659,7 @@ function router(options = {}) {
|
|
|
1583
1659
|
htmlTransformPlugin(),
|
|
1584
1660
|
virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, options, fragmentEntry),
|
|
1585
1661
|
linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options),
|
|
1662
|
+
devNitroClosePlugin(devNitro),
|
|
1586
1663
|
nitroPlugins
|
|
1587
1664
|
];
|
|
1588
1665
|
}
|
|
@@ -1647,7 +1724,7 @@ function devSourceModuleRequestPlugin() {
|
|
|
1647
1724
|
}
|
|
1648
1725
|
};
|
|
1649
1726
|
}
|
|
1650
|
-
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()) {
|
|
1651
1728
|
let clientOutDir = "";
|
|
1652
1729
|
let clientEnvironmentName = "";
|
|
1653
1730
|
let clientEnvironmentConfig;
|
|
@@ -1681,7 +1758,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
|
|
|
1681
1758
|
ssrEnvironment.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, config.root);
|
|
1682
1759
|
const serverWatch = withWatchIgnores(config.server?.watch);
|
|
1683
1760
|
return {
|
|
1684
|
-
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),
|
|
1685
1762
|
server: {
|
|
1686
1763
|
...config.server,
|
|
1687
1764
|
watch: serverWatch
|
|
@@ -1877,7 +1954,7 @@ function requestFileBuildTransformPlugin(root) {
|
|
|
1877
1954
|
}
|
|
1878
1955
|
};
|
|
1879
1956
|
}
|
|
1880
|
-
function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState()) {
|
|
1957
|
+
function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState(), devNitro = devNitroState()) {
|
|
1881
1958
|
const scanDirs = Array.isArray(nitroConfig?.scanDirs) ? nitroConfig.scanDirs.filter((dir) => typeof dir === "string") : [];
|
|
1882
1959
|
const watchOptions = withWatchIgnores(nitroConfig?.watchOptions, serverWatchIgnored);
|
|
1883
1960
|
const publicAssets = Array.isArray(nitroConfig?.publicAssets) ? nitroConfig.publicAssets : [];
|
|
@@ -1900,7 +1977,7 @@ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isB
|
|
|
1900
1977
|
modules: [
|
|
1901
1978
|
...nitroConfig?.modules ?? [],
|
|
1902
1979
|
fragmentRequestModule(),
|
|
1903
|
-
...isBuild ? [prerenderRendererModule(serverBuild)] : []
|
|
1980
|
+
...isBuild ? [prerenderRendererModule(serverBuild)] : [devNitroModule(devNitro)]
|
|
1904
1981
|
],
|
|
1905
1982
|
...isBuild && { routeRules: {
|
|
1906
1983
|
[`/${MARKLESS_BUILD_PREFIX}**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
|
|
@@ -2048,9 +2125,11 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
|
|
|
2048
2125
|
}
|
|
2049
2126
|
async function routerOptionsSource(options, documentFile) {
|
|
2050
2127
|
const documentSource = await readFile(documentFile, "utf8").catch(() => void 0);
|
|
2128
|
+
const handlerError = documentHandlerError(documentSource, documentFile);
|
|
2129
|
+
if (handlerError) throw new Error(handlerError);
|
|
2051
2130
|
return [
|
|
2052
2131
|
"export const routerMode = \"path\";",
|
|
2053
|
-
`export const documentNavigation = ${JSON.stringify(documentNavigationFor(documentSource
|
|
2132
|
+
`export const documentNavigation = ${JSON.stringify(documentNavigationFor(documentSource))};`,
|
|
2054
2133
|
linkIntentEnabled(options) ? `export const startLinkIntentPreloading = (root, preload) => (${listenForLinkIntent.toString()})(root, ${JSON.stringify(LINK_ATTRIBUTE)}, preload, ${JSON.stringify(PREFETCH_ATTRIBUTE)});` : "export const startLinkIntentPreloading = () => () => {};"
|
|
2055
2134
|
].join("\n");
|
|
2056
2135
|
}
|
|
@@ -2098,7 +2177,7 @@ function linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options) {
|
|
|
2098
2177
|
fragmentNavigationSource: minifiedFragmentNavigationSource(),
|
|
2099
2178
|
fragmentNavigationRunSource: minifiedFragmentNavigationSource(runFragmentNavigation),
|
|
2100
2179
|
prefetch: options.prefetch,
|
|
2101
|
-
documentNavigation: documentNavigationFor(documentSource
|
|
2180
|
+
documentNavigation: documentNavigationFor(documentSource),
|
|
2102
2181
|
routeFileIds: bundledPageFileIds(chunks, root),
|
|
2103
2182
|
debug
|
|
2104
2183
|
});
|
|
@@ -2132,13 +2211,13 @@ function serverEntrySource(root, options) {
|
|
|
2132
2211
|
const query = rootScopeQuery(root);
|
|
2133
2212
|
return [
|
|
2134
2213
|
`import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';`,
|
|
2135
|
-
`import { resumeEntryPath } from
|
|
2136
|
-
`import { prerenderWakeEntryPath } from
|
|
2137
|
-
`import { navigationEntryPath } from
|
|
2138
|
-
`import { fragmentEntryPath } from
|
|
2139
|
-
`import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap } from
|
|
2140
|
-
`import { pageModuleLoaders, routeFileIds } from
|
|
2141
|
-
`import { documentNavigation } from
|
|
2214
|
+
`import { resumeEntryPath } from ${JSON.stringify(`${RESUME_ENTRY_PATH_ID}${query}`)};`,
|
|
2215
|
+
`import { prerenderWakeEntryPath } from ${JSON.stringify(`${PRERENDER_WAKE_ENTRY_PATH_ID}${query}`)};`,
|
|
2216
|
+
`import { navigationEntryPath } from ${JSON.stringify(`${NAVIGATION_ENTRY_PATH_ID}${query}`)};`,
|
|
2217
|
+
`import { fragmentEntryPath } from ${JSON.stringify(`${FRAGMENT_ENTRY_PATH_ID}${query}`)};`,
|
|
2218
|
+
`import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap } from ${JSON.stringify(`${ROUTE_PRELOADS_ID}${query}`)};`,
|
|
2219
|
+
`import { pageModuleLoaders, routeFileIds } from ${JSON.stringify(`${ROUTE_DISCOVERY_ID}${query}`)};`,
|
|
2220
|
+
`import { documentNavigation } from ${JSON.stringify(`${ROUTER_OPTIONS_ID}${query}`)};`,
|
|
2142
2221
|
`const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
|
|
2143
2222
|
`const linkBridgePathsJson = globalThis.__marklessRouterLinkBridgePathsJson ?? ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)};`,
|
|
2144
2223
|
`const entry = createServerEntry({`,
|
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": {
|
|
@@ -57,22 +57,23 @@
|
|
|
57
57
|
"provenance": true
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@tsrx/yuku": "0.
|
|
60
|
+
"@tsrx/yuku": "0.8.0",
|
|
61
61
|
"@virtualstate/navigation": "1.0.1-alpha.212",
|
|
62
62
|
"hast-util-to-html": "^9.0.5",
|
|
63
|
-
"nitro": "3.0.
|
|
63
|
+
"nitro": "3.0.260903-beta",
|
|
64
64
|
"pathe": "^2.0.3",
|
|
65
65
|
"satteri": "0.6.3",
|
|
66
66
|
"ufo": "^1.6.4",
|
|
67
|
-
"@markless/
|
|
68
|
-
"@markless/bundler": "0.
|
|
67
|
+
"@markless/compiler": "0.9.0",
|
|
68
|
+
"@markless/bundler": "0.9.0",
|
|
69
|
+
"@markless/web": "0.9.0"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
|
-
"@markless/analyzer": "0.
|
|
72
|
+
"@markless/analyzer": "0.9.0"
|
|
72
73
|
},
|
|
73
74
|
"peerDependencies": {
|
|
74
75
|
"typescript": "^5.9.3",
|
|
75
|
-
"vite": "8.
|
|
76
|
+
"vite": "8.3.4"
|
|
76
77
|
},
|
|
77
78
|
"peerDependenciesMeta": {
|
|
78
79
|
"vite": {
|
|
@@ -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;
|
|
@@ -401,27 +402,132 @@ async function mergeHead(d: Document, next: Document) {
|
|
|
401
402
|
const rel = element.localName === 'link' ? (element as HTMLLinkElement).rel : '';
|
|
402
403
|
if (rel === 'modulepreload' || rel === 'preload') continue;
|
|
403
404
|
if (rel === 'stylesheet' || element.localName === 'style') stale.push(element);
|
|
404
|
-
else element.remove();
|
|
405
|
+
else if ((element as HTMLScriptElement).type !== 'importmap') element.remove();
|
|
405
406
|
}
|
|
406
407
|
await loaded(d.defaultView!, stylesheets);
|
|
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,6 +727,61 @@ async function streamAppends(
|
|
|
621
727
|
return signal.aborted ? undefined : all;
|
|
622
728
|
}
|
|
623
729
|
|
|
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;
|
|
732
|
+
|
|
733
|
+
type HeldPage = [parent: Node, before: Node | null, nodes: ChildNode[]][];
|
|
734
|
+
|
|
735
|
+
function holdPage(parent: Element, after: Node | undefined, before: Node | null): HeldPage {
|
|
736
|
+
const nodes: ChildNode[] = [];
|
|
737
|
+
for (let node = after ? after.nextSibling : parent.firstChild; node && node !== before;) {
|
|
738
|
+
const next: ChildNode | null = node.nextSibling;
|
|
739
|
+
nodes.push(node);
|
|
740
|
+
node.remove();
|
|
741
|
+
node = next;
|
|
742
|
+
}
|
|
743
|
+
return [[parent, before, nodes]];
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const elementsIn = (node: Node) =>
|
|
747
|
+
node.nodeType === 1 ? (node as Element).getElementsByTagName('*').length + 1 : 0;
|
|
748
|
+
|
|
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.
|
|
750
|
+
function fillPage(
|
|
751
|
+
w: Window,
|
|
752
|
+
held: HeldPage,
|
|
753
|
+
budget: number,
|
|
754
|
+
current: () => boolean,
|
|
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));
|
|
765
|
+
let used = 0;
|
|
766
|
+
while (held.length) {
|
|
767
|
+
const [parent, before, nodes] = held[0]!;
|
|
768
|
+
const size = elementsIn(nodes[0]!);
|
|
769
|
+
if (used > 0 && used + size > budget && size <= budget) return nextChunk();
|
|
770
|
+
const node = nodes.shift()!;
|
|
771
|
+
if (!nodes.length) held.shift();
|
|
772
|
+
if (size > budget && node.firstChild) {
|
|
773
|
+
const children = [...node.childNodes];
|
|
774
|
+
(node as Element).replaceChildren();
|
|
775
|
+
held.unshift([node, null, children]);
|
|
776
|
+
used += 1;
|
|
777
|
+
} else used += size;
|
|
778
|
+
parent.insertBefore(node, before);
|
|
779
|
+
if (used >= budget && held.length) return nextChunk();
|
|
780
|
+
}
|
|
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();
|
|
783
|
+
}
|
|
784
|
+
|
|
624
785
|
export async function navigate(
|
|
625
786
|
d: Document,
|
|
626
787
|
url: URL,
|
|
@@ -647,13 +808,16 @@ export async function navigate(
|
|
|
647
808
|
return;
|
|
648
809
|
}
|
|
649
810
|
if (id !== session.navigation) return taken.abort();
|
|
811
|
+
// A prefetch is shared by every link to the page, so the address takes the clicked link's #fragment.
|
|
812
|
+
const landed = new URL(fetched.url);
|
|
813
|
+
landed.hash = url.hash;
|
|
650
814
|
const next = fetched.doc;
|
|
651
815
|
const documentLoads = state.config.documentLoads;
|
|
652
816
|
const incoming = documentLoads ? next.body : regionOf(state, next);
|
|
653
817
|
const outgoing = documentLoads ? d.body : regionOf(state, d);
|
|
654
818
|
const scope = documentLoads && !isContainer(d.body) ? d.body : outgoing?.parentElement;
|
|
655
819
|
if (!incoming || !outgoing || !scope) {
|
|
656
|
-
fallback(
|
|
820
|
+
fallback(landed.href);
|
|
657
821
|
return;
|
|
658
822
|
}
|
|
659
823
|
// The destination shows once its own code has arrived, so its first interaction never waits on a fetch.
|
|
@@ -686,81 +850,131 @@ export async function navigate(
|
|
|
686
850
|
w.history.scrollRestoration = 'manual';
|
|
687
851
|
session.current = entryId();
|
|
688
852
|
const entry = { __marklessFragment: { id: session.current, x: 0, y: 0 } };
|
|
689
|
-
if (mode === 'push') w.history.pushState(entry, '',
|
|
690
|
-
else w.history.replaceState(entry, '',
|
|
691
|
-
}
|
|
692
|
-
const offsets: ScrolledBox[] = [];
|
|
693
|
-
for (const element of state.scrolled) {
|
|
694
|
-
if (!element.isConnected || !scope.contains(element)) continue;
|
|
695
|
-
if (element.scrollTop || element.scrollLeft)
|
|
696
|
-
offsets.push({
|
|
697
|
-
path: elementPath(scope, element),
|
|
698
|
-
id: element.id,
|
|
699
|
-
kind: boxKind(element),
|
|
700
|
-
top: element.scrollTop,
|
|
701
|
-
left: element.scrollLeft,
|
|
702
|
-
});
|
|
853
|
+
if (mode === 'push') w.history.pushState(entry, '', landed.href);
|
|
854
|
+
else w.history.replaceState(entry, '', landed.href);
|
|
703
855
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
let
|
|
712
|
-
let
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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)) {
|
|
731
892
|
const fresh: Element[] = [];
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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,
|
|
739
906
|
);
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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('')));
|
|
746
946
|
}
|
|
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
947
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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;
|
|
764
978
|
const ran = runScripts(
|
|
765
979
|
d,
|
|
766
980
|
[...head.scripts, ...(scripts as HTMLScriptElement[])],
|
|
@@ -773,7 +987,7 @@ export async function navigate(
|
|
|
773
987
|
if (restore === 'reset') {
|
|
774
988
|
const target = url.hash ? d.getElementById(decodeURIComponent(url.hash.slice(1))) : null;
|
|
775
989
|
if (target) target.scrollIntoView({ behavior: 'instant' });
|
|
776
|
-
else w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
|
990
|
+
else if (!progressive) w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
|
777
991
|
} else if (restore !== 'manual')
|
|
778
992
|
w.scrollTo({ top: restore.y, left: restore.x, behavior: 'instant' });
|
|
779
993
|
d.dispatchEvent(new CustomEvent('marklessrouterrendered'));
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import { a as __marklessDebugInvalidateElement, c as __marklessDebugRegisterDelegatedEvents, d as __marklessDebugSetBoundaries, f as __marklessDebugStartContainer, i as __marklessDebugDisposeContainer, l as __marklessDebugRegisterRouter, n as __marklessDebugBootstrapSource, o as __marklessDebugRecordInteraction, r as __marklessDebugChannelForTest, s as __marklessDebugRecordViolation, t as __marklessDebugActivateContainer, u as __marklessDebugResetForTest } from "./debug-channel-Di6SppZv.js";
|
|
2
|
-
export { __marklessDebugActivateContainer, __marklessDebugBootstrapSource, __marklessDebugChannelForTest, __marklessDebugDisposeContainer, __marklessDebugInvalidateElement, __marklessDebugRecordInteraction, __marklessDebugRecordViolation, __marklessDebugRegisterDelegatedEvents, __marklessDebugRegisterRouter, __marklessDebugResetForTest, __marklessDebugSetBoundaries, __marklessDebugStartContainer };
|