@markless/router 0.1.1 → 0.2.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,10 +1,13 @@
1
- import { n as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-OTaWQ_TC.js";
1
+ import { t as __marklessDebugBootstrapSource } from "../../debug-channel-C7v9u5SM.js";
2
+ import { i as normalizeRouteFileId, n as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-DPgGIyrS.js";
2
3
  import { renderToString } from "@markless/web/render-to-string";
3
4
  import { renderToStream } from "@markless/web/render-to-stream";
5
+ import { normalizeMarklessDevError, renderMarklessDevErrorDocument } from "@markless/bundler/dev-error";
4
6
  //#region src/vite/runtime/create-server-entry.ts
5
7
  const DOCUMENT_CHILDREN_PLACEHOLDER = "__markless_router_document_children__";
6
8
  function createServerEntry(options) {
7
9
  const manifest = buildRouteManifestFromFileIds(options.routeFileIds);
10
+ assertCurrentRouteAssets(options);
8
11
  async function fetch(request) {
9
12
  const url = new URL(request.url);
10
13
  if (isNitroApiPathname(url.pathname)) return new Response("Not found", { status: 404 });
@@ -14,7 +17,37 @@ function createServerEntry(options) {
14
17
  return await renderPage(url, match.route.file, match.params, 200);
15
18
  } catch (error) {
16
19
  console.error("[markless-router] page render failed:", error);
17
- return renderStatusPage(url, manifest.statusPages.error, 500, "Internal Server Error");
20
+ if (options.dev) return renderDevelopmentError(url, error);
21
+ try {
22
+ return await renderStatusPage(url, manifest.statusPages.error, 500, "Internal Server Error");
23
+ } catch (errorPageError) {
24
+ console.error("[markless-router] error page render failed:", errorPageError);
25
+ return new Response("Internal Server Error", {
26
+ status: 500,
27
+ headers: { "content-type": "text/plain;charset=utf-8" }
28
+ });
29
+ }
30
+ }
31
+ }
32
+ function renderDevelopmentError(url, error) {
33
+ const headers = {
34
+ "cache-control": "no-store",
35
+ "content-type": "text/html;charset=utf-8"
36
+ };
37
+ try {
38
+ const payload = normalizeMarklessDevError(error, { id: `navigation:${url.pathname}` });
39
+ return new Response(renderMarklessDevErrorDocument(payload), {
40
+ status: 500,
41
+ headers
42
+ });
43
+ } catch {
44
+ return new Response(escapeHtml(flattenError(error)), {
45
+ status: 500,
46
+ headers: {
47
+ ...headers,
48
+ "content-type": "text/plain;charset=utf-8"
49
+ }
50
+ });
18
51
  }
19
52
  }
20
53
  async function renderStatusPage(url, file, status, fallbackText) {
@@ -46,14 +79,14 @@ function createServerEntry(options) {
46
79
  headers
47
80
  });
48
81
  }
49
- const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath);
82
+ const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath, options.routeStylesheets?.[file]);
50
83
  const renderOptions = {
51
84
  props: pageProps,
52
85
  resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
53
86
  modulePreloads: (html) => modulePreloadsForPage(file, html, pageProps.url.href, manifest, options.routeModulePreloads, options.routeSsrModulePreloads)
54
87
  };
55
88
  if (options.render === "blocking") {
56
- const pageHtml = splitLeadingModulePreloadLinks(await renderToString(pageArtifact, renderOptions));
89
+ const pageHtml = splitLeadingHeadHtml(await renderToString(pageArtifact, renderOptions));
57
90
  const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
58
91
  return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
59
92
  status,
@@ -61,7 +94,7 @@ function createServerEntry(options) {
61
94
  });
62
95
  }
63
96
  const stream = await renderToStream(pageArtifact, renderOptions);
64
- const pageHtml = splitLeadingModulePreloadLinks(stream.shell);
97
+ const pageHtml = splitLeadingHeadHtml(stream.shell);
65
98
  const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
66
99
  if (stream.pendingArmCount === 0) return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
67
100
  status,
@@ -90,9 +123,38 @@ function createServerEntry(options) {
90
123
  }
91
124
  return { fetch };
92
125
  }
