@markless/router 0.7.0 → 0.8.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
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";
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) => {
@@ -836,7 +848,7 @@ function createServerEntry(options) {
836
848
  const renderSsr = baseArtifact?.renderSsr ?? pageModule.marklessRenderSsr;
837
849
  if (!renderSsr) {
838
850
  const shell = await renderDocumentShell(documentModule, pageProps, "");
839
- const message = `Page module must export an Markless compiled artifact: ${escapeHtml(file)}`;
851
+ const message = `Page module must export a Markless compiled artifact: ${escapeHtml(file)}`;
840
852
  return new Response(fillDocumentChildren(shell, message), http.commit(status, headers));
841
853
  }
842
854
  const documentLoads = options.documentNavigation === "document";
@@ -864,7 +876,7 @@ function createServerEntry(options) {
864
876
  resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
865
877
  modulePreloads: (html, resumes) => landingModulePreloads(resumes ? options.routeSsrModulePreloads?.[file] : void 0, linkBridges?.[html.includes("href=\"#/") ? 1 : 0])
866
878
  };
867
- const headStart = (documentLoads && fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (options.importMap ?? "");
879
+ const headStart = (documentLoads && fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (fragment ? "" : options.importMap ?? "");
868
880
  if (options.render === "blocking") {
869
881
  const pageHtml = splitPageHtml(await renderToString(routedArtifact, renderOptions));
870
882
  privateToVisitor();
@@ -1104,8 +1116,15 @@ function wholeDocument(shell, page, fragment, documentLoads) {
1104
1116
  async function renderDocumentModule(documentModule, props, htmlAttributes) {
1105
1117
  const output = await documentModule?.default?.renderSsr?.(props, { [HTML_ATTRIBUTES_SLOT]: htmlAttributes });
1106
1118
  assertDocumentStorageFree(output);
1119
+ assertDocumentHandlerFree(output);
1107
1120
  return output?.html;
1108
1121
  }
1122
+ function assertDocumentHandlerFree(output) {
1123
+ const view = output?.view;
1124
+ const names = [...new Set((view?.events ?? []).map((event) => event.eventName))];
1125
+ if (names.length === 0) return;
1126
+ throw new Error(documentHandlersMessage(`the document renders ${names.join(", ")} handlers`));
1127
+ }
1109
1128
  function assertDocumentStorageFree(output) {
1110
1129
  const storage = documentStorageRecords(output?.state);
1111
1130
  if (storage.length === 0) return;
@@ -1330,4 +1349,4 @@ function insertImportMap(html, importMap) {
1330
1349
  return `${html.slice(0, at)}${importMap}${html.slice(at)}`;
1331
1350
  }
1332
1351
  //#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 };
1352
+ 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
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";
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;
@@ -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-BsYTWdxi.js";
@@ -1186,4 +1186,5 @@ export declare function renderMdxChild(children: MdxChild[], component: MdxCompo
1186
1186
  export declare function composeMdxState(children: readonly MdxChild[]): MdxStatePayload | undefined;
1187
1187
  export declare function composeMdxView(parts: readonly MdxRoutePart[], children: readonly MdxChild[], initialElementOffset: number, initialCommentOffset?: number): MdxViewPayload | undefined;
1188
1188
  export declare function loadMdxSymbol(symbolId: string, children: readonly MdxChild[], loaders: readonly MdxSymbolLoader[]): unknown;
1189
- //#endregion
1189
+ //#endregion
1190
+ export {};
package/dist/vite.js CHANGED
@@ -1,8 +1,8 @@
1
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";
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-BsYTWdxi.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;
@@ -2048,9 +2098,11 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
2048
2098
  }
2049
2099
  async function routerOptionsSource(options, documentFile) {
2050
2100
  const documentSource = await readFile(documentFile, "utf8").catch(() => void 0);
2101
+ const handlerError = documentHandlerError(documentSource, documentFile);
2102
+ if (handlerError) throw new Error(handlerError);
2051
2103
  return [
2052
2104
  "export const routerMode = \"path\";",
2053
- `export const documentNavigation = ${JSON.stringify(documentNavigationFor(documentSource, documentFile))};`,
2105
+ `export const documentNavigation = ${JSON.stringify(documentNavigationFor(documentSource))};`,
2054
2106
  linkIntentEnabled(options) ? `export const startLinkIntentPreloading = (root, preload) => (${listenForLinkIntent.toString()})(root, ${JSON.stringify(LINK_ATTRIBUTE)}, preload, ${JSON.stringify(PREFETCH_ATTRIBUTE)});` : "export const startLinkIntentPreloading = () => () => {};"
2055
2107
  ].join("\n");
2056
2108
  }
@@ -2098,7 +2150,7 @@ function linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options) {
2098
2150
  fragmentNavigationSource: minifiedFragmentNavigationSource(),
2099
2151
  fragmentNavigationRunSource: minifiedFragmentNavigationSource(runFragmentNavigation),
2100
2152
  prefetch: options.prefetch,
2101
- documentNavigation: documentNavigationFor(documentSource, documentFile),
2153
+ documentNavigation: documentNavigationFor(documentSource),
2102
2154
  routeFileIds: bundledPageFileIds(chunks, root),
2103
2155
  debug
2104
2156
  });
@@ -2132,13 +2184,13 @@ function serverEntrySource(root, options) {
2132
2184
  const query = rootScopeQuery(root);
2133
2185
  return [
2134
2186
  `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}';`,
2187
+ `import { resumeEntryPath } from ${JSON.stringify(`${RESUME_ENTRY_PATH_ID}${query}`)};`,
2188
+ `import { prerenderWakeEntryPath } from ${JSON.stringify(`${PRERENDER_WAKE_ENTRY_PATH_ID}${query}`)};`,
2189
+ `import { navigationEntryPath } from ${JSON.stringify(`${NAVIGATION_ENTRY_PATH_ID}${query}`)};`,
2190
+ `import { fragmentEntryPath } from ${JSON.stringify(`${FRAGMENT_ENTRY_PATH_ID}${query}`)};`,
2191
+ `import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap } from ${JSON.stringify(`${ROUTE_PRELOADS_ID}${query}`)};`,
2192
+ `import { pageModuleLoaders, routeFileIds } from ${JSON.stringify(`${ROUTE_DISCOVERY_ID}${query}`)};`,
2193
+ `import { documentNavigation } from ${JSON.stringify(`${ROUTER_OPTIONS_ID}${query}`)};`,
2142
2194
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
2143
2195
  `const linkBridgePathsJson = globalThis.__marklessRouterLinkBridgePathsJson ?? ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)};`,
2144
2196
  `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.8.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/bundler": "0.8.0",
68
+ "@markless/web": "0.8.0",
69
+ "@markless/compiler": "0.8.0"
69
70
  },
70
71
  "devDependencies": {
71
- "@markless/analyzer": "0.7.0"
72
+ "@markless/analyzer": "0.8.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": {
@@ -401,7 +401,7 @@ async function mergeHead(d: Document, next: Document) {
401
401
  const rel = element.localName === 'link' ? (element as HTMLLinkElement).rel : '';
402
402
  if (rel === 'modulepreload' || rel === 'preload') continue;
403
403
  if (rel === 'stylesheet' || element.localName === 'style') stale.push(element);
404
- else element.remove();
404
+ else if ((element as HTMLScriptElement).type !== 'importmap') element.remove();
405
405
  }
406
406
  await loaded(d.defaultView!, stylesheets);
407
407
  return { stale, scripts };
@@ -621,6 +621,53 @@ async function streamAppends(
621
621
  return signal.aborted ? undefined : all;
622
622
  }
623
623
 
624
+ // Inserting a big page whole lays it out in one long frame; the HTML parser yields while it streams a document, and the swap does the same.
625
+ const FRAME_ELEMENTS = 800;
626
+
627
+ type HeldPage = [parent: Node, before: Node | null, nodes: ChildNode[]][];
628
+
629
+ function holdPage(parent: Element, after: Node | undefined, before: Node | null): HeldPage {
630
+ const nodes: ChildNode[] = [];
631
+ for (let node = after ? after.nextSibling : parent.firstChild; node && node !== before; ) {
632
+ const next: ChildNode | null = node.nextSibling;
633
+ nodes.push(node);
634
+ node.remove();
635
+ node = next;
636
+ }
637
+ return [[parent, before, nodes]];
638
+ }
639
+
640
+ const elementsIn = (node: Node) =>
641
+ node.nodeType === 1 ? (node as Element).getElementsByTagName('*').length + 1 : 0;
642
+
643
+ // Places a frame's worth synchronously, so the first paint shows the top of the page; true when everything is in.
644
+ function fillPage(
645
+ w: Window,
646
+ held: HeldPage,
647
+ budget: number,
648
+ current: () => boolean,
649
+ ): true | Promise<boolean> {
650
+ let used = 0;
651
+ while (held.length) {
652
+ const [parent, before, nodes] = held[0]!;
653
+ const node = nodes.shift()!;
654
+ if (!nodes.length) held.shift();
655
+ const size = elementsIn(node);
656
+ if (used + size > budget && node.firstChild) {
657
+ const children = [...node.childNodes];
658
+ (node as Element).replaceChildren();
659
+ held.unshift([node, null, children]);
660
+ used += 1;
661
+ } else used += size;
662
+ parent.insertBefore(node, before);
663
+ if (used >= budget && held.length)
664
+ return new Promise<void>((resolve) =>
665
+ w.requestAnimationFrame(() => w.setTimeout(resolve, 0)),
666
+ ).then(() => current() && fillPage(w, held, budget, current));
667
+ }
668
+ return true;
669
+ }
670
+
624
671
  export async function navigate(
625
672
  d: Document,
626
673
  url: URL,
@@ -647,13 +694,16 @@ export async function navigate(
647
694
  return;
648
695
  }
649
696
  if (id !== session.navigation) return taken.abort();
697
+ // A prefetch is shared by every link to the page, so the address takes the clicked link's #fragment.
698
+ const landed = new URL(fetched.url);
699
+ landed.hash = url.hash;
650
700
  const next = fetched.doc;
651
701
  const documentLoads = state.config.documentLoads;
652
702
  const incoming = documentLoads ? next.body : regionOf(state, next);
653
703
  const outgoing = documentLoads ? d.body : regionOf(state, d);
654
704
  const scope = documentLoads && !isContainer(d.body) ? d.body : outgoing?.parentElement;
655
705
  if (!incoming || !outgoing || !scope) {
656
- fallback(fetched.url);
706
+ fallback(landed.href);
657
707
  return;
658
708
  }
659
709
  // The destination shows once its own code has arrived, so its first interaction never waits on a fetch.
@@ -686,8 +736,8 @@ export async function navigate(
686
736
  w.history.scrollRestoration = 'manual';
687
737
  session.current = entryId();
688
738
  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);
739
+ if (mode === 'push') w.history.pushState(entry, '', landed.href);
740
+ else w.history.replaceState(entry, '', landed.href);
691
741
  }
692
742
  const offsets: ScrolledBox[] = [];
693
743
  for (const element of state.scrolled) {
@@ -709,7 +759,9 @@ export async function navigate(
709
759
  if (documentLoads) syncHtmlAttributes(d, next, state.config.servedHtmlAttributesProperty);
710
760
  let region: Element;
711
761
  let end: Comment;
762
+ let start: Comment | undefined;
712
763
  let scripts: Element[];
764
+ let held: HeldPage | undefined;
713
765
  if (documentLoads && !isContainer(d.body)) {
714
766
  const fresh: Element[] = [];
715
767
  region = d.body;
@@ -731,7 +783,7 @@ export async function navigate(
731
783
  const fresh: Element[] = [];
732
784
  if (documentLoads) morphChildren(d, kept, arriving, fresh);
733
785
  const leavingStart = pageStart(outgoing, state.config.pageStartComment);
734
- let start = pageStart(region, state.config.pageStartComment);
786
+ start = pageStart(region, state.config.pageStartComment);
735
787
  if (leavingStart || start) {
736
788
  start ??= region.insertBefore(
737
789
  d.createComment(state.config.pageStartComment),
@@ -746,16 +798,25 @@ export async function navigate(
746
798
  }
747
799
  scripts = [...scriptsIn(region), ...fresh.flatMap(scriptsIn)];
748
800
  region.append(kept);
801
+ held = holdPage(region, start, end);
749
802
  outgoing.replaceWith(region);
750
803
  // The document's own nodes moved to the new element; focus inside them stays put.
751
804
  if (focused && focused !== d.activeElement && region.contains(focused))
752
805
  focused.focus({ preventScroll: true });
753
806
  } else {
754
807
  scripts = scriptsIn(region);
808
+ held = holdPage(region, undefined, null);
755
809
  outgoing.replaceWith(region);
756
810
  region.after((end = d.createComment('')));
757
811
  }
758
812
  }
813
+ // A scroll target or a restored position needs the whole page in place before it is applied.
814
+ const progressive = restore === 'reset' && !url.hash && d.visibilityState !== 'hidden';
815
+ const filled = held && fillPage(w, held, progressive ? FRAME_ELEMENTS : Infinity, () => id === session.navigation);
816
+ if (filled instanceof Promise) {
817
+ w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
818
+ if (!(await filled)) return;
819
+ }
759
820
  // 'instant': a `scroll-behavior: smooth` page would otherwise animate every landing.
760
821
  for (const box of offsets)
761
822
  sameBox(scope, box)?.scrollTo({ top: box.top, left: box.left, behavior: 'instant' });
@@ -773,7 +834,7 @@ export async function navigate(
773
834
  if (restore === 'reset') {
774
835
  const target = url.hash ? d.getElementById(decodeURIComponent(url.hash.slice(1))) : null;
775
836
  if (target) target.scrollIntoView({ behavior: 'instant' });
776
- else w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
837
+ else if (!(filled instanceof Promise)) w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
777
838
  } else if (restore !== 'manual')
778
839
  w.scrollTo({ top: restore.y, left: restore.x, behavior: 'instant' });
779
840
  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 };