@markless/router 0.6.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
- import { a as ROUTE_SCRIPT_TYPE, c as PREFETCH_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, l as REPLACE_ATTRIBUTE, n as requestLocals, r as takeMiddlewareHeaders, s as LINK_ATTRIBUTE, u as SCROLL_ATTRIBUTE } from "./request-locals-z3rJTizs.js";
2
- import { i as matchRouteManifest, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId } from "./route-manifest-DMizh42B.js";
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-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,16 +876,17 @@ 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
  };
879
+ const headStart = (documentLoads && fragmentNavigation && !fragment ? servedHtmlAttributesScript() : "") + (fragment ? "" : options.importMap ?? "");
867
880
  if (options.render === "blocking") {
868
881
  const pageHtml = splitPageHtml(await renderToString(routedArtifact, renderOptions));
869
882
  privateToVisitor();
870
- const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml, options.importMap);
883
+ const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml, headStart);
871
884
  return new Response(wholeDocument(shell, pageHtml, fragment, documentLoads), http.commit(status, headers));
872
885
  }
873
886
  const stream = await renderToStream(routedArtifact, renderOptions);
874
887
  privateToVisitor();
875
888
  const pageHtml = splitPageHtml(stream.shell);
876
- const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml, options.importMap);
889
+ const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml, headStart);
877
890
  if (stream.pendingArmCount === 0) return new Response(wholeDocument(shell, pageHtml, fragment, documentLoads), http.commit(status, headers));
878
891
  const placed = placePage(shell, DOCUMENT_CHILDREN_PLACEHOLDER, pageHtml);
879
892
  const prefix = fragment ? fragmentDocument(shell, pageHtml, documentLoads) + FRAGMENT_REGION_END : placed.prefix;