93
- function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath) {
126
+ function flattenError(error) {
127
+ if (error instanceof Error) return error.message;
128
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") return error.message;
129
+ return String(error);
130
+ }
131
+ function assertCurrentRouteAssets(options) {
132
+ if (options.routeStylesheets === void 0) return;
133
+ const routeMaps = [
134
+ ["navigation", options.routeModulePreloads ?? {}],
135
+ ["SSR", options.routeSsrModulePreloads ?? {}],
136
+ ["style", options.routeStylesheets]
137
+ ];
138
+ const expected = [...new Set(options.routeFileIds.map(normalizeRouteFileId))].filter((file) => /^pages\/.+\.(?:tsrx|mdx)$/.test(file)).toSorted();
139
+ for (const [label, routes] of routeMaps) {
140
+ const actual = Object.keys(routes).toSorted();
141
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`Markless Router ${label} client-asset routes are stale; expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}. Rebuild the client environment before the server environment.`);
142
+ }
143
+ }
144
+ function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath, stylesheetHrefs) {
145
+ const headInjections = [...baseArtifact?.headInjections ?? [], ...(stylesheetHrefs ?? []).map((href) => ({
146
+ tag: "link",
147
+ location: "head",
148
+ attributes: {
149
+ rel: "stylesheet",
150
+ href
151
+ }
152
+ }))];
94
153
  return {
95
154
  resumeModuleUrl: baseArtifact?.resumeModuleUrl,
155
+ inlineResumerSources: baseArtifact?.inlineResumerSources,
156
+ storageSeeds: baseArtifact?.storageSeeds,
157
+ ...headInjections.length > 0 ? { headInjections } : {},
96
158
  async renderSsr(renderProps, renderContext) {
97
159
  const output = await renderSsr(renderProps ?? pageProps, renderContext);
98
160
  if (!output) return { html: "" };
@@ -156,20 +218,15 @@ function addModulePreloads(preloads, seen, items) {
156
218
  preloads.push(preload);
157
219
  }
158
220
  }
159
- function splitLeadingModulePreloadLinks(html) {
160
- let bodyHtml = html;
161
- const links = [];
162
- while (bodyHtml.startsWith("<link ")) {
163
- const end = bodyHtml.indexOf(">");
164
- if (end === -1) break;
165
- const link = bodyHtml.slice(0, end + 1);
166
- if (!link.includes("rel=\"modulepreload\"")) break;
167
- links.push(link);
168
- bodyHtml = bodyHtml.slice(end + 1);
169
- }
221
+ function splitLeadingHeadHtml(html) {
222
+ const containerStart = html.indexOf("<div data-async-container");
223
+ if (containerStart <= 0) return {
224
+ bodyHtml: html,
225
+ headHtml: ""
226
+ };
170
227
  return {
171
- bodyHtml,
172
- headHtml: links.join("")
228
+ bodyHtml: html.slice(containerStart),
229
+ headHtml: html.slice(0, containerStart)
173
230
  };
174
231
  }
175
232
  async function renderDocumentShell(documentModule, pageProps, headHtml) {
@@ -216,7 +273,20 @@ function renderRouteScript(file) {
216
273
  return `<script type="@markless/core/route">${escapeScriptJson({ file })}<\/script>`;
217
274
  }
218
275
  function renderLinkBridgeScript(resumeEntryPath) {
219
- return `<script data-markless-router-link-resumer>${escapeInlineScript(`(() => {
276
+ const debugBootstrap = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `<script data-markless-router-debug-bootstrap>${escapeInlineScript(`(() => {
277
+ const d = document;
278
+ const s = d.currentScript;
279
+ const r = s && s.closest('[data-async-container]');
280
+ if (!r) return;
281
+ try { globalThis[Symbol.for('markless.debug.channel.v1.bootstrap')] = ${__marklessDebugBootstrapSource()}(r, 'ssr-inline', false); } catch {}
282
+ })();`)}<\/script>` : "";
283
+ const debugRegistration = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `
284
+ try {
285
+ const key = Symbol.for('markless.debug.channel.v1.bootstrap');
286
+ const md = globalThis[key];
287
+ if (md) { md.router('ssr-link-bridge'); md.activate(); }
288
+ } catch {}` : "";
289
+ return `${debugBootstrap}<script data-markless-router-link-resumer>${escapeInlineScript(`(() => {
220
290
  const d = document;
221
291
  const s = d.currentScript;
222
292
  const r = s && s.closest('[data-async-container]');
@@ -266,7 +336,7 @@ function renderLinkBridgeScript(resumeEntryPath) {
266
336
  setTimeout(() => { throw error; });
267
337
  location.assign(url.href);
268
338
  }
269
- }, true);
339
+ }, true);${debugRegistration}
270
340
  })();`)}<\/script>`;
271
341
  }
272
342
  function htmlAttributes(documentModule, pageProps) {
package/dist/vite.js CHANGED
@@ -1,12 +1,13 @@
1
- import { t as buildRouteManifestFromFileIds } from "./route-manifest-OTaWQ_TC.js";
2
- import { r as transformRequestFileSource } from "./request-files-DTkeaKxU.js";
3
- import { dirname, extname, isAbsolute, join, normalize, relative } from "pathe";
1
+ import { t as buildRouteManifestFromFileIds } from "./route-manifest-DPgGIyrS.js";
2
+ import { r as transformRequestFileSource } from "./request-files-C-0LLSsx.js";
3
+ import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
4
4
  import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withoutLeadingSlash } from "ufo";
5
5
  import { existsSync } from "node:fs";
6
6
  import { nitro } from "nitro/vite";
7
7
  import { sortUserPlugins } from "vite";
8
8
  import { symbolVirtualModuleSourceFile } from "@markless/bundler/preload";
9
9
  import { parseModule } from "@tsrx/core";
10
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
10
11
  import { toHtml } from "hast-util-to-html";
11
12
  import { HastReader, createMdxHastHandle, defineHastPlugin, dropHandle, materializeHastTree, resolveHastSubscriptions, serializeHandle, visitHastHandle } from "satteri";
12
13
  //#region src/route-types.ts
@@ -371,6 +372,169 @@ function fail$1(message) {
371
372
  throw new Error(message);
372
373
  }
373
374
  //#endregion
375
+ //#region src/vite/client-assets-manifest.ts
376
+ const MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST = ".vite/markless-router-client-manifest.json";
377
+ const MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION = 1;
378
+ const VALID_ROUTE_FILE = /^pages\/.+\.(?:tsrx|mdx)$/;
379
+ const ABSOLUTE_OR_PROTOCOL_RELATIVE_URL = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i;
380
+ function createClientAssetsManifest(input) {
381
+ return {
382
+ version: MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION,
383
+ base: input.base,
384
+ entries: {
385
+ resume: input.resumeEntry,
386
+ navigation: input.navigationEntry
387
+ },
388
+ routes: {
389
+ navigation: sortRouteMap(input.routes.navigation),
390
+ ssr: sortRouteMap(input.routes.ssr),
391
+ styles: sortRouteMap(input.routes.styles)
392
+ }
393
+ };
394
+ }
395
+ async function removeClientAssetsManifest(clientOutDir) {
396
+ const path = clientAssetsManifestPath(clientOutDir);
397
+ await Promise.all([rm(path, { force: true }), rm(clientAssetsManifestTemporaryPath(clientOutDir), { force: true })]);
398
+ }
399
+ async function writeClientAssetsManifest(clientOutDir, manifest) {
400
+ const validated = await validateClientAssetsManifest(manifest, manifest.base, clientOutDir);
401
+ const path = clientAssetsManifestPath(clientOutDir);
402
+ const temporaryPath = clientAssetsManifestTemporaryPath(clientOutDir);
403
+ await mkdir(dirname(path), { recursive: true });
404
+ await writeFile(temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
405
+ await rename(temporaryPath, path);
406
+ }
407
+ async function readClientAssetsManifest(clientOutDir, base) {
408
+ const path = clientAssetsManifestPath(clientOutDir);
409
+ let source;
410
+ try {
411
+ source = await readFile(path, "utf8");
412
+ } catch (error) {
413
+ throw clientAssetsManifestError(`is missing at ${path}. Build the client environment before the server environment`, error);
414
+ }
415
+ let parsed;
416
+ try {
417
+ parsed = JSON.parse(source);
418
+ } catch (error) {
419
+ throw clientAssetsManifestError(`is malformed at ${path}`, error);
420
+ }
421
+ return validateClientAssetsManifest(parsed, base, clientOutDir);
422
+ }
423
+ function clientAssetFileName(href, base) {
424
+ const dummyOrigin = "https://markless-router.invalid/";
425
+ if (hasControlCharacter(href) || !ABSOLUTE_OR_PROTOCOL_RELATIVE_URL.test(base) && ABSOLUTE_OR_PROTOCOL_RELATIVE_URL.test(href)) throw clientAssetsManifestError(`contains an asset URL outside base ${JSON.stringify(base)}: ${JSON.stringify(href)}`);
426
+ let baseUrl;
427
+ let assetUrl;
428
+ try {
429
+ baseUrl = new URL(base || "/", dummyOrigin);
430
+ assetUrl = new URL(href, baseUrl);
431
+ } catch (error) {
432
+ throw clientAssetsManifestError(`contains an invalid asset URL ${JSON.stringify(href)}`, error);
433
+ }
434
+ if (assetUrl.origin !== baseUrl.origin || assetUrl.search || assetUrl.hash) throw clientAssetsManifestError(`contains an asset URL outside base ${JSON.stringify(base)}: ${JSON.stringify(href)}`);
435
+ const basePath = baseUrl.pathname.endsWith("/") ? baseUrl.pathname : `${baseUrl.pathname}/`;
436
+ if (!assetUrl.pathname.startsWith(basePath)) throw clientAssetsManifestError(`contains an asset URL outside base ${JSON.stringify(base)}: ${JSON.stringify(href)}`);
437
+ let fileName;
438
+ try {
439
+ fileName = decodeURIComponent(assetUrl.pathname.slice(basePath.length));
440
+ } catch (error) {
441
+ throw clientAssetsManifestError(`contains an invalid encoded asset URL ${JSON.stringify(href)}`, error);
442
+ }
443
+ const normalized = normalize(fileName);
444
+ if (fileName === "" || hasControlCharacter(fileName) || fileName.includes("\\") || isAbsolute(fileName) || normalized !== fileName || normalized === ".." || normalized.startsWith("../")) throw clientAssetsManifestError(`contains an unsafe asset path ${JSON.stringify(href)}`);
445
+ if (href !== joinURL(base, fileName)) throw clientAssetsManifestError(`contains a non-canonical asset URL ${JSON.stringify(href)} for base ${JSON.stringify(base)}`);
446
+ return fileName;
447
+ }
448
+ function clientAssetsManifestPath(clientOutDir) {
449
+ return join(clientOutDir, MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST);
450
+ }
451
+ function clientAssetsManifestTemporaryPath(clientOutDir) {
452
+ return `${clientAssetsManifestPath(clientOutDir)}.tmp`;
453
+ }
454
+ async function validateClientAssetsManifest(input, base, clientOutDir) {
455
+ if (!isRecord$1(input)) throw clientAssetsManifestError("must be a JSON object");
456
+ if (input.version !== MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION) throw clientAssetsManifestError(`has unsupported version ${JSON.stringify(input.version)}; expected ${MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION}`);
457
+ if (input.base !== base) throw clientAssetsManifestError(`was built for base ${JSON.stringify(input.base)}; expected ${JSON.stringify(base)}`);
458
+ if (!isRecord$1(input.entries)) throw clientAssetsManifestError("has invalid entries");
459
+ if (!isRecord$1(input.routes)) throw clientAssetsManifestError("has invalid route maps");
460
+ const entries = {
461
+ resume: requiredString(input.entries.resume, "entries.resume"),
462
+ navigation: requiredString(input.entries.navigation, "entries.navigation")
463
+ };
464
+ const routes = {
465
+ navigation: routeMap(input.routes.navigation, "routes.navigation"),
466
+ ssr: routeMap(input.routes.ssr, "routes.ssr"),
467
+ styles: routeMap(input.routes.styles, "routes.styles")
468
+ };
469
+ assertMatchingRouteKeys(routes);
470
+ const hrefs = new Set([
471
+ entries.resume,
472
+ entries.navigation,
473
+ ...Object.values(routes.navigation).flat(),
474
+ ...Object.values(routes.ssr).flat(),
475
+ ...Object.values(routes.styles).flat()
476
+ ]);
477
+ for (const href of hrefs) {
478
+ const assetPath = join(clientOutDir, clientAssetFileName(href, base));
479
+ const relativeAssetPath = relative(clientOutDir, assetPath);
480
+ if (relativeAssetPath === ".." || relativeAssetPath.startsWith("../") || isAbsolute(relativeAssetPath)) throw clientAssetsManifestError(`contains an unsafe asset path ${JSON.stringify(href)}`);
481
+ try {
482
+ if (!(await stat(assetPath)).isFile()) throw new Error("not a file");
483
+ } catch (error) {
484
+ throw clientAssetsManifestError(`references missing client asset ${JSON.stringify(href)}`, error);
485
+ }
486
+ }
487
+ return {
488
+ version: MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION,
489
+ base,
490
+ entries,
491
+ routes
492
+ };
493
+ }
494
+ function routeMap(input, label) {
495
+ if (!isRecord$1(input)) throw clientAssetsManifestError(`has invalid ${label}`);
496
+ const routes = {};
497
+ for (const [routeFile, hrefs] of Object.entries(input)) {
498
+ let decodedRouteFile;
499
+ try {
500
+ decodedRouteFile = decodeURIComponent(routeFile);
501
+ } catch (error) {
502
+ throw clientAssetsManifestError(`contains invalid route key ${JSON.stringify(routeFile)}`, error);
503
+ }
504
+ if (!VALID_ROUTE_FILE.test(routeFile) || hasControlCharacter(routeFile) || decodedRouteFile !== routeFile || routeFile.includes("\\") || normalize(routeFile) !== routeFile) throw clientAssetsManifestError(`contains invalid route key ${JSON.stringify(routeFile)}`);
505
+ if (!Array.isArray(hrefs) || !hrefs.every((href) => typeof href === "string")) throw clientAssetsManifestError(`has invalid assets for route ${JSON.stringify(routeFile)}`);
506
+ routes[routeFile] = hrefs;
507
+ }
508
+ return routes;
509
+ }
510
+ function assertMatchingRouteKeys(routes) {
511
+ const navigationKeys = Object.keys(routes.navigation).toSorted();
512
+ for (const [label, routeMap] of [["SSR", routes.ssr], ["style", routes.styles]]) {
513
+ const keys = Object.keys(routeMap).toSorted();
514
+ if (JSON.stringify(keys) !== JSON.stringify(navigationKeys)) throw clientAssetsManifestError(`${label} route keys do not match navigation route keys`);
515
+ }
516
+ }
517
+ function sortRouteMap(routes) {
518
+ return Object.fromEntries(Object.entries(routes).toSorted(([left], [right]) => left.localeCompare(right)).map(([routeFile, hrefs]) => [routeFile, [...hrefs]]));
519
+ }
520
+ function requiredString(input, label) {
521
+ if (typeof input !== "string" || input === "") throw clientAssetsManifestError(`has invalid ${label}`);
522
+ return input;
523
+ }
524
+ function hasControlCharacter(input) {
525
+ for (let index = 0; index < input.length; index++) {
526
+ const code = input.charCodeAt(index);
527
+ if (code <= 31 || code === 127) return true;
528
+ }
529
+ return false;
530
+ }
531
+ function isRecord$1(input) {
532
+ return typeof input === "object" && input !== null && !Array.isArray(input);
533
+ }
534
+ function clientAssetsManifestError(message, cause) {
535
+ return new Error(`Markless Router client-assets manifest ${message}.`, { cause });
536
+ }
537
+ //#endregion
374
538
  //#region src/vite/html-transform.ts
375
539
  const HELPER = "__marklessRouterHtmlAttributes";
376
540
  const SAFE_GLOBALS = new Set([
@@ -912,9 +1076,16 @@ function devSourceModuleRequestPlugin() {
912
1076
  };
913
1077
  }
914
1078
  function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry, routePreloads) {
1079
+ let clientOutDir = "";
1080
+ let clientEnvironmentName = "";
1081
+ let clientEnvironmentConfig;
1082
+ let serverEnvironmentConfig;
1083
+ let productionBuild = false;
1084
+ let pendingClientAssets;
915
1085
  return {
916
1086
  name: "markless-router:vite",
917
1087
  enforce: "pre",
1088
+ sharedDuringBuild: true,
918
1089
  config(config) {
919
1090
  throwIfUserAddedNitro(config.plugins, nitroPluginsFromRouter);
920
1091
  config.environments ??= {};
@@ -932,6 +1103,16 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
932
1103
  };
933
1104
  },
934
1105
  configResolved(config) {
1106
+ productionBuild = config.command === "build";
1107
+ if (productionBuild) {
1108
+ const clientEnvironments = Object.entries(config.environments).filter(([, environment]) => environment.consumer === "client");
1109
+ if (clientEnvironments.length !== 1) throw new Error(`Markless Router requires exactly one client build environment; found ${clientEnvironments.length}.`);
1110
+ const [name, environment] = clientEnvironments[0];
1111
+ clientEnvironmentName = name;
1112
+ clientEnvironmentConfig = environment;
1113
+ serverEnvironmentConfig = config.environments.ssr;
1114
+ clientOutDir = resolve(config.root, environment.build.outDir);
1115
+ }
935
1116
  for (const entry of [
936
1117
  resumeEntry,
937
1118
  navigationEntry,
@@ -942,20 +1123,65 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
942
1123
  configEnvironment(_name, config) {
943
1124
  configureRouteInputs(config);
944
1125
  },
945
- generateBundle(_options, bundle) {
946
- if (this.environment?.config.consumer !== "client") return;
947
- const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
948
- if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
949
- const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
950
- if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
951
- routePreloads.routes = routeModulePreloadsFromBundle({
952
- base: routePreloads.base,
953
- bundle,
954
- navigationChunk,
955
- resumeChunk,
956
- root: routePreloads.root
957
- });
958
- patchRoutePreloadsInBundle(bundle, routePreloads.routes);
1126
+ async buildStart() {
1127
+ if (!productionBuild) return;
1128
+ if (!clientOutDir) throw new Error("Markless Router could not resolve the client output directory for its client-assets manifest.");
1129
+ const environment = this.environment;
1130
+ const isClientEnvironment = environment?.name ? environment.name === clientEnvironmentName : environment?.config === clientEnvironmentConfig;
1131
+ const isRouterServerEnvironment = environment?.name ? environment.name === "ssr" : environment?.config === serverEnvironmentConfig;
1132
+ if (isClientEnvironment) {
1133
+ pendingClientAssets = void 0;
1134
+ resumeEntry.fileName = void 0;
1135
+ navigationEntry.fileName = void 0;
1136
+ routePreloads.routes = {
1137
+ navigation: {},
1138
+ ssr: {},
1139
+ styles: {}
1140
+ };
1141
+ routePreloads.persisted = false;
1142
+ await removeClientAssetsManifest(clientOutDir);
1143
+ return;
1144
+ }
1145
+ if (!isRouterServerEnvironment) return;
1146
+ const manifest = await readClientAssetsManifest(clientOutDir, routePreloads.base);
1147
+ resumeEntry.fileName = clientAssetFileName(manifest.entries.resume, manifest.base);
1148
+ navigationEntry.fileName = clientAssetFileName(manifest.entries.navigation, manifest.base);
1149
+ routePreloads.routes = manifest.routes;
1150
+ routePreloads.persisted = true;
1151
+ },
1152
+ generateBundle: {
1153
+ order: "post",
1154
+ handler(_options, bundle) {
1155
+ if (this.environment?.config.consumer !== "client") return;
1156
+ const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
1157
+ if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
1158
+ const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
1159
+ if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
1160
+ routePreloads.routes = routeModulePreloadsFromBundle({
1161
+ base: routePreloads.base,
1162
+ bundle,
1163
+ navigationChunk,
1164
+ resumeChunk,
1165
+ root: routePreloads.root
1166
+ });
1167
+ patchRoutePreloadsInBundle(bundle, routePreloads.routes);
1168
+ if (productionBuild) {
1169
+ if (!resumeEntry.fileName || !navigationEntry.fileName) throw new Error("Markless Router client build did not emit its resume and navigation entries.");
1170
+ pendingClientAssets = createClientAssetsManifest({
1171
+ base: routePreloads.base,
1172
+ resumeEntry: joinURL(routePreloads.base, resumeEntry.fileName),
1173
+ navigationEntry: joinURL(routePreloads.base, navigationEntry.fileName),
1174
+ routes: routePreloads.routes
1175
+ });
1176
+ }
1177
+ }
1178
+ },
1179
+ async writeBundle() {
1180
+ const environment = this.environment;
1181
+ const isClientEnvironment = environment?.name ? environment.name === clientEnvironmentName : environment?.config === clientEnvironmentConfig;
1182
+ if (!productionBuild || !isClientEnvironment) return;
1183
+ if (!pendingClientAssets) throw new Error("Markless Router client build finished without a client-assets manifest.");
1184
+ await writeClientAssetsManifest(clientOutDir, pendingClientAssets);
959
1185
  }
960
1186
  };
961
1187
  }
@@ -1083,10 +1309,12 @@ function resumeEntryState() {
1083
1309
  function routePreloadState() {
1084
1310
  return {
1085
1311
  base: "/",
1312
+ persisted: false,
1086
1313
  root: ".",
1087
1314
  routes: {
1088
1315
  navigation: {},
1089
- ssr: {}
1316
+ ssr: {},
1317
+ styles: {}
1090
1318
  }
1091
1319
  };
1092
1320
  }
@@ -1113,6 +1341,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
1113
1341
  let root = "";
1114
1342
  return {
1115
1343
  name: "markless-router:routes",
1344
+ sharedDuringBuild: true,
1116
1345
  configResolved(config) {
1117
1346
  root = config.root;
1118
1347
  },
@@ -1130,7 +1359,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
1130
1359
  if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root);
1131
1360
  if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
1132
1361
  if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
1133
- if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads);
1362
+ if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
1134
1363
  if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) return "export const routerMode = \"path\"; // retained for compat; hash paths are always first-class";
1135
1364
  }
1136
1365
  };
@@ -1145,14 +1374,16 @@ function serverEntrySource(root) {
1145
1374
  `import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';`,
1146
1375
  `import { resumeEntryPath } from '${RESUME_ENTRY_PATH_ID}${query}';`,
1147
1376
  `import { navigationEntryPath } from '${NAVIGATION_ENTRY_PATH_ID}${query}';`,
1148
- `import { routeModulePreloads, routeSsrModulePreloads } from '${ROUTE_PRELOADS_ID}${query}';`,
1377
+ `import { routeModulePreloads, routeSsrModulePreloads, routeStylesheets } from '${ROUTE_PRELOADS_ID}${query}';`,
1149
1378
  `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
1150
1379
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
1151
1380
  `const entry = createServerEntry({`,
1381
+ ` dev: import.meta.env.DEV,`,
1152
1382
  ` resumeEntryPath,`,
1153
1383
  ` navigationEntryPath,`,
1154
1384
  ` routeModulePreloads,`,
1155
1385
  ` routeSsrModulePreloads,`,
1386
+ ` routeStylesheets,`,
1156
1387
  ` documentModuleLoader: documentModuleLoaders['/document.tsrx'],`,
1157
1388
  ` pageModuleLoaders,`,
1158
1389
  ` routeFileIds,`,
@@ -1166,14 +1397,20 @@ function rootScopeQuery(root, id = "") {
1166
1397
  ...parseQuery(parsePath(id).search),
1167
1398
  "markless-router-root": root || "."
1168
1399
  });
1169
- return query ? `?${query}` : "";
1170
- }
1171
- function routePreloadsSource(state) {
1400
+ return query ? `?${query}&lang.ts` : "";
1401
+ }
1402
+ function routePreloadsSource(state, client) {
1403
+ const routes = client || !state.persisted ? {
1404
+ navigation: state.routes.navigation,
1405
+ ssr: state.routes.ssr
1406
+ } : state.routes;
1407
+ const routeStylesheetExport = client ? [] : [`export const routeStylesheets = ${state.persisted ? "routePreloadData.styles ?? {}" : "undefined"};`];
1172
1408
  return [
1173
1409
  `const routePreloadsJson = globalThis.__marklessRouterRoutePreloadsJson ?? ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)};`,
1174
- `const routePreloadData = routePreloadsJson === ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)} ? ${JSON.stringify(state.routes)} : JSON.parse(routePreloadsJson);`,
1410
+ `const routePreloadData = routePreloadsJson === ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)} ? ${JSON.stringify(routes)} : JSON.parse(routePreloadsJson);`,
1175
1411
  `export const routeModulePreloads = routePreloadData.navigation ?? {};`,
1176
1412
  `export const routeSsrModulePreloads = routePreloadData.ssr ?? {};`,
1413
+ ...routeStylesheetExport,
1177
1414
  `export function preloadRouteModule(file, document = globalThis.document) {`,
1178
1415
  ` const hrefs = routeModulePreloads[file];`,
1179
1416
  ` if (!hrefs?.length || !document?.head) return [];`,
@@ -1194,7 +1431,10 @@ function routePreloadsSource(state) {
1194
1431
  ].join("\n");
1195
1432
  }
1196
1433
  function patchRoutePreloadsInBundle(bundle, routes) {
1197
- const replacement = jsStringLiteralContent(JSON.stringify(routes));
1434
+ const replacement = jsStringLiteralContent(JSON.stringify({
1435
+ navigation: routes.navigation,
1436
+ ssr: routes.ssr
1437
+ }));
1198
1438
  for (const chunk of outputChunks(bundle)) {
1199
1439
  if (!chunk.code?.includes(ROUTE_PRELOADS_PLACEHOLDER)) continue;
1200
1440
  chunk.code = chunk.code.replace(ROUTE_PRELOADS_PLACEHOLDER, replacement);
@@ -1211,13 +1451,17 @@ function routeModulePreloadsFromBundle(input) {
1211
1451
  for (const chunk of chunks) {
1212
1452
  const routeFile = routeFileForChunk(input.root, chunk);
1213
1453
  if (!routeFile) continue;
1214
- routeChunks.set(routeFile, chunk);
1454
+ const routeFileChunks = routeChunks.get(routeFile) ?? [];
1455
+ routeFileChunks.push(chunk);
1456
+ routeChunks.set(routeFile, routeFileChunks);
1215
1457
  routeChunkFileNames.add(chunk.fileName);
1216
1458
  }
1217
1459
  const symbolChunks = symbolChunksBySourceFile(chunks);
1218
1460
  const navigation = {};
1219
1461
  const ssr = {};
1220
- for (const [routeFile, routeChunk] of routeChunks) {
1462
+ const styles = {};
1463
+ for (const [routeFile, routeFileChunks] of routeChunks) {
1464
+ const routeChunk = primaryRouteChunk(input.root, routeFile, routeFileChunks);
1221
1465
  const navigationFileNames = /* @__PURE__ */ new Set();
1222
1466
  if (input.navigationChunk) {
1223
1467
  includeChunk(navigationFileNames, chunksByFileName, input.navigationChunk.fileName);
@@ -1242,11 +1486,28 @@ function routeModulePreloadsFromBundle(input) {
1242
1486
  }
1243
1487
  navigation[routeFile] = [...navigationFileNames].map((fileName) => joinURL(input.base, fileName));
1244
1488
  ssr[routeFile] = [...ssrFileNames].map((fileName) => joinURL(input.base, fileName));
1489
+ styles[routeFile] = routeStylesheetsForChunk(routeChunk, chunksByFileName, input.base);
1245
1490
  }
1246
1491
  return {
1247
1492
  navigation,
1248
- ssr
1493
+ ssr,
1494
+ styles
1495
+ };
1496
+ }
1497
+ function routeStylesheetsForChunk(routeChunk, chunksByFileName, base) {
1498
+ const styles = /* @__PURE__ */ new Set();
1499
+ const visited = /* @__PURE__ */ new Set();
1500
+ const visit = (chunk) => {
1501
+ if (visited.has(chunk.fileName)) return;
1502
+ visited.add(chunk.fileName);
1503
+ for (const imported of [...chunk.imports, ...codeStaticImports(chunk)]) {
1504
+ const dependency = chunksByFileName.get(imported);
1505
+ if (dependency) visit(dependency);
1506
+ }
1507
+ for (const stylesheet of chunk.viteMetadata?.importedCss ?? []) styles.add(joinURL(base, stylesheet));
1249
1508
  };
1509
+ visit(routeChunk);
1510
+ return [...styles];
1250
1511
  }
1251
1512
  function symbolChunksBySourceFile(chunks) {
1252
1513
  const bySourceFile = /* @__PURE__ */ new Map();
@@ -1334,6 +1595,28 @@ function routeFileForChunk(root, chunk) {
1334
1595
  if (routeFile) return routeFile;
1335
1596
  }
1336
1597
  }
1598
+ function primaryRouteChunk(root, routeFile, chunks) {
1599
+ let selected = chunks[0];
1600
+ let selectedRank = routeChunkRank(root, routeFile, selected);
1601
+ for (const chunk of chunks.slice(1)) {
1602
+ const rank = routeChunkRank(root, routeFile, chunk);
1603
+ if (rank <= selectedRank) continue;
1604
+ selected = chunk;
1605
+ selectedRank = rank;
1606
+ }
1607
+ return selected;
1608
+ }
1609
+ function routeChunkRank(root, routeFile, chunk) {
1610
+ let rank = 0;
1611
+ for (const moduleId of [chunk.facadeModuleId, ...chunk.moduleIds ?? []]) {
1612
+ if (routeFileForModuleId(root, moduleId) !== routeFile) continue;
1613
+ const search = parsePath(moduleId).search;
1614
+ if (/(?:^|[?&])markless-symbols(?:[=&]|$)/.test(search)) rank = Math.max(rank, 1);
1615
+ else if (/(?:^|[?&])markless-resume(?:[=&]|$)/.test(search)) rank = Math.max(rank, 2);
1616
+ else return 3;
1617
+ }
1618
+ return rank;
1619
+ }
1337
1620
  function routeFileForModuleId(root, moduleId) {
1338
1621
  if (!moduleId || moduleId.startsWith("\0")) return void 0;
1339
1622
  const pathname = decodePath(parseURL(moduleId).pathname);