@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.
@@ -1,5 +1,5 @@
1
- import { a as ROUTE_SCRIPT_TYPE, c as servedHtmlAttributesScript, d as REPLACE_ATTRIBUTE, f as SCROLL_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, l as LINK_ATTRIBUTE, n as requestLocals, o as SERVED_HTML_ATTRIBUTES_PROPERTY, r as takeMiddlewareHeaders, u as PREFETCH_ATTRIBUTE } from "./request-locals-BDAhNpEa.js";
2
- import { i as matchRouteManifest, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId } from "./route-manifest-DMizh42B.js";
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) => segment === "**" || segment === ":" || segment === parts[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: new DOMParser().parseFromString(prefix, "text/html"),
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 an Markless compiled artifact: ${escapeHtml(file)}`;
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 = (documentLoads && fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (options.importMap ?? "");
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, listenForLinkIntent as s, createServerEntry as t };
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 { d as REPLACE_ATTRIBUTE, f as SCROLL_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, l as LINK_ATTRIBUTE, s as routeMountTarget, t as rememberRequestLocals, u as PREFETCH_ATTRIBUTE } from "./request-locals-BDAhNpEa.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-DMizh42B.js";
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-CewnC9fH.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-CewnC9fH.js").then((debug) => debug.__marklessDebugRegisterRouter(void 0, "navigation-event")).catch(() => {});
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.${SERVED_HTML_ATTRIBUTES_PROPERTY}=document.documentElement.getAttributeNames()<\/script>`;
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, servedHtmlAttributesScript as c, REPLACE_ATTRIBUTE as d, SCROLL_ATTRIBUTE as f, HTML_ATTRIBUTES_SLOT as i, LINK_ATTRIBUTE as l, requestLocals as n, SERVED_HTML_ATTRIBUTES_PROPERTY as o, takeMiddlewareHeaders as r, routeMountTarget as s, rememberRequestLocals as t, PREFETCH_ATTRIBUTE as u };
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
  }
@@ -1,2 +1 @@
1
- import { t as __marklessRouteHref } from "../route-href-CGooO_g6.js";
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-DMizh42B.js";
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
- import { n as routerLinkBridgeModules, t as createServerEntry } from "../../create-server-entry-yboa-2m6.js";
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 REPLACE_ATTRIBUTE, f as SCROLL_ATTRIBUTE, l as LINK_ATTRIBUTE, u as PREFETCH_ATTRIBUTE } from "./request-locals-BDAhNpEa.js";
2
- import { i as matchRouteManifest, l as transformRequestFileSource, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId, r as isPageModuleFile, t as assertNoEndpointConflicts } from "./route-manifest-DMizh42B.js";
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 listenForLinkIntent } from "./create-server-entry-yboa-2m6.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-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 { parseModule } from "@tsrx/yuku";
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 parseModule(code, id);
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, id) {
1405
+ function documentNavigationFor(source) {
1393
1406
  if (source === void 0) return "client";
1394
1407
  let program;
1395
1408
  try {
1396
- program = parseModule(source, id);
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, serverBuildState()),
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, documentFile))};`,
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, documentFile),
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 '${RESUME_ENTRY_PATH_ID}${query}';`,
2136
- `import { prerenderWakeEntryPath } from '${PRERENDER_WAKE_ENTRY_PATH_ID}${query}';`,
2137
- `import { navigationEntryPath } from '${NAVIGATION_ENTRY_PATH_ID}${query}';`,
2138
- `import { fragmentEntryPath } from '${FRAGMENT_ENTRY_PATH_ID}${query}';`,
2139
- `import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap } from '${ROUTE_PRELOADS_ID}${query}';`,
2140
- `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
2141
- `import { documentNavigation } from '${ROUTER_OPTIONS_ID}${query}';`,
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.7.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.2.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.260429-beta",
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/web": "0.7.0",
68
- "@markless/bundler": "0.7.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.7.0"
72
+ "@markless/analyzer": "0.9.0"
72
73
  },
73
74
  "peerDependencies": {
74
75
  "typescript": "^5.9.3",
75
- "vite": "8.2.2"
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
- // Only attributes a server document set are the server's to remove: scripts own the rest (a theme class).
411
- const serverHtmlAttributes = new WeakMap<Document, Set<string>>();
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 root = d.documentElement;
415
- const incoming = next.documentElement;
416
- const served =
417
- serverHtmlAttributes.get(d) ??
418
- ((root as unknown as Record<string, readonly string[] | undefined>)[servedProperty] || []);
419
- for (const name of served)
420
- if (!incoming.hasAttribute(name)) root.removeAttribute(name);
421
- for (const attribute of incoming.attributes)
422
- if (root.getAttribute(attribute.name) !== attribute.value)
423
- root.setAttribute(attribute.name, attribute.value);
424
- serverHtmlAttributes.set(d, new Set(incoming.getAttributeNames()));
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(fetched.url);
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, '', fetched.url);
690
- else w.history.replaceState(entry, '', fetched.url);
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
- state.scrolled.clear();
705
- retire(state, outgoing);
706
- (w as unknown as Record<string, unknown>)[state.config.streamExecutorGlobal] = undefined;
707
- for (const element of d.querySelectorAll(state.config.streamedArmSelector)) element.remove();
708
- state.shown = pageKey(new URL(w.location.href));
709
- if (documentLoads) syncHtmlAttributes(d, next, state.config.servedHtmlAttributesProperty);
710
- let region: Element;
711
- let end: Comment;
712
- let scripts: Element[];
713
- if (documentLoads && !isContainer(d.body)) {
714
- const fresh: Element[] = [];
715
- region = d.body;
716
- morphChildren(d, region, incoming, fresh);
717
- scripts = fresh.flatMap(scriptsIn);
718
- region.append((end = d.createComment('')));
719
- } else {
720
- region = d.adoptNode(incoming);
721
- const leavingEnd = pageEnd(outgoing, state.config.pageEndComment);
722
- if (leavingEnd) {
723
- carryHostAttributes(d, outgoing, region, state.config.containerAttributes);
724
- end =
725
- pageEnd(region, state.config.pageEndComment) ??
726
- region.appendChild(d.createComment(state.config.pageEndComment));
727
- // The document's own nodes after the page stay; a document app morphs them to its next shell.
728
- const focused = d.activeElement as HTMLElement | null;
729
- const kept = nodesAfter(d, leavingEnd);
730
- const arriving = nodesAfter(d, end);
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
- if (documentLoads) morphChildren(d, kept, arriving, fresh);
733
- const leavingStart = pageStart(outgoing, state.config.pageStartComment);
734
- let start = pageStart(region, state.config.pageStartComment);
735
- if (leavingStart || start) {
736
- start ??= region.insertBefore(
737
- d.createComment(state.config.pageStartComment),
738
- region.firstChild,
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
- const keptBefore = leavingStart
741
- ? nodesBefore(d, leavingStart)
742
- : d.createDocumentFragment();
743
- const arrivingBefore = nodesBefore(d, start);
744
- if (documentLoads) morphChildren(d, keptBefore, arrivingBefore, fresh);
745
- region.insertBefore(keptBefore, start);
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
- // 'instant': a `scroll-behavior: smooth` page would otherwise animate every landing.
760
- for (const box of offsets)
761
- sameBox(scope, box)?.scrollTo({ top: box.top, left: box.left, behavior: 'instant' });
762
- for (const element of head.stale) element.remove();
763
- if (next.title && d.title !== next.title) d.title = next.title;
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 };