@@ -1103,8 +1116,15 @@ function wholeDocument(shell, page, fragment, documentLoads) {
1103
1116
  async function renderDocumentModule(documentModule, props, htmlAttributes) {
1104
1117
  const output = await documentModule?.default?.renderSsr?.(props, { [HTML_ATTRIBUTES_SLOT]: htmlAttributes });
1105
1118
  assertDocumentStorageFree(output);
1119
+ assertDocumentHandlerFree(output);
1106
1120
  return output?.html;
1107
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
+ }
1108
1128
  function assertDocumentStorageFree(output) {
1109
1129
  const storage = documentStorageRecords(output?.state);
1110
1130
  if (storage.length === 0) return;
@@ -1269,6 +1289,7 @@ function fragmentConfig(fragment, documentLoads) {
1269
1289
  replaceAttribute: REPLACE_ATTRIBUTE,
1270
1290
  scrollAttribute: SCROLL_ATTRIBUTE,
1271
1291
  routeScriptType: ROUTE_SCRIPT_TYPE,
1292
+ servedHtmlAttributesProperty: SERVED_HTML_ATTRIBUTES_PROPERTY,
1272
1293
  retireProperty: CONTAINER_RETIRE_PROPERTY,
1273
1294
  streamExecutorGlobal: STREAM_ARM_EXECUTOR_GLOBAL,
1274
1295
  streamedArmSelector: STREAMED_ARM_SELECTOR,
@@ -1328,4 +1349,4 @@ function insertImportMap(html, importMap) {
1328
1349
  return `${html.slice(0, at)}${importMap}${html.slice(at)}`;
1329
1350
  }
1330
1351
  //#endregion
1331
- 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
- import { c as PREFETCH_ATTRIBUTE, i as HTML_ATTRIBUTES_SLOT, l as REPLACE_ATTRIBUTE, o as routeMountTarget, s as LINK_ATTRIBUTE, t as rememberRequestLocals, u as SCROLL_ATTRIBUTE } from "./request-locals-z3rJTizs.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 { 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-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;
@@ -8,6 +8,10 @@ const PREFETCH_ATTRIBUTE = "data-markless-router-prefetch";
8
8
  //#region src/route-dom.ts
9
9
  const ROUTE_SCRIPT_TYPE = "@markless/core/route";
10
10
  const HTML_ATTRIBUTES_SLOT = "__marklessRouterHtmlAttributes";
11
+ const SERVED_HTML_ATTRIBUTES_PROPERTY = "__marklessServedHtmlAttributes";
12
+ function servedHtmlAttributesScript() {
13
+ return `<script>document.documentElement.${SERVED_HTML_ATTRIBUTES_PROPERTY}=document.documentElement.getAttributeNames()<\/script>`;
14
+ }
11
15
  function routeMountTarget(document, currentRoot) {
12
16
  const root = currentRoot ?? document.querySelector(`script[type="@markless/core/route"]`)?.parentElement;
13
17
  if (!root) return document.body;
@@ -87,4 +91,4 @@ function takeMiddlewareHeaders(request) {
87
91
  return record.taken;
88
92
  }
89
93
  //#endregion
90
- export { ROUTE_SCRIPT_TYPE as a, PREFETCH_ATTRIBUTE as c, HTML_ATTRIBUTES_SLOT as i, REPLACE_ATTRIBUTE as l, requestLocals as n, routeMountTarget as o, takeMiddlewareHeaders as r, LINK_ATTRIBUTE as s, rememberRequestLocals as t, SCROLL_ATTRIBUTE as u };
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 };
@@ -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-DaYssO9x.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 {};
@@ -1,5 +1,64 @@
1
1
  import { i as protocolStateVersion, n as protocolInstanceQualifies, t as protocolInstancePath } from "../../protocol-constants-B4WxrZAS.js";
2
2
  //#endregion
3
+ //#region ../serializer/src/protocol-record-keys.ts
4
+ /** The field names each templated record list may spell, in the order its templates index them. */
5
+ const PROTOCOL_RECORD_KEYS = {
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",
8
+ sharedSeeds: "graphNodeId deriveSymbolId dependencies path reads",
9
+ sharedDefinitions: "id name exportedName scope version graphNodeIds projectionIds dependencies definitionId definitionName returnProperties kind graphNodeId path"
10
+ },
11
+ "markless/view": {
12
+ domUpdates: "hostNodeId source graphNodeId path target kind prefix suffix trueValue falseValue textNode name constantClass leadingClass symbolId",
13
+ behaviors: "hostNodeId source functionSource inputSources inputValues inputGraphReads inputIndex graphNodeId path symbolId usesGraph",
14
+ elementHandles: "hostNodeId handleId name plural"
15
+ }
16
+ };
17
+ //#endregion
18
+ //#region ../serializer/src/protocol-templated-lists.ts
19
+ /**
20
+ * The record lists a payload script may serve as record templates instead of a plain array. The
21
+ * inline resumer reads the others at page load, before any decoder has loaded, so they stay plain.
22
+ */
23
+ const PROTOCOL_TEMPLATED_LISTS = {
24
+ "markless/state": [
25
+ "computed",
26
+ "sharedSeeds",
27
+ "sharedDefinitions"
28
+ ],
29
+ "markless/view": [
30
+ "domUpdates",
31
+ "behaviors",
32
+ "elementHandles"
33
+ ]
34
+ };
35
+ //#endregion
36
+ //#region ../serializer/src/protocol-record-templates.ts
37
+ function expandRecordTemplates(list, keys = "") {
38
+ if (Array.isArray(list)) return list;
39
+ const { t, r } = list;
40
+ const names = keys.split(" ");
41
+ const parts = t.map((text) => text.replace(/~(\d+):/g, (_, at) => `"${names[+at]}":`).split(/\^(\d+)/));
42
+ return r.map((ref) => {
43
+ if (typeof ref === "number") return JSON.parse(parts[ref][0]);
44
+ if (!Array.isArray(ref)) return ref;
45
+ const part = parts[ref[0]];
46
+ let text = part[0];
47
+ for (let at = 1; at < part.length; at += 2) text += ref[+part[at] + 1] + part[at + 1];
48
+ return JSON.parse(text);
49
+ });
50
+ }
51
+ /** Expands, in place, every templated list of a freshly parsed payload script. */
52
+ function decodeProtocolPayloadLists(type, payload) {
53
+ const record = payload;
54
+ const keys = PROTOCOL_RECORD_KEYS[type];
55
+ if (record) for (const key of PROTOCOL_TEMPLATED_LISTS[type]) {
56
+ const list = record[key];
57
+ if (list && !Array.isArray(list)) record[key] = expandRecordTemplates(list, keys[key]);
58
+ }
59
+ return payload;
60
+ }
61
+ //#endregion
3
62
  //#region ../serializer/src/storage-cookie.ts
4
63
  const STORAGE_RENDER_CONTEXT_READER = "storageValue";
5
64
  //#endregion
@@ -747,7 +806,7 @@ function locate(root, view, host) {
747
806
  }
748
807
  function readScript(root, type) {
749
808
  const text = root.querySelector(`script[type="${type}"]`)?.textContent;
750
- return text ? JSON.parse(text) : void 0;
809
+ return text ? decodeProtocolPayloadLists(type, JSON.parse(text)) : void 0;
751
810
  }
752
811
  function references(value, id) {
753
812
  return value === id || !!value && typeof value === "object" && Object.values(value).some((child) => references(child, id));
package/dist/vite.js CHANGED
@@ -1,20 +1,30 @@
1
- import { c as PREFETCH_ATTRIBUTE, l as REPLACE_ATTRIBUTE, s as LINK_ATTRIBUTE, u as SCROLL_ATTRIBUTE } from "./request-locals-z3rJTizs.js";
2
- import { l as transformRequestFileSource, n as buildRouteManifestFromFileIds, o as normalizeRouteFileId, r as isPageModuleFile, t as assertNoEndpointConflicts } from "./route-manifest-DMizh42B.js";
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-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-DaYssO9x.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
- import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutLeadingSlash } from "ufo";
8
- import { existsSync } from "node:fs";
7
+ import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutBase, withoutLeadingSlash } from "ufo";
8
+ import { existsSync, readdirSync } from "node:fs";
9
9
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
10
10
  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;
@@ -1574,6 +1624,7 @@ function router(options = {}) {
1574
1624
  return [
1575
1625
  routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuildState()),
1576
1626
  devSourceModuleRequestPlugin(),
1627
+ devPageForUrlPlugin(options),
1577
1628
  pageServerOnlyPropsPlugin(),
1578
1629
  mdxTransformPlugin(),
1579
1630
  requestFileTransformPlugin(),
@@ -1595,6 +1646,37 @@ function pageServerOnlyPropsPlugin() {
1595
1646
  }
1596
1647
  };
1597
1648
  }
1649
+ function devPageForUrlPlugin(options) {
1650
+ return {
1651
+ name: "markless-router:dev-page-for-url",
1652
+ apply: "serve",
1653
+ configResolved(config) {
1654
+ config.plugins.find((plugin) => typeof plugin.api?.declarePageForUrl === "function")?.api.declarePageForUrl((href) => devPageForUrl(config.root, config.base, options.mode, href));
1655
+ }
1656
+ };
1657
+ }
1658
+ function devPageForUrl(root, base, mode, href) {
1659
+ let url;
1660
+ try {
1661
+ url = new URL(href);
1662
+ } catch {
1663
+ return;
1664
+ }
1665
+ const pagesDir = join(root, "pages");
1666
+ if (!existsSync(pagesDir)) return void 0;
1667
+ const fileIds = readdirSync(pagesDir, {
1668
+ recursive: true,
1669
+ encoding: "utf8"
1670
+ }).map((file) => join("pages", file));
1671
+ let manifest;
1672
+ try {
1673
+ manifest = buildRouteManifestFromFileIds(fileIds);
1674
+ } catch {
1675
+ return;
1676
+ }
1677
+ const pathname = mode === "hash" && url.hash.startsWith("#/") ? url.hash.slice(1) : withoutBase(url.pathname, base);
1678
+ return matchRouteManifest(pathname, manifest)?.route.file ?? manifest.statusPages.notFound;
1679
+ }
1598
1680
  function devSourceModuleRequestPlugin() {
1599
1681
  let root = "";
1600
1682
  return {
@@ -2016,9 +2098,11 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
2016
2098
  }
2017
2099
  async function routerOptionsSource(options, documentFile) {
2018
2100
  const documentSource = await readFile(documentFile, "utf8").catch(() => void 0);
2101
+ const handlerError = documentHandlerError(documentSource, documentFile);
2102
+ if (handlerError) throw new Error(handlerError);
2019
2103
  return [
2020
2104
  "export const routerMode = \"path\";",
2021
- `export const documentNavigation = ${JSON.stringify(documentNavigationFor(documentSource, documentFile))};`,
2105
+ `export const documentNavigation = ${JSON.stringify(documentNavigationFor(documentSource))};`,
2022
2106
  linkIntentEnabled(options) ? `export const startLinkIntentPreloading = (root, preload) => (${listenForLinkIntent.toString()})(root, ${JSON.stringify(LINK_ATTRIBUTE)}, preload, ${JSON.stringify(PREFETCH_ATTRIBUTE)});` : "export const startLinkIntentPreloading = () => () => {};"
2023
2107
  ].join("\n");
2024
2108
  }
@@ -2066,7 +2150,7 @@ function linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options) {
2066
2150
  fragmentNavigationSource: minifiedFragmentNavigationSource(),
2067
2151
  fragmentNavigationRunSource: minifiedFragmentNavigationSource(runFragmentNavigation),
2068
2152
  prefetch: options.prefetch,
2069
- documentNavigation: documentNavigationFor(documentSource, documentFile),
2153
+ documentNavigation: documentNavigationFor(documentSource),
2070
2154
  routeFileIds: bundledPageFileIds(chunks, root),
2071
2155
  debug
2072
2156
  });
@@ -2100,13 +2184,13 @@ function serverEntrySource(root, options) {
2100
2184
  const query = rootScopeQuery(root);
2101
2185
  return [
2102
2186
  `import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';`,
2103
- `import { resumeEntryPath } from '${RESUME_ENTRY_PATH_ID}${query}';`,
2104
- `import { prerenderWakeEntryPath } from '${PRERENDER_WAKE_ENTRY_PATH_ID}${query}';`,
2105
- `import { navigationEntryPath } from '${NAVIGATION_ENTRY_PATH_ID}${query}';`,
2106
- `import { fragmentEntryPath } from '${FRAGMENT_ENTRY_PATH_ID}${query}';`,
2107
- `import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap } from '${ROUTE_PRELOADS_ID}${query}';`,
2108
- `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
2109
- `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}`)};`,
2110
2194
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
2111
2195
  `const linkBridgePathsJson = globalThis.__marklessRouterLinkBridgePathsJson ?? ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)};`,
2112
2196
  `const entry = createServerEntry({`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markless/router",
3
- "version": "0.6.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/bundler": "0.6.0",
68
- "@markless/web": "0.6.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.6.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": {
@@ -9,6 +9,7 @@ export interface FragmentNavigationConfig {
9
9
  readonly replaceAttribute: string;
10
10
  readonly scrollAttribute: string;
11
11
  readonly routeScriptType: string;
12
+ readonly servedHtmlAttributesProperty: string;
12
13
  readonly retireProperty: string;
13
14
  readonly streamExecutorGlobal: string;
14
15
  readonly streamedArmSelector: string;
@@ -400,7 +401,7 @@ async function mergeHead(d: Document, next: Document) {
400
401
  const rel = element.localName === 'link' ? (element as HTMLLinkElement).rel : '';
401
402
  if (rel === 'modulepreload' || rel === 'preload') continue;
402
403
  if (rel === 'stylesheet' || element.localName === 'style') stale.push(element);
403
- else element.remove();
404
+ else if ((element as HTMLScriptElement).type !== 'importmap') element.remove();
404
405
  }
405
406
  await loaded(d.defaultView!, stylesheets);
406
407
  return { stale, scripts };
@@ -409,10 +410,13 @@ async function mergeHead(d: Document, next: Document) {
409
410
  // Only attributes a server document set are the server's to remove: scripts own the rest (a theme class).
410
411
  const serverHtmlAttributes = new WeakMap<Document, Set<string>>();
411
412
 
412
- function syncHtmlAttributes(d: Document, next: Document): void {
413
+ function syncHtmlAttributes(d: Document, next: Document, servedProperty: string): void {
413
414
  const root = d.documentElement;
414
415
  const incoming = next.documentElement;
415
- for (const name of serverHtmlAttributes.get(d) ?? [])
416
+ const served =
417
+ serverHtmlAttributes.get(d) ??
418
+ ((root as unknown as Record<string, readonly string[] | undefined>)[servedProperty] || []);
419
+ for (const name of served)
416
420
  if (!incoming.hasAttribute(name)) root.removeAttribute(name);
417
421
  for (const attribute of incoming.attributes)
418
422
  if (root.getAttribute(attribute.name) !== attribute.value)
@@ -617,6 +621,53 @@ async function streamAppends(
617
621
  return signal.aborted ? undefined : all;
618
622
  }
619
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
+
620
671
  export async function navigate(
621
672
  d: Document,
622
673
  url: URL,
@@ -643,13 +694,16 @@ export async function navigate(
643
694
  return;
644
695
  }
645
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;
646
700
  const next = fetched.doc;
647
701
  const documentLoads = state.config.documentLoads;
648
702
  const incoming = documentLoads ? next.body : regionOf(state, next);
649
703
  const outgoing = documentLoads ? d.body : regionOf(state, d);
650
704
  const scope = documentLoads && !isContainer(d.body) ? d.body : outgoing?.parentElement;
651
705
  if (!incoming || !outgoing || !scope) {
652
- fallback(fetched.url);
706
+ fallback(landed.href);
653
707
  return;
654
708
  }
655
709
  // The destination shows once its own code has arrived, so its first interaction never waits on a fetch.
@@ -682,8 +736,8 @@ export async function navigate(
682
736
  w.history.scrollRestoration = 'manual';
683
737
  session.current = entryId();
684
738
  const entry = { __marklessFragment: { id: session.current, x: 0, y: 0 } };
685
- if (mode === 'push') w.history.pushState(entry, '', fetched.url);
686
- 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);
687
741
  }
688
742
  const offsets: ScrolledBox[] = [];
689
743
  for (const element of state.scrolled) {
@@ -702,10 +756,12 @@ export async function navigate(
702
756
  (w as unknown as Record<string, unknown>)[state.config.streamExecutorGlobal] = undefined;
703
757
  for (const element of d.querySelectorAll(state.config.streamedArmSelector)) element.remove();
704
758
  state.shown = pageKey(new URL(w.location.href));
705
- if (documentLoads) syncHtmlAttributes(d, next);
759
+ if (documentLoads) syncHtmlAttributes(d, next, state.config.servedHtmlAttributesProperty);
706
760
  let region: Element;
707
761
  let end: Comment;
762
+ let start: Comment | undefined;
708
763
  let scripts: Element[];
764
+ let held: HeldPage | undefined;
709
765
  if (documentLoads && !isContainer(d.body)) {
710
766
  const fresh: Element[] = [];
711
767
  region = d.body;
@@ -727,7 +783,7 @@ export async function navigate(
727
783
  const fresh: Element[] = [];
728
784
  if (documentLoads) morphChildren(d, kept, arriving, fresh);
729
785
  const leavingStart = pageStart(outgoing, state.config.pageStartComment);
730
- let start = pageStart(region, state.config.pageStartComment);
786
+ start = pageStart(region, state.config.pageStartComment);
731
787
  if (leavingStart || start) {
732
788
  start ??= region.insertBefore(
733
789
  d.createComment(state.config.pageStartComment),
@@ -742,16 +798,25 @@ export async function navigate(
742
798
  }
743
799
  scripts = [...scriptsIn(region), ...fresh.flatMap(scriptsIn)];
744
800
  region.append(kept);
801
+ held = holdPage(region, start, end);
745
802
  outgoing.replaceWith(region);
746
803
  // The document's own nodes moved to the new element; focus inside them stays put.
747
804
  if (focused && focused !== d.activeElement && region.contains(focused))
748
805
  focused.focus({ preventScroll: true });
749
806
  } else {
750
807
  scripts = scriptsIn(region);
808
+ held = holdPage(region, undefined, null);
751
809
  outgoing.replaceWith(region);
752
810
  region.after((end = d.createComment('')));
753
811
  }
754
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
+ }
755
820
  // 'instant': a `scroll-behavior: smooth` page would otherwise animate every landing.
756
821
  for (const box of offsets)
757
822
  sameBox(scope, box)?.scrollTo({ top: box.top, left: box.left, behavior: 'instant' });
@@ -769,7 +834,7 @@ export async function navigate(
769
834
  if (restore === 'reset') {
770
835
  const target = url.hash ? d.getElementById(decodeURIComponent(url.hash.slice(1))) : null;
771
836
  if (target) target.scrollIntoView({ behavior: 'instant' });
772
- else w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
837
+ else if (!(filled instanceof Promise)) w.scrollTo({ top: 0, left: 0, behavior: 'instant' });
773
838
  } else if (restore !== 'manual')
774
839
  w.scrollTo({ top: restore.y, left: restore.x, behavior: 'instant' });
775
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 };