@markless/router 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vite.js CHANGED
@@ -1,16 +1,20 @@
1
- import { t as buildRouteManifestFromFileIds } from "./route-manifest-DPgGIyrS.js";
2
- import { r as transformRequestFileSource } from "./request-files-C-0LLSsx.js";
3
- import { __marklessRouteHref } from "./vite/route-href.js";
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";
3
+ import { t as __marklessRouteHref } from "./route-href-CGooO_g6.js";
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";
4
6
  import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
5
7
  import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withQuery, withoutLeadingSlash } from "ufo";
6
8
  import { existsSync } from "node:fs";
7
- import { nitro } from "nitro/vite";
8
- import { sortUserPlugins } from "vite";
9
- import { symbolVirtualModuleSourceFile } from "@markless/bundler/preload";
10
- import { parseModule } from "yuku-tsrx";
11
9
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
10
+ import { nitro } from "nitro/vite";
11
+ import { minifySync, sortUserPlugins } from "vite";
12
+ import { isClientPrimarySourceRequest, isExecutionLogChunk, isMarklessNavigationPack, isRenderDataSourceRequest, isResumeSourceRequest, isRouteNavigationSourceRequest, isSymbolOnlySourceRequest, lazyStartupPackName, marklessDeferredChunkFileNames, optimizedDepsToInclude, plannedLandingFiles, resolverVirtualModuleSourceFile, startupPackOfLazy, symbolVirtualModuleSourceFile, warmEvaluatedImports } from "@markless/bundler/preload";
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";
12
15
  import { toHtml } from "hast-util-to-html";
13
16
  import { HastReader, createMdxHastHandle, defineHastPlugin, dropHandle, materializeHastTree, resolveHastSubscriptions, serializeHandle, visitHastHandle } from "satteri";
17
+ import "@markless/web/render-to-string";
14
18
  //#region src/route-types.ts
15
19
  const routeTypesEnvPath = "markless-router-env.d.ts";
16
20
  const routeTypesOutputPath = ".output/markless/router/types/routes.d.ts";
@@ -96,7 +100,7 @@ function createLinkTypes() {
96
100
  "};",
97
101
  "",
98
102
  "export type MarklessRouterNavigationProps = {",
99
- " readonly prefetch?: boolean | \"intent\" | \"viewport\";",
103
+ " readonly prefetch?: boolean;",
100
104
  " readonly replace?: boolean;",
101
105
  " readonly scroll?: boolean;",
102
106
  "};",
@@ -113,7 +117,8 @@ function createLinkTypes() {
113
117
  }
114
118
  //#endregion
115
119
  //#region src/vite/route-typegen.ts
116
- const PAGE_EXTENSIONS = new Set([".tsrx", ".mdx"]);
120
+ const PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".tsrx", ".mdx"]);
121
+ const ENDPOINT_EXTENSIONS = /* @__PURE__ */ new Set([".ts"]);
117
122
  function routeTypegenPlugin() {
118
123
  let root = "";
119
124
  return {
@@ -130,27 +135,27 @@ function routeTypegenPlugin() {
130
135
  };
131
136
  }
132
137
  async function writeRouteTypes(fs, root) {
133
- const manifest = buildRouteManifestFromFileIds(await discoverPageFiles(fs, root));
138
+ const pageFiles = await discoverPageFiles(fs, root);
139
+ const manifest = buildRouteManifestFromFileIds(pageFiles);
140
+ assertNoEndpointConflicts(manifest, await discoverFiles(fs, root, "api", ENDPOINT_EXTENSIONS));
134
141
  await writeIfChanged(fs, join(root, routeTypesEnvPath), routeTypesEnvDeclaration);
135
142
  await writeIfChanged(fs, join(root, routeTypesOutputPath), createRouteTypesDeclaration(manifest));
136
143
  }
137
- async function discoverPageFiles(fs, root) {
138
- const pagesDir = join(root, "pages");
144
+ function discoverPageFiles(fs, root) {
145
+ return discoverFiles(fs, root, "pages", PAGE_EXTENSIONS);
146
+ }
147
+ async function discoverFiles(fs, root, dir, extensions) {
139
148
  const files = [];
140
- await collectPageFiles(fs, root, pagesDir, files);
149
+ await collectFiles(fs, root, join(root, dir), extensions, files);
141
150
  return files.toSorted((left, right) => left.localeCompare(right));
142
151
  }
143
- async function collectPageFiles(fs, root, dir, files) {
152
+ async function collectFiles(fs, root, dir, extensions, files) {
144
153
  const entries = await readdirIfExists(fs, dir);
145
- await Promise.all(entries.map((entry) => collectPageFile(fs, root, dir, files, entry)));
146
- }
147
- async function collectPageFile(fs, root, dir, files, entry) {
148
- const path = join(dir, entry.name);
149
- if (entry.isDirectory()) {
150
- await collectPageFiles(fs, root, path, files);
151
- return;
152
- }
153
- if (entry.isFile() && PAGE_EXTENSIONS.has(extname(path))) files.push(withLeadingSlash(relative(root, path)));
154
+ await Promise.all(entries.map(async (entry) => {
155
+ const path = join(dir, entry.name);
156
+ if (entry.isDirectory()) await collectFiles(fs, root, path, extensions, files);
157
+ else if (entry.isFile() && extensions.has(extname(path))) files.push(withLeadingSlash(relative(root, path)));
158
+ }));
154
159
  }
155
160
  async function readdirIfExists(fs, dir) {
156
161
  try {
@@ -180,14 +185,20 @@ function isNotFoundError(error) {
180
185
  //#region src/vite/anchor-transform.ts
181
186
  const ROUTE_HREF_HELPER_ID = "virtual:markless-router/route-href";
182
187
  const JSX_FILE_FILTER = /\.(?:[jt]sx|tsrx)(?:$|\?)/;
183
- const ROUTER_IMPORT_SOURCES = new Set(["@markless/router", "@markless/core/router"]);
188
+ const REWRITE_CACHE_LIMIT = 2048;
189
+ const ROUTER_IMPORT_SOURCES = /* @__PURE__ */ new Set(["@markless/router", "@markless/core/router"]);
184
190
  function anchorTransformPlugin() {
185
191
  let root = "";
186
192
  let routePatterns = /* @__PURE__ */ new Map();
187
193
  let routesLoaded = false;
194
+ let routeFingerprint = "";
195
+ const rewrites = /* @__PURE__ */ new Map();
188
196
  const refreshRoutes = async (fs) => {
189
197
  routePatterns = routePatternMap(buildRouteManifestFromFileIds(await discoverPageFiles(fs, root)));
190
198
  routesLoaded = true;
199
+ const fingerprint = JSON.stringify([...routePatterns]);
200
+ if (fingerprint !== routeFingerprint) rewrites.clear();
201
+ routeFingerprint = fingerprint;
191
202
  };
192
203
  return {
193
204
  name: "markless-router:anchors",
@@ -197,7 +208,18 @@ function anchorTransformPlugin() {
197
208
  async handler(code, id) {
198
209
  if (!isTransformCandidateCode(code)) return;
199
210
  if (!routesLoaded) await refreshRoutes(this.fs);
200
- const transformed = transformAnchorSource(code, parseTransformAst(code, id, (source, options) => this.parse(source, options)), routePatterns);
211
+ const key = `${id.includes(".tsrx") ? "tsrx" : id.includes(".jsx") ? "jsx" : "tsx"}\0${code}`;
212
+ let transformed = rewrites.get(key);
213
+ if (transformed === void 0) {
214
+ const ast = parseTransformAst(code, id, (source, options) => this.parse(source, options));
215
+ transformed = transformAnchorSource(code, ast, routePatterns);
216
+ if (id.includes(".tsrx")) {
217
+ const linkedAst = transformed === code ? ast : parseTransformAst(transformed, id, (source, options) => this.parse(source, options));
218
+ transformed = lowerRouterLinks(transformed, linkedAst);
219
+ }
220
+ if (rewrites.size >= REWRITE_CACHE_LIMIT) rewrites.delete(rewrites.keys().next().value);
221
+ rewrites.set(key, transformed);
222
+ }
201
223
  return transformed === code ? void 0 : {
202
224
  code: transformed,
203
225
  map: null
@@ -215,6 +237,68 @@ function anchorTransformPlugin() {
215
237
  }
216
238
  };
217
239
  }
240
+ function lowerRouterLinks(code, ast) {
241
+ const linkNames = marklessRouterLinkImportNames(ast);
242
+ const edits = [];
243
+ walk(ast, (current) => {
244
+ if (current.type !== "JSXElement") return;
245
+ const opening = node$2(current.openingElement);
246
+ const name = opening && node$2(opening.name);
247
+ if (!name || !linkNames.has(jsxName$1(name) ?? "")) return;
248
+ const attributes = nodes$2(opening?.attributes);
249
+ if (attributes.some((attribute) => attribute.type === "JSXSpreadAttribute")) return;
250
+ if (findJsxAttribute(attributes, "children")) return;
251
+ const href = findJsxAttribute(attributes, "href") ? "" : " href=\"#\"";
252
+ edits.push({
253
+ ...rangeOf(name),
254
+ text: `a ${LINK_ATTRIBUTE}${href}`
255
+ });
256
+ const closingName = node$2(node$2(current.closingElement)?.name);
257
+ if (closingName) edits.push({
258
+ ...rangeOf(closingName),
259
+ text: "a"
260
+ });
261
+ for (const attribute of attributes) {
262
+ const attributeName = jsxName$1(node$2(attribute.name));
263
+ if (attributeName === "params" || attributeName === "prefetch" && !attribute.value) edits.push({
264
+ ...attributeRemovalRange(code, attribute),
265
+ text: ""
266
+ });
267
+ else if (attributeName === "prefetch") {
268
+ const value = node$2(attribute.value);
269
+ const literal = jsxExpression(value) ?? value;
270
+ if ([
271
+ "Literal",
272
+ "StringLiteral",
273
+ "BooleanLiteral"
274
+ ].includes(literal.type ?? "")) {
275
+ const choice = literal.value;
276
+ edits.push(choice === false ? {
277
+ ...rangeOf(attribute),
278
+ text: `${PREFETCH_ATTRIBUTE}="none"`
279
+ } : {
280
+ ...attributeRemovalRange(code, attribute),
281
+ text: ""
282
+ });
283
+ continue;
284
+ }
285
+ const expression = `(${slice$1(code, jsxExpression(value) ?? value)})`;
286
+ edits.push({
287
+ ...rangeOf(attribute),
288
+ text: `${PREFETCH_ATTRIBUTE}={${expression} === false ? 'none' : undefined}`
289
+ });
290
+ } else if (attributeName === "replace" || attributeName === "scroll") {
291
+ const value = node$2(attribute.value);
292
+ const expression = value ? slice$1(code, jsxExpression(value) ?? value) : "true";
293
+ edits.push({
294
+ ...rangeOf(attribute),
295
+ text: attributeName === "replace" ? `${REPLACE_ATTRIBUTE}={(${expression}) ? '' : undefined}` : `${SCROLL_ATTRIBUTE}={(${expression}) === false ? 'manual' : undefined}`
296
+ });
297
+ }
298
+ }
299
+ });
300
+ return applyEdits(code, edits);
301
+ }
218
302
  function parseTransformAst(code, id, parse) {
219
303
  if (id.includes(".tsrx")) return parseModule(code, id);
220
304
  return parse(code, {
@@ -227,24 +311,24 @@ function isTransformCandidateCode(code) {
227
311
  return code.includes("<a") || [...ROUTER_IMPORT_SOURCES].some((importSource) => code.includes(importSource));
228
312
  }
229
313
  function transformAnchorSource(code, astInput, routePatterns) {
230
- const ast = node$1(astInput);
314
+ const ast = node$2(astInput);
231
315
  if (!ast) return code;
232
316
  const edits = [];
233
317
  let needsHrefHelper = false;
234
318
  const linkNames = marklessRouterLinkImportNames(ast);
235
319
  walk(ast, (current) => {
236
320
  if (current.type !== "JSXOpeningElement") return;
237
- const name = jsxName$1(node$1(current.name));
321
+ const name = jsxName$1(node$2(current.name));
238
322
  if (name !== "a" && (!name || !linkNames.has(name))) return;
239
- const attributes = nodes$1(current.attributes);
323
+ const attributes = nodes$2(current.attributes);
240
324
  const href = findJsxAttribute(attributes, "href");
241
- const hrefValue = href ? stringLiteralValue(node$1(href.value)) : void 0;
325
+ const hrefValue = href ? stringLiteralValue(node$2(href.value)) : void 0;
242
326
  if (!href || !hrefValue || !isRoutePatternHref(hrefValue)) return;
243
327
  const routeParams = routePatterns.get(hrefValue);
244
328
  if (!routeParams) fail$1(`Typed route error: ${hrefValue} does not match any route in pages/.`);
245
329
  const params = findJsxAttribute(attributes, "params");
246
330
  if (!params) fail$1(missingParamsMessage(hrefValue, routeParams));
247
- const paramsExpression = jsxExpression(node$1(params.value));
331
+ const paramsExpression = jsxExpression(node$2(params.value));
248
332
  if (!paramsExpression) fail$1(missingParamsMessage(hrefValue, routeParams));
249
333
  validateObjectLiteralParams(hrefValue, routeParams, paramsExpression);
250
334
  const staticParams = staticObjectValue(paramsExpression);
@@ -270,11 +354,11 @@ function transformAnchorSource(code, astInput, routePatterns) {
270
354
  function staticObjectValue(expression) {
271
355
  if (expression.type !== "ObjectExpression") return void 0;
272
356
  const value = {};
273
- for (const property of nodes$1(expression.properties)) {
357
+ for (const property of nodes$2(expression.properties)) {
274
358
  if (property.type !== "Property" || property.computed === true) return void 0;
275
- const key = node$1(property.key);
359
+ const key = node$2(property.key);
276
360
  const name = key?.type === "Identifier" ? string$1(key.name) : literalString(key);
277
- const propertyValue = staticExpressionValue(node$1(property.value));
361
+ const propertyValue = staticExpressionValue(node$2(property.value));
278
362
  if (!name || !propertyValue.known) return void 0;
279
363
  value[name] = propertyValue.value;
280
364
  }
@@ -288,7 +372,7 @@ function staticExpressionValue(expression) {
288
372
  };
289
373
  if (expression.type === "ArrayExpression") {
290
374
  const values = [];
291
- for (const element of nodes$1(expression.elements)) {
375
+ for (const element of nodes$2(expression.elements)) {
292
376
  const value = staticExpressionValue(element);
293
377
  if (!value.known) return { known: false };
294
378
  values.push(value.value);
@@ -303,9 +387,9 @@ function staticExpressionValue(expression) {
303
387
  function marklessRouterLinkImportNames(ast) {
304
388
  const names = /* @__PURE__ */ new Set();
305
389
  walk(ast, (current) => {
306
- if (current.type !== "ImportDeclaration" || !ROUTER_IMPORT_SOURCES.has(literalString(node$1(current.source)) ?? "")) return;
307
- for (const specifier of nodes$1(current.specifiers)) if (specifier.type === "ImportSpecifier" && identifierName(node$1(specifier.imported)) === "Link") {
308
- const localName = identifierName(node$1(specifier.local));
390
+ if (current.type !== "ImportDeclaration" || !ROUTER_IMPORT_SOURCES.has(literalString(node$2(current.source)) ?? "")) return;
391
+ for (const specifier of nodes$2(current.specifiers)) if (specifier.type === "ImportSpecifier" && identifierName(node$2(specifier.imported)) === "Link") {
392
+ const localName = identifierName(node$2(specifier.local));
309
393
  if (localName) names.add(localName);
310
394
  }
311
395
  });
@@ -324,9 +408,9 @@ function validateObjectLiteralParams(pattern, routeParams, paramsExpression) {
324
408
  if (missing.length > 0) fail$1(`Typed route error: ${pattern} requires params:\n${list(missing)}`);
325
409
  }
326
410
  function objectLiteralKeys(expression) {
327
- return nodes$1(expression.properties).flatMap((property) => {
411
+ return nodes$2(expression.properties).flatMap((property) => {
328
412
  if (property.type !== "Property") return [];
329
- const key = node$1(property.key);
413
+ const key = node$2(property.key);
330
414
  const name = key?.type === "Identifier" ? string$1(key.name) : literalString(key);
331
415
  return name ? [name] : [];
332
416
  });
@@ -341,7 +425,7 @@ function list(items) {
341
425
  return items.map((item) => `- ${item}`).join("\n");
342
426
  }
343
427
  function findJsxAttribute(attributes, name) {
344
- return attributes.find((attr) => attr.type === "JSXAttribute" && jsxName$1(node$1(attr.name)) === name);
428
+ return attributes.find((attr) => attr.type === "JSXAttribute" && jsxName$1(node$2(attr.name)) === name);
345
429
  }
346
430
  function stringLiteralValue(value) {
347
431
  return value?.type === "Literal" ? string$1(value.value) : void 0;
@@ -354,7 +438,7 @@ function identifierName(value) {
354
438
  }
355
439
  function jsxExpression(value) {
356
440
  if (value?.type !== "JSXExpressionContainer") return;
357
- const expression = node$1(value.expression);
441
+ const expression = node$2(value.expression);
358
442
  return expression?.type === "JSXEmptyExpression" ? void 0 : expression;
359
443
  }
360
444
  function isRoutePatternHref(href) {
@@ -377,7 +461,7 @@ function applyEdits(code, edits) {
377
461
  function walk(root, visit) {
378
462
  const stack = [root];
379
463
  while (stack.length > 0) {
380
- const current = node$1(stack.pop());
464
+ const current = node$2(stack.pop());
381
465
  if (!current) continue;
382
466
  visit(current);
383
467
  for (const child of Object.values(current)) if (Array.isArray(child)) stack.push(...child);
@@ -402,10 +486,10 @@ function rangeOf(value) {
402
486
  end: value.end
403
487
  };
404
488
  }
405
- function nodes$1(value) {
406
- return Array.isArray(value) ? value.filter((item) => !!node$1(item)) : [];
489
+ function nodes$2(value) {
490
+ return Array.isArray(value) ? value.filter((item) => !!node$2(item)) : [];
407
491
  }
408
- function node$1(value) {
492
+ function node$2(value) {
409
493
  return typeof value === "object" && value !== null ? value : void 0;
410
494
  }
411
495
  function string$1(value) {
@@ -416,7 +500,7 @@ function fail$1(message) {
416
500
  }
417
501
  //#endregion
418
502
  //#region src/vite/client-assets-manifest.ts
419
- const MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST = ".vite/markless-router-client-manifest.json";
503
+ const MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST = "router-client-manifest.json";
420
504
  const MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION = 1;
421
505
  const VALID_ROUTE_FILE = /^pages\/.+\.(?:tsrx|mdx)$/;
422
506
  const ABSOLUTE_OR_PROTOCOL_RELATIVE_URL = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i;
@@ -427,7 +511,9 @@ function createClientAssetsManifest(input) {
427
511
  entries: {
428
512
  resume: input.resumeEntry,
429
513
  ...input.prerenderWakeEntry ? { prerenderWake: input.prerenderWakeEntry } : {},
430
- navigation: input.navigationEntry
514
+ navigation: input.navigationEntry,
515
+ ...input.fragmentEntry ? { fragment: input.fragmentEntry } : {},
516
+ ...input.fragmentImports?.length ? { fragmentImports: input.fragmentImports } : {}
431
517
  },
432
518
  routes: {
433
519
  navigation: sortRouteMap(input.routes.navigation),
@@ -436,6 +522,16 @@ function createClientAssetsManifest(input) {
436
522
  }
437
523
  };
438
524
  }
525
+ function renameClientAssetChunks(manifest, renames) {
526
+ const urls = new Map([...renames].map(([previous, next]) => [joinURL(manifest.base, previous), joinURL(manifest.base, next)]));
527
+ return renameUrls(manifest, (url) => urls.get(url) ?? url);
528
+ }
529
+ function renameUrls(value, rename) {
530
+ if (typeof value === "string") return rename(value);
531
+ if (Array.isArray(value)) return value.map((item) => renameUrls(item, rename));
532
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, key === "base" ? item : renameUrls(item, rename)]));
533
+ return value;
534
+ }
439
535
  async function removeClientAssetsManifest(clientOutDir) {
440
536
  const path = clientAssetsManifestPath(clientOutDir);
441
537
  await Promise.all([rm(path, { force: true }), rm(clientAssetsManifestTemporaryPath(clientOutDir), { force: true })]);
@@ -489,8 +585,9 @@ function clientAssetFileName(href, base) {
489
585
  if (href !== joinURL(base, fileName)) throw clientAssetsManifestError(`contains a non-canonical asset URL ${JSON.stringify(href)} for base ${JSON.stringify(base)}`);
490
586
  return fileName;
491
587
  }
588
+ /** Where the client build leaves this manifest for the server build: outside the served client output. */
492
589
  function clientAssetsManifestPath(clientOutDir) {
493
- return join(clientOutDir, MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST);
590
+ return join(marklessBuildManifestDir(clientOutDir), MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST);
494
591
  }
495
592
  function clientAssetsManifestTemporaryPath(clientOutDir) {
496
593
  return `${clientAssetsManifestPath(clientOutDir)}.tmp`;
@@ -504,7 +601,9 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
504
601
  const entries = {
505
602
  resume: requiredString(input.entries.resume, "entries.resume"),
506
603
  ...input.entries.prerenderWake !== void 0 ? { prerenderWake: requiredString(input.entries.prerenderWake, "entries.prerenderWake") } : {},
507
- navigation: requiredString(input.entries.navigation, "entries.navigation")
604
+ navigation: requiredString(input.entries.navigation, "entries.navigation"),
605
+ ...input.entries.fragment !== void 0 ? { fragment: requiredString(input.entries.fragment, "entries.fragment") } : {},
606
+ ...input.entries.fragmentImports !== void 0 ? { fragmentImports: requiredStrings(input.entries.fragmentImports, "entries.fragmentImports") } : {}
508
607
  };
509
608
  const routes = {
510
609
  navigation: routeMap(input.routes.navigation, "routes.navigation"),
@@ -512,16 +611,21 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
512
611
  styles: routeMap(input.routes.styles, "routes.styles")
513
612
  };
514
613
  assertMatchingRouteKeys(routes);
515
- const hrefs = new Set([
614
+ const importMap = input.importMap === void 0 ? void 0 : importMapImports(input.importMap);
615
+ const hrefs = /* @__PURE__ */ new Set([
516
616
  entries.resume,
517
617
  ...entries.prerenderWake !== void 0 ? [entries.prerenderWake] : [],
518
618
  entries.navigation,
619
+ ...entries.fragment !== void 0 ? [entries.fragment] : [],
620
+ ...entries.fragmentImports ?? [],
519
621
  ...Object.values(routes.navigation).flat(),
520
622
  ...Object.values(routes.ssr).flat(),
521
- ...Object.values(routes.styles).flat()
623
+ ...Object.values(routes.styles).flat(),
624
+ ...Object.values(importMap?.imports ?? {})
522
625
  ]);
523
626
  for (const href of hrefs) {
524
- const assetPath = join(clientOutDir, clientAssetFileName(href, base));
627
+ const fileName = clientAssetFileName(href, base);
628
+ const assetPath = join(clientOutDir, fileName);
525
629
  const relativeAssetPath = relative(clientOutDir, assetPath);
526
630
  if (relativeAssetPath === ".." || relativeAssetPath.startsWith("../") || isAbsolute(relativeAssetPath)) throw clientAssetsManifestError(`contains an unsafe asset path ${JSON.stringify(href)}`);
527
631
  try {
@@ -534,9 +638,14 @@ async function validateClientAssetsManifest(input, base, clientOutDir) {
534
638
  version: MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION,
535
639
  base,
536
640
  entries,
537
- routes
641
+ routes,
642
+ ...importMap ? { importMap } : {}
538
643
  };
539
644
  }
645
+ function requiredStrings(input, label) {
646
+ if (!Array.isArray(input)) throw clientAssetsManifestError(`has invalid ${label}`);
647
+ return input.map((item, index) => requiredString(item, `${label}[${index}]`));
648
+ }
540
649
  function routeMap(input, label) {
541
650
  if (!isRecord$1(input)) throw clientAssetsManifestError(`has invalid ${label}`);
542
651
  const routes = {};
@@ -580,10 +689,15 @@ function isRecord$1(input) {
580
689
  function clientAssetsManifestError(message, cause) {
581
690
  return new Error(`Markless Router client-assets manifest ${message}.`, { cause });
582
691
  }
692
+ function importMapImports(input) {
693
+ const imports = isRecord$1(input) ? input.imports : void 0;
694
+ if (!isRecord$1(imports) || !Object.values(imports).every((href) => typeof href === "string")) throw clientAssetsManifestError("has an invalid import map");
695
+ return { imports };
696
+ }
583
697
  //#endregion
584
698
  //#region src/vite/html-transform.ts
585
699
  const HELPER = "__marklessRouterHtmlAttributes";
586
- const SAFE_GLOBALS = new Set([
700
+ const SAFE_GLOBALS = /* @__PURE__ */ new Set([
587
701
  "String",
588
702
  "Number",
589
703
  "Boolean",
@@ -609,41 +723,42 @@ function htmlTransformPlugin() {
609
723
  };
610
724
  }
611
725
  function transformHtmlSource(code, astInput) {
612
- const ast = node(astInput);
726
+ const ast = node$1(astInput);
613
727
  if (!ast) fail(`MarklessRouter could not read ${DOCUMENT_FILE_LABEL}. Check it for syntax errors.`);
614
728
  const fn = findDefaultDocumentFunction(ast);
615
729
  if (fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression") fail(`MarklessRouter needs to see your document body in ${DOCUMENT_FILE_LABEL}. Write \`export default (props) => <Html>...</Html>\` instead of passing a named function.`);
616
- const props = first(nodes(fn.params));
730
+ const props = first(nodes$1(fn.params));
617
731
  const propsName = props?.type === "Identifier" ? string(props.name) : void 0;
618
732
  const propsSource = props ? slice(code, props) : "";
619
- const attrs = nodes(node(findReturnedHtml(fn).openingElement)?.attributes).map((attr) => htmlAttr(code, attr, propsName));
620
- const helper = `export function ${HELPER}(${propsSource}) {\n return ${attrs.length ? `{\n${attrs.map(([name, value]) => ` ${JSON.stringify(name)}: ${value}`).join(",\n")}\n }` : "{}"};\n}\n`;
733
+ const attrs = nodes$1(node$1(findReturnedHtml(fn).openingElement)?.attributes).map((attr) => htmlAttr(code, attr, propsName));
734
+ const body = attrs.length ? `{\n${attrs.map(([name, value]) => ` ${JSON.stringify(name)}: ${value}`).join(",\n")}\n }` : "{}";
735
+ const helper = `export function ${HELPER}(${propsSource}) {\n return ${body};\n}\n`;
621
736
  return code.endsWith("\n") ? `${code}\n${helper}` : `${code}\n\n${helper}`;
622
737
  }
623
738
  function findDefaultDocumentFunction(ast) {
624
- for (const statement of nodes(ast.body)) {
625
- const declaration = node(statement.declaration);
739
+ for (const statement of nodes$1(ast.body)) {
740
+ const declaration = node$1(statement.declaration);
626
741
  if (statement.type !== "ExportDefaultDeclaration") continue;
627
742
  if (declaration?.type === "ArrowFunctionExpression" || declaration?.type === "FunctionExpression") return declaration;
628
743
  }
629
744
  fail(`MarklessRouter could not find the document function. ${DOCUMENT_FILE_LABEL} must default export \`() => <Html>...</Html>\`.`);
630
745
  }
631
746
  function findReturnedHtml(fn) {
632
- const body = unwrap(node(fn.body));
633
- const returned = body?.type === "BlockStatement" ? unwrap(node(nodes(body.body).find((item) => item.type === "ReturnStatement")?.argument)) : body;
747
+ const body = unwrap(node$1(fn.body));
748
+ const returned = body?.type === "BlockStatement" ? unwrap(node$1(nodes$1(body.body).find((item) => item.type === "ReturnStatement")?.argument)) : body;
634
749
  if (returned?.type !== "JSXElement") fail(`MarklessRouter expected ${DOCUMENT_FILE_LABEL} to return <Html> at the top level. Wrap your document shell in \`<Html>...</Html>\`.`);
635
- if (jsxName(node(node(returned.openingElement)?.name)) !== "Html") fail(`MarklessRouter expected ${DOCUMENT_FILE_LABEL} to return <Html> at the top level. Put <head> and <body> inside \`<Html>...</Html>\`.`);
750
+ if (jsxName(node$1(node$1(returned.openingElement)?.name)) !== "Html") fail(`MarklessRouter expected ${DOCUMENT_FILE_LABEL} to return <Html> at the top level. Put <head> and <body> inside \`<Html>...</Html>\`.`);
636
751
  return returned;
637
752
  }
638
753
  function htmlAttr(code, attr, propsName) {
639
754
  if (attr.type === "JSXSpreadAttribute") fail("MarklessRouter does not support spreading props onto <Html> yet. Write each html attribute directly, like `<Html lang=\"en\">`.");
640
- const name = jsxName(node(attr.name));
755
+ const name = jsxName(node$1(attr.name));
641
756
  if (!name) fail("MarklessRouter found an unsupported <Html> attribute name. Use plain html attributes like `lang`, `class`, or `data-theme`.");
642
- const value = node(attr.value);
757
+ const value = node$1(attr.value);
643
758
  if (!value) return [name, "true"];
644
759
  if (value.type === "Literal") return [name, JSON.stringify(value.value)];
645
760
  if (value.type !== "JSXExpressionContainer") fail(`MarklessRouter could not understand the <Html> "${name}" attribute. Use a string or JSX expression, like \`${name}="value"\` or \`${name}={props.url.pathname}\`.`);
646
- const expression = unwrap(node(value.expression));
761
+ const expression = unwrap(node$1(value.expression));
647
762
  if (!expression || expression.type === "JSXEmptyExpression") fail(`MarklessRouter found an empty <Html> "${name}" attribute expression. Add a value or remove the attribute.`);
648
763
  const ref = unsupportedIdentifier(expression, propsName);
649
764
  if (ref) fail(`MarklessRouter cannot use "${ref}" in <Html ${name}={...}> because html attributes are read before Markless renders ${DOCUMENT_FILE_LABEL}. Use props directly, like \`${name}={props.url.pathname}\`, or a literal value.`);
@@ -652,7 +767,7 @@ function htmlAttr(code, attr, propsName) {
652
767
  function unsupportedIdentifier(root, propsName) {
653
768
  const stack = [root];
654
769
  while (stack.length > 0) {
655
- const current = node(stack.pop());
770
+ const current = node$1(stack.pop());
656
771
  if (!current) continue;
657
772
  if (current.type === "Identifier") {
658
773
  const name = string(current.name);
@@ -678,7 +793,7 @@ function jsxName(name) {
678
793
  }
679
794
  function unwrap(value) {
680
795
  let current = value;
681
- while (current?.type === "ParenthesizedExpression" || current?.type === "TSAsExpression" || current?.type === "TSNonNullExpression") current = node(current.expression);
796
+ while (current?.type === "ParenthesizedExpression" || current?.type === "TSAsExpression" || current?.type === "TSNonNullExpression") current = node$1(current.expression);
682
797
  return current;
683
798
  }
684
799
  function slice(code, value) {
@@ -686,10 +801,10 @@ function slice(code, value) {
686
801
  if (range) return code.slice(range[0], range[1]);
687
802
  return code.slice(value.start, value.end);
688
803
  }
689
- function nodes(value) {
690
- return Array.isArray(value) ? value.filter((item) => !!node(item)) : [];
804
+ function nodes$1(value) {
805
+ return Array.isArray(value) ? value.filter((item) => !!node$1(item)) : [];
691
806
  }
692
- function node(value) {
807
+ function node$1(value) {
693
808
  return typeof value === "object" && value !== null ? value : void 0;
694
809
  }
695
810
  function first(items) {
@@ -702,11 +817,72 @@ function fail(message) {
702
817
  throw new Error(message);
703
818
  }
704
819
  //#endregion
820
+ //#region ../bundler/src/scalar-plan-source.ts
821
+ const MARKLESS_SCALAR_PLAN_SOURCE_QUERY = "markless-scalar-plans";
822
+ function scalarPlanSourceReference(source) {
823
+ return withQuery(source, {
824
+ "markless-symbols": null,
825
+ [MARKLESS_SCALAR_PLAN_SOURCE_QUERY]: null
826
+ });
827
+ }
828
+ /**
829
+ * The same one line for the live roster, gated on the same fact the reconcile
830
+ * plane is gated on: a payload with no computed nodes can hold no roster
831
+ * derivation, so it never names the module and never emits its chunk.
832
+ *
833
+ * Both call sites (the start-up wiring and a sync computed's re-derive) sit in
834
+ * modules every resumed page runs, which is why neither may write the specifier.
835
+ * A composed MDX route writes it too: it cannot see its islands' computeds.
836
+ */
837
+ function emitRosterResumeLoaderInstall() {
838
+ return "globalThis.__marklessRosterResume ??= () => import('@markless/web/fns/roster-resume');";
839
+ }
840
+ function emitQueuedResumeContainerEvent(source, { censusRepair = true } = {}) {
841
+ const implementation = source.replace("export async function resumeContainerEvent", "async function marklessResumeContainerEvent");
842
+ if (implementation === source) throw new Error("MARKLESS_RESUME_DISPATCH_EXPORT_MISSING");
843
+ return [
844
+ implementation,
845
+ "export function resumeContainerEvent(input) {",
846
+ " const root = input.root;",
847
+ censusRepair ? " if (root.__marklessShifted) return import('@markless/web/fns/served-census-repair').then((census) => (census.marklessRepairCensus(root), resumeContainerEvent(input)));" : "",
848
+ " if (!root.__marklessDispatch) {",
849
+ " let handler = marklessResumeContainerEvent, tail = Promise.resolve(), done = tail, lastEvent = null, lastEventTime = undefined, seen = new WeakMap();",
850
+ " root.__marklessRegisterDispatch = next => {",
851
+ " const fallback = handler;",
852
+ " handler = input => next(input, fallback);",
853
+ " };",
854
+ " root.__marklessDispatch = next => {",
855
+ " const event = next.event, eventTime = event?.timeStamp;",
856
+ " if (event && next.eventRecord && next.element) event.__marklessPressHost ??= next.element;",
857
+ " if ((event === lastEvent && eventTime === lastEventTime) || (seen.has(event) && seen.get(event) === eventTime)) return done;",
858
+ " if (Object(event) === event) seen.set(event, eventTime);",
859
+ " lastEvent = event;",
860
+ " lastEventTime = eventTime;",
861
+ " let release;",
862
+ " const started = new Promise(resolve => release = resolve);",
863
+ " const run = () => (root.__marklessReleaseDispatch = release, handler(next));",
864
+ " done = tail.then(run, run);",
865
+ " done.then(release, release);",
866
+ " tail = started;",
867
+ " return done;",
868
+ " };",
869
+ " }",
870
+ " return root.__marklessDispatch(input);",
871
+ "}"
872
+ ].join("\n");
873
+ }
874
+ //#endregion
705
875
  //#region src/vite/mdx.ts
876
+ const MDX_ROUTE_RUNTIME_SPECIFIER = "@markless/router/vite/runtime/mdx-route";
706
877
  function mdxTransformPlugin() {
707
878
  return {
708
879
  name: "markless-router:mdx",
709
880
  enforce: "pre",
881
+ config(config, env) {
882
+ if (env.command !== "serve") return void 0;
883
+ const include = optimizedDepsToInclude([MDX_ROUTE_RUNTIME_SPECIFIER], config);
884
+ return include.length > 0 ? { optimizeDeps: { include } } : void 0;
885
+ },
710
886
  transform: {
711
887
  order: "pre",
712
888
  async handler(code, id) {
@@ -723,7 +899,7 @@ async function transformMdxRoute(source, id) {
723
899
  const route = parseMdxRoute(source, id);
724
900
  const html = route.parts.map((part) => part.kind === "html" ? part.html : "").join("");
725
901
  if (route.components.length === 0) return [
726
- `import { createMdxRenderDataSurface } from '@markless/router/vite/runtime/mdx-route';`,
902
+ `import { createMdxRenderDataSurface } from '${MDX_ROUTE_RUNTIME_SPECIFIER}';`,
727
903
  `const marklessMdxParts = ${JSON.stringify(route.parts)};`,
728
904
  "const marklessMdxRenderData = createMdxRenderDataSurface(marklessMdxParts, []);",
729
905
  "const marklessMdxPage = {",
@@ -736,24 +912,52 @@ async function transformMdxRoute(source, id) {
736
912
  "export default marklessMdxPage;",
737
913
  ""
738
914
  ].join("\n");
915
+ const query = new URLSearchParams(parsePath(id).search);
916
+ if (query.has("markless-route") || query.has("markless-resume")) return emitMdxClientRoute(route, id, query.has("markless-resume"));
739
917
  return emitComposedMdxRoute(route, id);
740
918
  }
919
+ function emitMdxClientRoute(route, id, resume) {
920
+ return [
921
+ `import { createMdxRenderDataSurface, loadMdxSymbol${resume ? ", tryResumeMdxScalar" : ""} } from '${MDX_ROUTE_RUNTIME_SPECIFIER}';`,
922
+ `const marklessMdxSymbolLoaders = ${renderSymbolLoaders(route.components)};`,
923
+ `const marklessMdxRenderData = ${renderMdxRenderDataLoader(route.components, id, route.parts)};`,
924
+ renderOverlayLoader(),
925
+ emitRosterResumeLoaderInstall(),
926
+ "const marklessMdxPage = {",
927
+ " renderData: marklessMdxRenderData,",
928
+ " loadSymbol: marklessMdxLoadSymbol,",
929
+ "};",
930
+ "export default marklessMdxPage;",
931
+ ...resume ? renderMdxResumeHandler() : [],
932
+ "function marklessMdxLoadSymbol(symbolId, children = []) {",
933
+ " return loadMdxSymbol(symbolId, children, marklessMdxSymbolLoaders);",
934
+ "}",
935
+ ""
936
+ ].join("\n");
937
+ }
938
+ function renderOverlayLoader() {
939
+ return "globalThis.__marklessOverlay ??= (root) => root.querySelector('[overlay]') ? import('@markless/web/fns/overlay').then((m) => m.installOverlayBehavior(root)) : undefined;";
940
+ }
741
941
  function emitComposedMdxRoute(route, id) {
742
942
  return [
743
- `import { resumeFromPayloadDocument } from '@markless/core/web/resume';`,
744
- `import { composeMdxState, composeMdxView, createMdxRenderDataSurface, loadMdxSymbol, renderMdxChild } from '@markless/router/vite/runtime/mdx-route';`,
943
+ `import { composeMdxState, composeMdxView, createMdxRenderDataSurface, loadMdxSymbol, renderMdxChild, tryResumeMdxScalar } from '${MDX_ROUTE_RUNTIME_SPECIFIER}';`,
745
944
  ...route.imports,
746
945
  "",
747
946
  `const marklessMdxParts = ${JSON.stringify(route.parts)};`,
748
947
  `const marklessMdxSymbolLoaders = ${renderSymbolLoaders(route.components)};`,
749
948
  `const marklessMdxRenderData = ${renderMdxRenderDataLoader(route.components, id)};`,
750
949
  `const marklessMdxStorageSeeds = ${renderStorageSeeds(route.components)};`,
950
+ `const marklessMdxHeadInjections = ${renderHeadInjections(route.components)};`,
951
+ "",
952
+ renderOverlayLoader(),
953
+ emitRosterResumeLoaderInstall(),
751
954
  "",
752
955
  "const marklessMdxPage = {",
753
956
  " renderData: marklessMdxRenderData,",
754
957
  " storageSeeds: marklessMdxStorageSeeds,",
958
+ " headInjections: marklessMdxHeadInjections,",
755
959
  " loadSymbol: marklessMdxLoadSymbol,",
756
- " async renderSsr(props = {}) {",
960
+ " async renderSsr(props = {}, marklessMdxRenderContext) {",
757
961
  " const marklessMdxChildren = [];",
758
962
  ` const html = ${renderHtmlExpression(route)};`,
759
963
  " const state = composeMdxState(marklessMdxChildren);",
@@ -764,15 +968,7 @@ function emitComposedMdxRoute(route, id) {
764
968
  "};",
765
969
  "export default marklessMdxPage;",
766
970
  "",
767
- "export async function resumeContainerEvent(input) {",
768
- " input.root.__asyncResumeRuntimeStarted = true;",
769
- " const { runtime } = await resumeFromPayloadDocument({",
770
- " document: input.root,",
771
- " root: input.root,",
772
- " loadSymbol: marklessMdxLoadSymbol,",
773
- " });",
774
- " await runtime.dispatch(input.event, { syncPolicyAlreadyApplied: true });",
775
- "}",
971
+ ...renderMdxResumeHandler(),
776
972
  "",
777
973
  "function marklessMdxLoadSymbol(symbolId, children = []) {",
778
974
  " return loadMdxSymbol(symbolId, children, marklessMdxSymbolLoaders);",
@@ -780,6 +976,32 @@ function emitComposedMdxRoute(route, id) {
780
976
  ""
781
977
  ].join("\n");
782
978
  }
979
+ function renderMdxResumeHandler() {
980
+ return [
981
+ "let marklessResumeModule;",
982
+ emitQueuedResumeContainerEvent([
983
+ "export async function resumeContainerEvent(input) {",
984
+ " if (await tryResumeMdxScalar(input, marklessMdxLoadScalarPlan, marklessMdxLoadSymbol)) return;",
985
+ " input.root.__asyncResumeRuntimeStarted = true;",
986
+ " const { resumeFromPayloadDocument } = await (marklessResumeModule ??= import('@markless/core/web/resume').catch(error => { marklessResumeModule = undefined; throw error; }));",
987
+ " const { runtime } = await resumeFromPayloadDocument({",
988
+ " document: input.root,",
989
+ " root: input.root,",
990
+ " loadSymbol: marklessMdxLoadSymbol,",
991
+ " handoffDispatchOptions: marklessMdxDispatchOptions,",
992
+ " renderData: marklessMdxRenderData,",
993
+ " });",
994
+ " await runtime.dispatch(input.event, marklessMdxDispatchOptions(input));",
995
+ "}",
996
+ "function marklessMdxDispatchOptions(input) {",
997
+ " return { syncPolicyAlreadyApplied: true, propagationStopped: input.propagationStopped, ignoreUnmatched: input.eventRecord == null };",
998
+ "}"
999
+ ].join("\n")),
1000
+ "function marklessMdxLoadScalarPlan(symbolId) {",
1001
+ " return marklessMdxSymbolLoaders.find(loader => symbolId.startsWith(loader.prefix))?.loadScalarActionPlan(symbolId);",
1002
+ "}"
1003
+ ];
1004
+ }
783
1005
  function renderMdxPreload(components) {
784
1006
  return [
785
1007
  " preload() {",
@@ -828,7 +1050,8 @@ function tsrxImportsFromProgram(program, id) {
828
1050
  if (statement.type !== "ImportDeclaration") throw new Error(`Markless Router MDX only supports importing TSRX components before content: ${id}`);
829
1051
  const specifier = statement.source.value;
830
1052
  const defaultImport = statement.specifiers.find((importSpecifier) => importSpecifier.type === "ImportDefaultSpecifier");
831
- if (typeof specifier !== "string" || !specifier.endsWith(".tsrx") || !defaultImport || statement.specifiers.length !== 1) throw new Error(`Markless Router MDX currently supports default imports from .tsrx files only: ${id}`);
1053
+ if (typeof specifier !== "string" || !specifier.endsWith(".tsrx")) throw new Error(`Markless Router MDX currently supports default imports from .tsrx files only: ${id}`);
1054
+ if (!defaultImport || statement.specifiers.length !== 1) throw new Error(tsrxImportShapeMessage(statement.specifiers, specifier, id));
832
1055
  imports.push({
833
1056
  localName: defaultImport.local.name,
834
1057
  specifier
@@ -836,20 +1059,32 @@ function tsrxImportsFromProgram(program, id) {
836
1059
  }
837
1060
  return imports;
838
1061
  }
1062
+ function tsrxImportShapeMessage(specifiers, specifier, id) {
1063
+ const intro = `Markless Router MDX imports a .tsrx component as that file's default export, one component per import: ${id}.`;
1064
+ const source = JSON.stringify(specifier).slice(1, -1);
1065
+ const defaultImport = specifiers.find((entry) => entry.type === "ImportDefaultSpecifier");
1066
+ if (defaultImport) return `${intro} Write \`import ${defaultImport.local.name} from '${source}';\` alone.`;
1067
+ const named = specifiers.find((entry) => entry.type === "ImportSpecifier");
1068
+ if (named) {
1069
+ const exported = named.imported.type === "Identifier" ? named.imported.name : String(named.imported.value);
1070
+ return `${intro} Write \`import ${named.local.name} from '${source}';\` and make ${exported} the file's \`export default\`.`;
1071
+ }
1072
+ return `${intro} Write \`import Component from '${source}';\` and make the component the file's \`export default\`.`;
1073
+ }
839
1074
  function routePartsFromHast(nodes, context) {
840
1075
  const parts = [];
841
1076
  let staticNodes = [];
842
1077
  for (const node of nodes) {
843
1078
  if (isMdxEsm(node)) continue;
844
- if (isMdxJsxFlowElement(node)) {
845
- appendHtmlPart(parts, staticNodes, context.id);
1079
+ if (isMdxJsxFlowElement(node) && !isIntrinsicElement(node, context)) {
1080
+ appendHtmlPart(parts, staticNodes, context);
846
1081
  staticNodes = [];
847
1082
  parts.push(componentPart(node, context));
848
1083
  continue;
849
1084
  }
850
1085
  staticNodes.push(node);
851
1086
  }
852
- appendHtmlPart(parts, staticNodes, context.id);
1087
+ appendHtmlPart(parts, staticNodes, context);
853
1088
  return parts;
854
1089
  }
855
1090
  function componentPart(node, context) {
@@ -858,10 +1093,15 @@ function componentPart(node, context) {
858
1093
  if (!imported) throw new Error(`Markless Router MDX component is not imported from .tsrx: ${name}`);
859
1094
  const componentIndex = context.components.length;
860
1095
  const props = componentProps(node, context.id);
861
- const children = renderStaticHtml(node.children, context.id);
1096
+ const childNodes = normalizeStaticHastNodes(node.children, context);
1097
+ const children = childNodes.length ? toHtml({
1098
+ type: "root",
1099
+ children: childNodes
1100
+ }, { allowDangerousHtml: true }) : "";
862
1101
  context.components.push({
863
1102
  ...imported,
864
- prefix: `m${componentIndex}:`,
1103
+ prefix: protocolIslandSegment(componentIndex),
1104
+ childrenElementTags: children ? hastElementTags(childNodes) : [],
865
1105
  props: children ? [...props, {
866
1106
  name: "children",
867
1107
  value: children,
@@ -873,50 +1113,72 @@ function componentPart(node, context) {
873
1113
  componentIndex
874
1114
  };
875
1115
  }
876
- function appendHtmlPart(parts, nodes, id) {
1116
+ function appendHtmlPart(parts, nodes, context) {
877
1117
  if (nodes.length === 0) return;
878
- const staticNodes = normalizeStaticHastNodes(nodes, id);
1118
+ const staticNodes = normalizeStaticHastNodes(nodes, context);
879
1119
  const html = toHtml({
880
1120
  type: "root",
881
1121
  children: staticNodes
882
1122
  }, { allowDangerousHtml: true });
883
1123
  if (html) {
884
1124
  const elementTags = hastElementTags(staticNodes);
1125
+ const commentCount = hastCommentCount(staticNodes);
885
1126
  parts.push({
886
1127
  kind: "html",
887
1128
  html,
888
1129
  elementCount: elementTags.length,
1130
+ ...commentCount > 0 ? { commentCount } : {},
889
1131
  elementTags
890
1132
  });
891
1133
  }
892
1134
  }
893
- function renderStaticHtml(nodes, id) {
894
- if (nodes.length === 0) return "";
895
- return toHtml({
896
- type: "root",
897
- children: normalizeStaticHastNodes(nodes, id)
898
- }, { allowDangerousHtml: true });
899
- }
900
- function normalizeStaticHastNodes(nodes, id) {
901
- return nodes.flatMap((node) => normalizeStaticHastNode(node, id));
1135
+ function normalizeStaticHastNodes(nodes, context) {
1136
+ return nodes.flatMap((node) => normalizeStaticHastNode(node, context));
902
1137
  }
903
- function normalizeStaticHastNode(node, id) {
1138
+ function normalizeStaticHastNode(node, context) {
1139
+ const { id } = context;
904
1140
  if (isMdxEsm(node)) return [];
905
- if (isMdxJsxFlowElement(node) || isMdxJsxTextElement(node)) throw new Error(`Markless Router MDX only supports imported TSRX components as route-level MDX elements or static markdown children: ${id}`);
1141
+ if (isMdxJsxFlowElement(node) || isMdxJsxTextElement(node)) {
1142
+ if (!isIntrinsicElement(node, context)) throw new Error(`Markless Router MDX only supports imported TSRX components as route-level MDX elements or static markdown children: ${id}`);
1143
+ return [intrinsicElement(node, context)];
1144
+ }
906
1145
  if (isMdxExpression(node)) {
1146
+ if (isEmptyMdxExpression(node.value)) return [];
907
1147
  const value = literalSafeExpressionValue(node.value, id);
908
1148
  return [{
909
1149
  type: "text",
910
1150
  value: value == null ? "" : String(value)
911
1151
  }];
912
1152
  }
913
- if (node.type === "root") return normalizeStaticHastNodes(node.children, id);
1153
+ if (node.type === "root") return normalizeStaticHastNodes(node.children, context);
914
1154
  if ("children" in node && Array.isArray(node.children)) return [{
915
1155
  ...node,
916
- children: normalizeStaticHastNodes(node.children, id)
1156
+ children: normalizeStaticHastNodes(node.children, context)
917
1157
  }];
918
1158
  return [node];
919
1159
  }
1160
+ function isIntrinsicElement(node, context) {
1161
+ const name = node.name;
1162
+ return typeof name === "string" && /^[a-z][a-z0-9-]*$/.test(name) && !context.importedComponents.has(name);
1163
+ }
1164
+ const JSX_ATTRIBUTE_NAMES = {
1165
+ className: "class",
1166
+ htmlFor: "for"
1167
+ };
1168
+ function intrinsicElement(node, context) {
1169
+ const properties = {};
1170
+ for (const { name, value } of componentProps(node, context.id)) {
1171
+ if (value === false || value == null) continue;
1172
+ if (typeof value !== "string" && typeof value !== "number" && value !== true) throw literalExpressionError(context.id);
1173
+ properties[JSX_ATTRIBUTE_NAMES[name] ?? name.toLowerCase()] = value === true ? true : String(value);
1174
+ }
1175
+ return {
1176
+ type: "element",
1177
+ tagName: node.name,
1178
+ properties,
1179
+ children: normalizeStaticHastNodes(node.children, context)
1180
+ };
1181
+ }
920
1182
  function componentProps(node, id) {
921
1183
  return node.attributes.map((attribute) => componentProp(attribute, id));
922
1184
  }
@@ -947,6 +1209,9 @@ function literalSafeExpressionValue(source, id) {
947
1209
  if (program.body.length !== 1 || statement?.type !== "ExpressionStatement") throw new Error(`Markless Router MDX only supports literal-safe expressions in attributes and markdown: ${id}`);
948
1210
  return evaluateLiteralExpression(statement.expression, id);
949
1211
  }
1212
+ function isEmptyMdxExpression(source) {
1213
+ return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").trim() === "";
1214
+ }
950
1215
  function parseMdxExpressionValue(source, id) {
951
1216
  const handle = createMdxHastHandle(`{${source}}`);
952
1217
  let program = null;
@@ -1015,9 +1280,12 @@ function renderHtmlExpression(route) {
1015
1280
  return joinExpressions(route.parts.map((part) => {
1016
1281
  if (part.kind === "html") return JSON.stringify(part.html);
1017
1282
  const component = route.components[part.componentIndex];
1018
- return `(await renderMdxChild(marklessMdxChildren, ${component.localName}, ${componentPropsExpression(component)}, { componentIndex: ${part.componentIndex}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)} }))`;
1283
+ return `(await renderMdxChild(marklessMdxChildren, ${component.localName}, ${componentPropsExpression(component)}, { componentIndex: ${part.componentIndex}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)}${childrenElementTagsField(component)} }, marklessMdxRenderContext))`;
1019
1284
  }));
1020
1285
  }
1286
+ function childrenElementTagsField(component) {
1287
+ return component.childrenElementTags.length > 0 ? `, childrenElementTags: ${JSON.stringify(component.childrenElementTags)}` : "";
1288
+ }
1021
1289
  function componentPropsExpression(component) {
1022
1290
  if (component.props.length === 0) return "{}";
1023
1291
  return `{ ${component.props.map((prop) => `${JSON.stringify(prop.name)}: ${prop.valueExpression}`).join(", ")} }`;
@@ -1034,14 +1302,26 @@ function hastElementTags(nodes) {
1034
1302
  }
1035
1303
  return tags;
1036
1304
  }
1305
+ function hastCommentCount(nodes) {
1306
+ let comments = 0;
1307
+ for (const node of nodes) {
1308
+ if (node.type === "comment") comments++;
1309
+ if ("children" in node && Array.isArray(node.children)) comments += hastCommentCount(node.children);
1310
+ }
1311
+ return comments;
1312
+ }
1037
1313
  function renderStorageSeeds(components) {
1038
1314
  const localNames = [...new Set(components.map((component) => component.localName))];
1039
1315
  return localNames.length === 0 ? "[]" : `[${localNames.map((localName) => `...(${localName}.storageSeeds ?? [])`).join(", ")}]`;
1040
1316
  }
1317
+ function renderHeadInjections(components) {
1318
+ const localNames = [...new Set(components.map((component) => component.localName))];
1319
+ return localNames.length === 0 ? "[]" : `[...new Map([${localNames.map((localName) => `...(${localName}.headInjections ?? [])`).join(", ")}].map((injection) => [JSON.stringify(injection), injection])).values()]`;
1320
+ }
1041
1321
  function renderSymbolLoaders(components) {
1042
- return `[${components.map((component) => `{ prefix: ${JSON.stringify(component.prefix)}, loadSymbol(symbolId) { return import(${JSON.stringify(`${component.specifier}?markless-symbols`)}).then((mod) => mod.loadSymbol(symbolId.slice(${component.prefix.length}))); } }`).join(", ")}]`;
1322
+ return `[${components.map((component) => `(() => { let module; const loadModule = () => module ??= import(${JSON.stringify(scalarPlanSourceReference(component.specifier))}).catch(error => { module = undefined; throw error; }); return { prefix: ${JSON.stringify(component.prefix)}, loadSymbol(symbolId) { return loadModule().then((mod) => mod.loadSymbol(symbolId.slice(${component.prefix.length}))); }, loadScalarActionPlan(symbolId) { return loadModule().then(mod => mod.loadScalarActionPlan?.(symbolId.slice(${component.prefix.length}))).then(action => action && ({ ...action, scope: ${JSON.stringify(component.prefix)} + action.scope })); } }; })()`).join(", ")}]`;
1043
1323
  }
1044
- function renderMdxRenderDataLoader(components, id) {
1324
+ function renderMdxRenderDataLoader(components, id, parts) {
1045
1325
  const reachedFrom = mdxMaterializedReachRoot(id);
1046
1326
  const imports = components.map((component) => {
1047
1327
  const source = withQuery(component.specifier, {
@@ -1052,7 +1332,8 @@ function renderMdxRenderDataLoader(components, id) {
1052
1332
  });
1053
1333
  const exportName = reachedFrom ? "marklessPrerenderData" : "marklessRenderData";
1054
1334
  const children = components.map((component, index) => `{ componentIndex: ${index}, hostPrefix: ${JSON.stringify(component.prefix)}, symbolPrefix: ${JSON.stringify(component.prefix)}, props: ${componentPropsExpression(component)}, surface: modules[${index}].${exportName} }`);
1055
- return `() => Promise.all([${imports.join(", ")}]).then(modules => createMdxRenderDataSurface(marklessMdxParts, [${children.join(", ")}]))`;
1335
+ const load = `Promise.all([${imports.join(", ")}]).then(modules => createMdxRenderDataSurface(marklessMdxParts, [${children.join(", ")}]))`;
1336
+ return parts ? `() => {\nconst marklessMdxParts = ${JSON.stringify(parts)};\nreturn ${load};\n}` : `() => ${load}`;
1056
1337
  }
1057
1338
  function mdxMaterializedReachRoot(id) {
1058
1339
  const query = new URLSearchParams(parsePath(id).search);
@@ -1073,7 +1354,166 @@ function isMdxExpression(node) {
1073
1354
  return node.type === "mdxFlowExpression" || node.type === "mdxTextExpression";
1074
1355
  }
1075
1356
  //#endregion
1357
+ //#region src/vite/route-preload-data.ts
1358
+ function compactRoutePreloadData(data) {
1359
+ const urls = [];
1360
+ const indices = /* @__PURE__ */ new Map();
1361
+ const encode = (routes) => Object.fromEntries(Object.keys(routes).sort().map((route) => [route, routes[route].map((href) => {
1362
+ let index = indices.get(href);
1363
+ if (index === void 0) {
1364
+ index = urls.length;
1365
+ urls.push(href);
1366
+ indices.set(href, index);
1367
+ }
1368
+ return index;
1369
+ })]));
1370
+ return [
1371
+ urls,
1372
+ encode(data.navigation),
1373
+ encode(data.ssr)
1374
+ ];
1375
+ }
1376
+ function routePreloadDecoderSource(chunkSpecifiers) {
1377
+ return [
1378
+ "if (Array.isArray(routePreloadData)) {",
1379
+ " const [urls, navigation, ssr] = routePreloadData;",
1380
+ ...chunkSpecifiers ? [` const imports = Object.assign({}, ...[...(globalThis.document?.querySelectorAll('script[type="importmap"]') ?? [])].map((script) => { try { return JSON.parse(script.textContent).imports; } catch { return {}; } }));`, ` const href = (url) => (url.startsWith(${JSON.stringify(MARKLESS_CHUNK_SPECIFIER_PREFIX)}) && imports[url]) || url;`] : [],
1381
+ ` const decode = (routes) => Object.fromEntries(Object.entries(routes).map(([file, indices]) => [file, indices.map((index) => ${chunkSpecifiers ? "href(urls[index])" : "urls[index]"})]));`,
1382
+ " routePreloadData = { navigation: decode(navigation), ssr: decode(ssr) };",
1383
+ "}"
1384
+ ].join("\n");
1385
+ }
1386
+ //#endregion
1387
+ //#region src/navigation-polyfill.ts
1388
+ const NAVIGATION_POLYFILL_MODULE = "@virtualstate/navigation";
1389
+ //#endregion
1390
+ //#region src/vite/document-navigation.ts
1391
+ const ROUTE_INVARIANT_DOCUMENT_PROPS = /* @__PURE__ */ new Set(["children"]);
1392
+ function documentNavigationFor(source, id) {
1393
+ if (source === void 0) return "client";
1394
+ let program;
1395
+ try {
1396
+ program = parseModule(source, id);
1397
+ } catch {
1398
+ return "document";
1399
+ }
1400
+ const body = nodes(program.body);
1401
+ if (body.some(exportsHtmlAttributesHook)) return "document";
1402
+ const component = defaultExportedFunction(body);
1403
+ if (!component) return "document";
1404
+ return readsOnlyRouteInvariantProps(nodes(component.params)) ? "client" : "document";
1405
+ }
1406
+ function readsOnlyRouteInvariantProps(params) {
1407
+ if (params.length === 0) return true;
1408
+ if (params.length > 1) return false;
1409
+ const [param] = params;
1410
+ const pattern = param?.type === "AssignmentPattern" ? node(param.left) : param;
1411
+ if (pattern?.type !== "ObjectPattern") return false;
1412
+ return nodes(pattern.properties).every((property) => {
1413
+ if (property.type !== "Property" || property.computed === true) return false;
1414
+ const key = node(property.key);
1415
+ const name = key?.type === "Identifier" ? key.name : key?.value;
1416
+ return typeof name === "string" && ROUTE_INVARIANT_DOCUMENT_PROPS.has(name);
1417
+ });
1418
+ }
1419
+ function defaultExportedFunction(body) {
1420
+ const declaration = node(node(body.find((statement) => statement.type === "ExportDefaultDeclaration"))?.declaration);
1421
+ if (isFunction(declaration)) return declaration;
1422
+ if (declaration?.type !== "Identifier") return void 0;
1423
+ const name = declaration.name;
1424
+ for (const statement of body) {
1425
+ const local = statement.type === "ExportNamedDeclaration" ? node(statement.declaration) : statement;
1426
+ if (local?.type === "FunctionDeclaration" && node(local.id)?.name === name) return local;
1427
+ if (local?.type !== "VariableDeclaration") continue;
1428
+ for (const declarator of nodes(local.declarations)) {
1429
+ if (node(declarator.id)?.name !== name) continue;
1430
+ const init = node(declarator.init);
1431
+ return isFunction(init) ? init : void 0;
1432
+ }
1433
+ }
1434
+ }
1435
+ function exportsHtmlAttributesHook(statement) {
1436
+ if (statement.type !== "ExportNamedDeclaration") return false;
1437
+ const declaration = node(statement.declaration);
1438
+ return [
1439
+ node(declaration?.id)?.name,
1440
+ ...nodes(declaration?.declarations).map((declarator) => node(declarator.id)?.name),
1441
+ ...nodes(statement.specifiers).map((specifier) => node(specifier.exported)?.name)
1442
+ ].includes("__marklessRouterHtmlAttributes");
1443
+ }
1444
+ function isFunction(value) {
1445
+ return value?.type === "FunctionDeclaration" || value?.type === "FunctionExpression" || value?.type === "ArrowFunctionExpression";
1446
+ }
1447
+ function node(value) {
1448
+ return typeof value === "object" && value !== null ? value : void 0;
1449
+ }
1450
+ function nodes(value) {
1451
+ return Array.isArray(value) ? value.filter((item) => node(item)) : [];
1452
+ }
1453
+ //#endregion
1454
+ //#region src/vite/nitro-fragment-requests.ts
1455
+ function fragmentRequestModule() {
1456
+ return {
1457
+ name: "markless-router:fragment-requests",
1458
+ async setup(nitro) {
1459
+ const dir = join(nitro.options.buildDir, "markless-router");
1460
+ const plugin = join(dir, "fragment-requests.mjs");
1461
+ await mkdir(dir, { recursive: true });
1462
+ await writeFile(plugin, [
1463
+ `const prefix = ${JSON.stringify(`${FRAGMENT_PATH_PREFIX}/`)};`,
1464
+ "export default function fragmentRequests(nitroApp) {",
1465
+ " nitroApp.hooks?.hook('request', (event) => {",
1466
+ " const { pathname } = event.url;",
1467
+ " if (!pathname.startsWith(prefix)) return;",
1468
+ " const page = new URL(event.url.href);",
1469
+ " page.pathname = pathname.slice(prefix.length - 1);",
1470
+ " event.url = page;",
1471
+ " });",
1472
+ "}",
1473
+ ""
1474
+ ].join("\n"));
1475
+ nitro.options.plugins.push(plugin);
1476
+ }
1477
+ };
1478
+ }
1479
+ //#endregion
1480
+ //#region src/vite/nitro-prerender.ts
1481
+ function serverBuildState() {
1482
+ return { entry: void 0 };
1483
+ }
1484
+ function recordServerEntry(state, outDir, bundle, isServerEntry) {
1485
+ if (!outDir) return;
1486
+ const entries = Object.values(bundle).filter((item) => item.type === "chunk" && item.isEntry);
1487
+ const entry = entries.find((chunk) => chunk.facadeModuleId && isServerEntry(chunk.facadeModuleId)) ?? (entries.length === 1 ? entries[0] : void 0);
1488
+ if (entry) state.entry = resolve(outDir, entry.fileName);
1489
+ }
1490
+ function prerenderRendererModule(state) {
1491
+ return {
1492
+ name: "markless-router:prerender-renderer",
1493
+ setup(nitro) {
1494
+ nitro.hooks.hook("prerender:config", async (config) => {
1495
+ if (!state.entry) return;
1496
+ const dir = join(nitro.options.buildDir, "markless-router");
1497
+ const handler = join(dir, "prerender-renderer.mjs");
1498
+ await mkdir(dir, { recursive: true });
1499
+ await writeFile(handler, [
1500
+ `import * as service from ${JSON.stringify(state.entry)};`,
1501
+ "const server = service.default ?? service;",
1502
+ "export default ({ req }) => server.fetch(req);",
1503
+ ""
1504
+ ].join("\n"));
1505
+ config.renderer = { handler };
1506
+ });
1507
+ }
1508
+ };
1509
+ }
1510
+ //#endregion
1076
1511
  //#region src/vite/index.ts
1512
+ const PAGE_SERVER_ONLY_PROPS = [{
1513
+ exportName: "default",
1514
+ propName: "http",
1515
+ description: "the request with its cookies and headers, the locals middleware set, and the response being written"
1516
+ }];
1077
1517
  const ROUTE_DISCOVERY_ID = "virtual:markless-router/routes";
1078
1518
  const CLIENT_ENTRY_ID = "virtual:markless-router/client-entry";
1079
1519
  const RESUME_ENTRY_ID = "virtual:markless-router/resume-entry";
@@ -1085,12 +1525,17 @@ const PRERENDER_WAKE_ENTRY_PATH_ID = "virtual:markless-router/prerender-wake-ent
1085
1525
  const NAVIGATION_ENTRY_ID = "virtual:markless-router/navigation-entry";
1086
1526
  const NAVIGATION_ENTRY_ORIGIN = "/entries/client-entry.ts";
1087
1527
  const NAVIGATION_ENTRY_PATH_ID = "virtual:markless-router/navigation-entry-path";
1528
+ const FRAGMENT_ENTRY_ID = "virtual:markless-router/fragment-entry";
1529
+ const FRAGMENT_ENTRY_ORIGIN = "/entries/fragment-entry.ts";
1530
+ const FRAGMENT_ENTRY_PATH_ID = "virtual:markless-router/fragment-entry-path";
1088
1531
  const ROUTE_PRELOADS_ID = "virtual:markless-router/route-preloads";
1089
1532
  const ROUTE_PRELOADS_PLACEHOLDER = "__MARKLESS_ROUTER_ROUTE_PRELOADS__";
1533
+ const DOCUMENT_STYLESHEETS_PLACEHOLDER = "__MARKLESS_ROUTER_DOCUMENT_STYLESHEETS__";
1534
+ const LINK_BRIDGE_PATHS_PLACEHOLDER = "__MARKLESS_ROUTER_LINK_BRIDGE_PATHS__";
1090
1535
  const SERVER_ENTRY_ID = "virtual:markless-router/server-entry";
1091
1536
  const ROUTE_HREF_ID = "virtual:markless-router/route-href";
1092
1537
  const ROUTER_OPTIONS_ID = "virtual:markless-router/options";
1093
- const PUBLIC_VIRTUAL_MODULE_ID_RE = /^virtual:markless-router\/(?:routes|client-entry|resume-entry|resume-entry-path|prerender-wake-entry|prerender-wake-entry-path|navigation-entry|navigation-entry-path|route-preloads|server-entry|route-href|options)(?:\?.*)?$/;
1538
+ const PUBLIC_VIRTUAL_MODULE_ID_RE = /^virtual:markless-router\/(?:routes|client-entry|resume-entry|resume-entry-path|prerender-wake-entry|prerender-wake-entry-path|navigation-entry|navigation-entry-path|fragment-entry|fragment-entry-path|route-preloads|server-entry|route-href|options)(?:\?.*)?$/;
1094
1539
  const VITE_PLUGIN_FILE = decodePath(parseURL(import.meta.url).pathname);
1095
1540
  const VIRTUAL_ENTRY_DIR = VITE_PLUGIN_FILE.endsWith(".ts") ? join(dirname(VITE_PLUGIN_FILE), "entries") : join(dirname(dirname(VITE_PLUGIN_FILE)), "src/vite/entries");
1096
1541
  const DEFAULT_WATCH_IGNORES = [
@@ -1105,36 +1550,51 @@ const virtualEntryFiles = {
1105
1550
  [RESUME_ENTRY_ID]: "resume-entry.ts",
1106
1551
  [PRERENDER_WAKE_ENTRY_ID]: "prerender-wake-entry.ts",
1107
1552
  [NAVIGATION_ENTRY_ID]: "client-entry.ts",
1553
+ [FRAGMENT_ENTRY_ID]: "fragment-entry.ts",
1108
1554
  [SERVER_ENTRY_ID]: "server-entry.ts",
1109
1555
  [ROUTE_HREF_ID]: "route-href.ts"
1110
1556
  };
1111
1557
  function router(options = {}) {
1112
1558
  prerenderWakeChannelCaptured = process.env.MARKLESS_PRERENDER_WAKE === "1";
1113
1559
  if (options.nitro === false) return [
1560
+ pageServerOnlyPropsPlugin(),
1114
1561
  mdxTransformPlugin(),
1115
1562
  requestFileTransformPlugin(),
1116
1563
  routeTypegenPlugin(),
1117
1564
  anchorTransformPlugin(),
1118
1565
  htmlTransformPlugin(),
1119
- virtualModulesPlugin()
1566
+ virtualModulesPlugin(void 0, void 0, void 0, void 0, options)
1120
1567
  ];
1121
1568
  const nitroPlugins = nitro();
1122
1569
  const resumeEntry = resumeEntryState();
1123
1570
  const prerenderWakeEntry = resumeEntryState();
1124
1571
  const navigationEntry = resumeEntryState();
1572
+ const fragmentEntry = resumeEntryState();
1125
1573
  const routePreloads = routePreloadState();
1126
1574
  return [
1127
- routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads),
1575
+ routerConfigPlugin(nitroPlugins, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry, serverBuildState()),
1128
1576
  devSourceModuleRequestPlugin(),
1577
+ pageServerOnlyPropsPlugin(),
1129
1578
  mdxTransformPlugin(),
1130
1579
  requestFileTransformPlugin(),
1131
1580
  routeTypegenPlugin(),
1132
1581
  anchorTransformPlugin(),
1133
1582
  htmlTransformPlugin(),
1134
- virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads),
1583
+ virtualModulesPlugin(resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, options, fragmentEntry),
1584
+ linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options),
1135
1585
  nitroPlugins
1136
1586
  ];
1137
1587
  }
1588
+ function pageServerOnlyPropsPlugin() {
1589
+ return {
1590
+ name: "markless-router:page-server-only-props",
1591
+ configResolved(config) {
1592
+ const markless = config.plugins.find((plugin) => typeof plugin.api?.declareServerOnlyProps === "function");
1593
+ markless?.api.declareServerOnlyProps((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)) ? PAGE_SERVER_ONLY_PROPS : void 0);
1594
+ markless?.api.declareRouteModules?.((moduleId) => isPageModuleFile(normalizeRouteFileId(moduleId)));
1595
+ }
1596
+ };
1597
+ }
1138
1598
  function devSourceModuleRequestPlugin() {
1139
1599
  let root = "";
1140
1600
  return {
@@ -1155,18 +1615,32 @@ function devSourceModuleRequestPlugin() {
1155
1615
  }
1156
1616
  };
1157
1617
  }
1158
- function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads) {
1618
+ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEntry, navigationEntry, routePreloads, fragmentEntry = resumeEntryState(), serverBuild = serverBuildState()) {
1159
1619
  let clientOutDir = "";
1160
1620
  let clientEnvironmentName = "";
1161
1621
  let clientEnvironmentConfig;
1162
1622
  let serverEnvironmentConfig;
1163
1623
  let productionBuild = false;
1164
1624
  let pendingClientAssets;
1625
+ let capturedChunkNames = /* @__PURE__ */ new Map();
1626
+ const navigationPolyfillIds = /* @__PURE__ */ new Set();
1165
1627
  return {
1166
1628
  name: "markless-router:vite",
1167
1629
  enforce: "pre",
1168
1630
  sharedDuringBuild: true,
1169
- config(config) {
1631
+ resolveId: {
1632
+ filter: { id: new RegExp(`^${escapeRegExp(NAVIGATION_POLYFILL_MODULE)}$`) },
1633
+ async handler(source, importer, options) {
1634
+ if (source !== "@virtualstate/navigation") return null;
1635
+ const resolved = await this.resolve(source, importer, {
1636
+ ...options,
1637
+ skipSelf: true
1638
+ });
1639
+ if (resolved) navigationPolyfillIds.add(resolved.id);
1640
+ return resolved;
1641
+ }
1642
+ },
1643
+ config(config, env) {
1170
1644
  throwIfUserAddedNitro(config.plugins, nitroPluginsFromRouter);
1171
1645
  config.environments ??= {};
1172
1646
  const ssrEnvironment = config.environments.ssr ??= {};
@@ -1175,7 +1649,7 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1175
1649
  ssrEnvironment.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, config.root);
1176
1650
  const serverWatch = withWatchIgnores(config.server?.watch);
1177
1651
  return {
1178
- nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored),
1652
+ nitro: createNitroConfig(config.nitro, config.root, serverWatch.ignored, env?.command === "build", serverBuild),
1179
1653
  server: {
1180
1654
  ...config.server,
1181
1655
  watch: serverWatch
@@ -1197,9 +1671,13 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1197
1671
  resumeEntry,
1198
1672
  prerenderWakeEntry,
1199
1673
  navigationEntry,
1674
+ fragmentEntry,
1200
1675
  routePreloads
1201
1676
  ]) entry.base = config.base;
1202
1677
  routePreloads.root = config.root;
1678
+ const markless = (config.plugins ?? []).find((plugin) => typeof plugin.api?.chunkImportMap === "function");
1679
+ markless?.api.enableChunkImportMap?.();
1680
+ routePreloads.chunkSpecifiers = markless?.api.chunkImportMap() === true;
1203
1681
  },
1204
1682
  configEnvironment(_name, config) {
1205
1683
  configureRouteInputs(config);
@@ -1215,12 +1693,14 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1215
1693
  resumeEntry.fileName = void 0;
1216
1694
  prerenderWakeEntry.fileName = void 0;
1217
1695
  navigationEntry.fileName = void 0;
1696
+ fragmentEntry.fileName = void 0;
1218
1697
  routePreloads.routes = {
1219
1698
  navigation: {},
1220
1699
  ssr: {},
1221
1700
  styles: {}
1222
1701
  };
1223
1702
  routePreloads.persisted = false;
1703
+ routePreloads.importMap = void 0;
1224
1704
  await removeClientAssetsManifest(clientOutDir);
1225
1705
  return;
1226
1706
  }
@@ -1229,28 +1709,48 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1229
1709
  resumeEntry.fileName = clientAssetFileName(manifest.entries.resume, manifest.base);
1230
1710
  prerenderWakeEntry.fileName = manifest.entries.prerenderWake ? clientAssetFileName(manifest.entries.prerenderWake, manifest.base) : void 0;
1231
1711
  navigationEntry.fileName = clientAssetFileName(manifest.entries.navigation, manifest.base);
1712
+ fragmentEntry.fileName = manifest.entries.fragment ? clientAssetFileName(manifest.entries.fragment, manifest.base) : void 0;
1713
+ fragmentEntry.imports = manifest.entries.fragmentImports;
1232
1714
  routePreloads.routes = manifest.routes;
1233
1715
  routePreloads.persisted = true;
1716
+ routePreloads.importMap = manifest.importMap;
1234
1717
  },
1235
1718
  generateBundle: {
1236
1719
  order: "post",
1237
1720
  handler(_options, bundle) {
1238
- if (this.environment?.config.consumer !== "client") return;
1721
+ if (this.environment?.config.consumer !== "client") {
1722
+ if (this.environment?.config.consumer === "server") patchDocumentStylesheetsInBundle(bundle, documentStylesheetsFromBundle(bundle, routePreloads.root, routePreloads.base));
1723
+ return;
1724
+ }
1239
1725
  const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
1240
1726
  if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
1241
1727
  const prerenderWakeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, PRERENDER_WAKE_ENTRY_ORIGIN));
1242
1728
  if (prerenderWakeChunk) prerenderWakeEntry.fileName = prerenderWakeChunk.fileName;
1243
1729
  const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
1244
1730
  if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
1731
+ const fragmentChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, FRAGMENT_ENTRY_ORIGIN));
1732
+ if (fragmentChunk) {
1733
+ fragmentEntry.fileName = fragmentChunk.fileName;
1734
+ const everyChunk = new Map(outputChunks(bundle).map((chunk) => [chunk.fileName, chunk]));
1735
+ const closure = /* @__PURE__ */ new Set();
1736
+ includeChunk(closure, {
1737
+ chunks: everyChunk,
1738
+ everyChunk
1739
+ }, fragmentChunk.fileName);
1740
+ closure.delete(fragmentChunk.fileName);
1741
+ fragmentEntry.imports = [...closure].map((fileName) => joinURL(routePreloads.base, fileName));
1742
+ }
1245
1743
  routePreloads.routes = routeModulePreloadsFromBundle({
1246
1744
  base: routePreloads.base,
1247
1745
  bundle,
1248
1746
  navigationChunk,
1747
+ navigationPolyfillIds,
1249
1748
  prerenderWakeChunk,
1250
1749
  resumeChunk,
1251
1750
  root: routePreloads.root
1252
1751
  });
1253
1752
  patchRoutePreloadsInBundle(bundle, routePreloads.routes);
1753
+ capturedChunkNames = new Map(outputChunks(bundle).map((chunk) => [chunk.fileName, chunk.preliminaryFileName]));
1254
1754
  if (productionBuild) {
1255
1755
  const wakeRequired = prerenderWakeChannelEnabled();
1256
1756
  if (!resumeEntry.fileName || wakeRequired && !prerenderWakeEntry.fileName || !navigationEntry.fileName) throw new Error(wakeRequired ? "Markless Router client build did not emit its resume, prerender-wake, and navigation entries." : "Markless Router client build did not emit its resume and navigation entries.");
@@ -1259,16 +1759,38 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, prerenderWakeEn
1259
1759
  resumeEntry: joinURL(routePreloads.base, resumeEntry.fileName),
1260
1760
  ...prerenderWakeEntry.fileName ? { prerenderWakeEntry: joinURL(routePreloads.base, prerenderWakeEntry.fileName) } : {},
1261
1761
  navigationEntry: joinURL(routePreloads.base, navigationEntry.fileName),
1762
+ ...fragmentEntry.fileName ? {
1763
+ fragmentEntry: joinURL(routePreloads.base, fragmentEntry.fileName),
1764
+ fragmentImports: fragmentEntry.imports
1765
+ } : {},
1262
1766
  routes: routePreloads.routes
1263
1767
  });
1264
1768
  }
1265
1769
  }
1266
1770
  },
1267
- async writeBundle() {
1771
+ async writeBundle(options, bundle) {
1268
1772
  const environment = this.environment;
1269
1773
  const isClientEnvironment = environment?.name ? environment.name === clientEnvironmentName : environment?.config === clientEnvironmentConfig;
1774
+ if (productionBuild && environment?.name === "ssr") recordServerEntry(serverBuild, options.dir, bundle, (id) => id.startsWith(`\0${SERVER_ENTRY_ID}`));
1270
1775
  if (!productionBuild || !isClientEnvironment) return;
1271
1776
  if (!pendingClientAssets) throw new Error("Markless Router client build finished without a client-assets manifest.");
1777
+ const renames = bundle ? chunkRenamesSince(capturedChunkNames, bundle) : /* @__PURE__ */ new Map();
1778
+ if (renames.size > 0) {
1779
+ pendingClientAssets = renameClientAssetChunks(pendingClientAssets, renames);
1780
+ routePreloads.routes = pendingClientAssets.routes;
1781
+ for (const entry of [
1782
+ resumeEntry,
1783
+ prerenderWakeEntry,
1784
+ navigationEntry,
1785
+ fragmentEntry
1786
+ ]) if (entry.fileName) entry.fileName = renames.get(entry.fileName) ?? entry.fileName;
1787
+ fragmentEntry.imports = pendingClientAssets.entries.fragmentImports;
1788
+ }
1789
+ const importMap = bundle?.[MARKLESS_IMPORT_MAP_ASSET];
1790
+ if (importMap?.type === "asset") pendingClientAssets = {
1791
+ ...pendingClientAssets,
1792
+ importMap: JSON.parse(String(importMap.source))
1793
+ };
1272
1794
  await writeClientAssetsManifest(clientOutDir, pendingClientAssets);
1273
1795
  }
1274
1796
  };
@@ -1279,16 +1801,7 @@ function configureRouteInputs(config) {
1279
1801
  if (config.consumer === "client") {
1280
1802
  config.build.rolldownOptions.input = routerClientInput(config.build.rolldownOptions.input, configRoot(config));
1281
1803
  config.build.rolldownOptions.preserveEntrySignatures ??= "exports-only";
1282
- } else {
1283
- config.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, configRoot(config));
1284
- config.build.rolldownOptions.external = withExternal(config.build.rolldownOptions.external, "nitro");
1285
- }
1286
- }
1287
- function withExternal(external, id) {
1288
- if (Array.isArray(external)) return external.includes(id) ? external : [...external, id];
1289
- if (typeof external === "string") return external === id ? external : [external, id];
1290
- if (external === void 0) return [id];
1291
- return external;
1804
+ } else config.build.rolldownOptions.input ??= scopedVirtualEntryId(SERVER_ENTRY_ID, configRoot(config));
1292
1805
  }
1293
1806
  function requestFileTransformPlugin() {
1294
1807
  let root = "";
@@ -1315,6 +1828,7 @@ function requestFileIdForTransform(root, id) {
1315
1828
  if (!isAbsolute(pathname)) return;
1316
1829
  const relativeFileId = root ? withoutLeadingSlash(normalize(relative(root, pathname))) : withoutLeadingSlash(normalize(pathname));
1317
1830
  if (relativeFileId.startsWith("api/") || relativeFileId.startsWith("middleware/")) return relativeFileId;
1831
+ if (relativeFileId.startsWith("pages/")) return void 0;
1318
1832
  return relativeFileId.match(/(?:^|\/)((?:api|middleware)\/.+\.ts)$/)?.[1];
1319
1833
  }
1320
1834
  function requestFileBuildTransformPlugin(root) {
@@ -1331,7 +1845,7 @@ function requestFileBuildTransformPlugin(root) {
1331
1845
  }
1332
1846
  };
1333
1847
  }
1334
- function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = []) {
1848
+ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = [], isBuild = false, serverBuild = serverBuildState()) {
1335
1849
  const scanDirs = Array.isArray(nitroConfig?.scanDirs) ? nitroConfig.scanDirs.filter((dir) => typeof dir === "string") : [];
1336
1850
  const watchOptions = withWatchIgnores(nitroConfig?.watchOptions, serverWatchIgnored);
1337
1851
  const publicAssets = Array.isArray(nitroConfig?.publicAssets) ? nitroConfig.publicAssets : [];
@@ -1351,9 +1865,20 @@ function createNitroConfig(nitroConfig, root = ".", serverWatchIgnored = []) {
1351
1865
  ...publicAssets
1352
1866
  ],
1353
1867
  routesDir: nitroConfig?.routesDir ?? ".output/markless/router/nitro-routes",
1868
+ modules: [
1869
+ ...nitroConfig?.modules ?? [],
1870
+ fragmentRequestModule(),
1871
+ ...isBuild ? [prerenderRendererModule(serverBuild)] : []
1872
+ ],
1873
+ ...isBuild && { routeRules: {
1874
+ [`/${MARKLESS_BUILD_PREFIX}**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
1875
+ [`/_markless/links/**`]: { headers: { "cache-control": "public, max-age=31536000, immutable" } },
1876
+ ...Object.fromEntries(MARKLESS_PUBLIC_BUILD_METADATA_FILES.map((file) => [`/${file}`, { headers: { "cache-control": "public, max-age=0, must-revalidate" } }])),
1877
+ ...nitroConfig?.routeRules
1878
+ } },
1354
1879
  rolldownConfig: withRequestFileBuildPlugin(nitroConfig?.rolldownConfig, root),
1355
1880
  rollupConfig: withRequestFileBuildPlugin(nitroConfig?.rollupConfig, root),
1356
- scanDirs: [...new Set([".", ...scanDirs])],
1881
+ scanDirs: [.../* @__PURE__ */ new Set([".", ...scanDirs])],
1357
1882
  watchOptions
1358
1883
  };
1359
1884
  }
@@ -1400,6 +1925,7 @@ function resumeEntryState() {
1400
1925
  function routePreloadState() {
1401
1926
  return {
1402
1927
  base: "/",
1928
+ chunkSpecifiers: false,
1403
1929
  persisted: false,
1404
1930
  root: ".",
1405
1931
  routes: {
@@ -1417,33 +1943,40 @@ function routerClientInput(input, root) {
1417
1943
  const resumeEntryId = scopedVirtualEntryId(RESUME_ENTRY_ID, root);
1418
1944
  const prerenderWakeEntryId = scopedVirtualEntryId(PRERENDER_WAKE_ENTRY_ID, root);
1419
1945
  const navigationEntryId = scopedVirtualEntryId(NAVIGATION_ENTRY_ID, root);
1946
+ const fragmentEntryId = scopedVirtualEntryId(FRAGMENT_ENTRY_ID, root);
1420
1947
  const wakeEntries = prerenderWakeChannelEnabled() ? [prerenderWakeEntryId] : [];
1421
1948
  if (input === void 0) return [
1422
1949
  resumeEntryId,
1423
1950
  ...wakeEntries,
1424
- navigationEntryId
1951
+ navigationEntryId,
1952
+ fragmentEntryId
1425
1953
  ];
1426
1954
  if (typeof input === "string" || Array.isArray(input)) return [
1427
1955
  resumeEntryId,
1428
1956
  ...wakeEntries,
1429
1957
  navigationEntryId,
1958
+ fragmentEntryId,
1430
1959
  ...Array.isArray(input) ? input : [input]
1431
1960
  ];
1432
1961
  if (isRecord(input)) return {
1433
1962
  ...input,
1434
1963
  "markless-router-resume": resumeEntryId,
1435
1964
  ...prerenderWakeChannelEnabled() ? { "markless-router-prerender-wake": prerenderWakeEntryId } : {},
1436
- "markless-router-navigation": navigationEntryId
1965
+ "markless-router-navigation": navigationEntryId,
1966
+ "markless-router-fragment": fragmentEntryId
1437
1967
  };
1438
1968
  return input;
1439
1969
  }
1440
1970
  function isVirtualEntryChunk(chunk, origin) {
1441
1971
  return [chunk.facadeModuleId, ...chunk.moduleIds ?? []].some((id) => id ? decodePath(parseURL(id).pathname).endsWith(origin) : false);
1442
1972
  }
1443
- function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState()) {
1973
+ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEntry = resumeEntryState(), navigationEntry = resumeEntryState(), routePreloads = routePreloadState(), options = {}, fragmentEntry = resumeEntryState()) {
1444
1974
  let root = "";
1445
1975
  return {
1446
1976
  name: "markless-router:routes",
1977
+ config() {
1978
+ return { define: { __MARKLESS_ROUTER_LINK_INTENT__: JSON.stringify(linkIntentEnabled(options)) } };
1979
+ },
1447
1980
  sharedDuringBuild: true,
1448
1981
  configResolved(config) {
1449
1982
  root = config.root;
@@ -1452,48 +1985,148 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), prerenderWakeEnt
1452
1985
  filter: { id: PUBLIC_VIRTUAL_MODULE_ID_RE },
1453
1986
  handler(id) {
1454
1987
  const baseId = virtualModuleBaseId(id);
1455
- if (baseId === SERVER_ENTRY_ID || baseId === RESUME_ENTRY_PATH_ID || baseId === PRERENDER_WAKE_ENTRY_PATH_ID || baseId === NAVIGATION_ENTRY_PATH_ID || baseId === ROUTE_PRELOADS_ID || baseId === ROUTER_OPTIONS_ID) return `\0${baseId}${rootScopeQuery(root, id)}`;
1988
+ if (baseId === ROUTER_OPTIONS_ID) return {
1989
+ id: `\0${baseId}${rootScopeQuery(root, id)}`,
1990
+ moduleSideEffects: false
1991
+ };
1992
+ if (baseId === SERVER_ENTRY_ID || baseId === RESUME_ENTRY_PATH_ID || baseId === PRERENDER_WAKE_ENTRY_PATH_ID || baseId === NAVIGATION_ENTRY_PATH_ID || baseId === FRAGMENT_ENTRY_PATH_ID || baseId === ROUTE_PRELOADS_ID) return `\0${baseId}${rootScopeQuery(root, id)}`;
1456
1993
  const entryFile = virtualEntryFiles[baseId];
1457
1994
  if (!entryFile) return;
1995
+ if (baseId === ROUTE_HREF_ID) return join(VIRTUAL_ENTRY_DIR, entryFile);
1458
1996
  return `${join(VIRTUAL_ENTRY_DIR, entryFile)}${rootScopeQuery(root, id)}`;
1459
1997
  }
1460
1998
  },
1461
1999
  load(id) {
1462
- if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root);
2000
+ if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root, options);
1463
2001
  if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
1464
2002
  if (id.startsWith(`\0${PRERENDER_WAKE_ENTRY_PATH_ID}`)) {
1465
2003
  if (!prerenderWakeChannelEnabled() || !prerenderWakeEntry.fileName) return "export const prerenderWakeEntryPath = undefined;";
1466
2004
  return entryPathSource("prerenderWakeEntryPath", prerenderWakeEntry, PRERENDER_WAKE_ENTRY_ID, root);
1467
2005
  }
1468
2006
  if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
2007
+ if (id.startsWith(`\0${FRAGMENT_ENTRY_PATH_ID}`)) return entryPathSource("fragmentEntryPath", fragmentEntry, FRAGMENT_ENTRY_ID, root);
1469
2008
  if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
1470
- if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) return "export const routerMode = \"path\"; // retained for compat; hash paths are always first-class";
2009
+ if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) {
2010
+ const documentFile = join(root, "document.tsrx");
2011
+ this?.addWatchFile?.(documentFile);
2012
+ return routerOptionsSource(options, documentFile);
2013
+ }
1471
2014
  }
1472
2015
  };
1473
2016
  }
2017
+ async function routerOptionsSource(options, documentFile) {
2018
+ const documentSource = await readFile(documentFile, "utf8").catch(() => void 0);
2019
+ return [
2020
+ "export const routerMode = \"path\";",
2021
+ `export const documentNavigation = ${JSON.stringify(documentNavigationFor(documentSource, documentFile))};`,
2022
+ linkIntentEnabled(options) ? `export const startLinkIntentPreloading = (root, preload) => (${listenForLinkIntent.toString()})(root, ${JSON.stringify(LINK_ATTRIBUTE)}, preload, ${JSON.stringify(PREFETCH_ATTRIBUTE)});` : "export const startLinkIntentPreloading = () => () => {};"
2023
+ ].join("\n");
2024
+ }
2025
+ function linkIntentEnabled(options) {
2026
+ return options.prefetch !== false;
2027
+ }
1474
2028
  function entryPathSource(exportName, entry, entryId, root) {
1475
- const path = entry.fileName ? joinURL(entry.base, entry.fileName) : joinURL(entry.base, "@id", scopedVirtualEntryId(entryId, root));
1476
- return `export const ${exportName} = ${JSON.stringify(path)};`;
2029
+ return `export const ${exportName} = ${JSON.stringify(entryPath(entry, entryId, root))};`;
2030
+ }
2031
+ function entryPath(entry, entryId, root) {
2032
+ return entry.fileName ? joinURL(entry.base, entry.fileName) : joinURL(entry.base, "@id", scopedVirtualEntryId(entryId, root));
1477
2033
  }
1478
- function serverEntrySource(root) {
2034
+ function linkBridgeFilesPlugin(navigationEntry, fragmentEntry, options) {
2035
+ let root = "";
2036
+ let base = "/";
2037
+ let clientOutDir = "";
2038
+ let debug = false;
2039
+ let modules = [];
2040
+ return {
2041
+ name: "markless-router:link-bridge-files",
2042
+ apply: "build",
2043
+ sharedDuringBuild: true,
2044
+ configResolved(config) {
2045
+ root = config.root;
2046
+ base = config.base;
2047
+ debug = config.define?.__MARKLESS_DEBUG_ENABLED__ === "true";
2048
+ const client = Object.values(config.environments).find((environment) => environment.consumer === "client");
2049
+ if (client) clientOutDir = resolve(config.root, client.build.outDir);
2050
+ },
2051
+ generateBundle: {
2052
+ order: "post",
2053
+ async handler(_options, bundle) {
2054
+ if (this.environment?.name !== "ssr") return;
2055
+ const chunks = outputChunks(bundle);
2056
+ const target = chunks.find((chunk) => chunk.code?.includes(LINK_BRIDGE_PATHS_PLACEHOLDER));
2057
+ if (!target) return;
2058
+ if (!navigationEntry.fileName) throw new Error("Markless Router built its server entry before its navigation entry.");
2059
+ const documentFile = join(root, "document.tsrx");
2060
+ const documentSource = await readFile(documentFile, "utf8").catch(() => void 0);
2061
+ const built = routerLinkBridgeModules({
2062
+ base,
2063
+ navigationEntryPath: entryPath(navigationEntry, NAVIGATION_ENTRY_ID, root),
2064
+ fragmentEntryPath: entryPath(fragmentEntry, FRAGMENT_ENTRY_ID, root),
2065
+ fragmentEntryImports: fragmentEntry.imports,
2066
+ fragmentNavigationSource: minifiedFragmentNavigationSource(),
2067
+ fragmentNavigationRunSource: minifiedFragmentNavigationSource(runFragmentNavigation),
2068
+ prefetch: options.prefetch,
2069
+ documentNavigation: documentNavigationFor(documentSource, documentFile),
2070
+ routeFileIds: bundledPageFileIds(chunks, root),
2071
+ debug
2072
+ });
2073
+ modules = built;
2074
+ target.code = target.code.replace(LINK_BRIDGE_PATHS_PLACEHOLDER, jsStringLiteralContent(JSON.stringify(built.slice(0, 2).map((module) => module.path))));
2075
+ }
2076
+ },
2077
+ async writeBundle() {
2078
+ if (this.environment?.name !== "ssr" || modules.length === 0) return;
2079
+ const prefix = base.replace(/\/?$/, "/");
2080
+ for (const module of modules) {
2081
+ const file = join(clientOutDir, module.path.slice(prefix.length));
2082
+ await mkdir(dirname(file), { recursive: true });
2083
+ await writeFile(file, module.source, "utf8");
2084
+ }
2085
+ }
2086
+ };
2087
+ }
2088
+ function bundledPageFileIds(chunks, root) {
2089
+ const files = /* @__PURE__ */ new Set();
2090
+ for (const chunk of chunks) for (const id of chunk.moduleIds ?? []) {
2091
+ if (id.startsWith("\0")) continue;
2092
+ const pathname = decodePath(parseURL(id).pathname);
2093
+ if (!isAbsolute(pathname)) continue;
2094
+ const file = normalizeRouteFileId(relative(root, pathname));
2095
+ if (isPageModuleFile(file)) files.add(file);
2096
+ }
2097
+ return [...files];
2098
+ }
2099
+ function serverEntrySource(root, options) {
1479
2100
  const query = rootScopeQuery(root);
1480
2101
  return [
1481
2102
  `import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';`,
1482
2103
  `import { resumeEntryPath } from '${RESUME_ENTRY_PATH_ID}${query}';`,
1483
2104
  `import { prerenderWakeEntryPath } from '${PRERENDER_WAKE_ENTRY_PATH_ID}${query}';`,
1484
2105
  `import { navigationEntryPath } from '${NAVIGATION_ENTRY_PATH_ID}${query}';`,
1485
- `import { routeModulePreloads, routeSsrModulePreloads, routeStylesheets } from '${ROUTE_PRELOADS_ID}${query}';`,
2106
+ `import { fragmentEntryPath } from '${FRAGMENT_ENTRY_PATH_ID}${query}';`,
2107
+ `import { documentStylesheets, routeModulePreloads, routeSsrModulePreloads, routeStylesheets, documentImportMap } from '${ROUTE_PRELOADS_ID}${query}';`,
1486
2108
  `import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
2109
+ `import { documentNavigation } from '${ROUTER_OPTIONS_ID}${query}';`,
1487
2110
  `const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
2111
+ `const linkBridgePathsJson = globalThis.__marklessRouterLinkBridgePathsJson ?? ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)};`,
1488
2112
  `const entry = createServerEntry({`,
1489
2113
  ` dev: import.meta.env.DEV,`,
2114
+ ` linkBridgePaths: linkBridgePathsJson === ${JSON.stringify(LINK_BRIDGE_PATHS_PLACEHOLDER)} ? undefined : JSON.parse(linkBridgePathsJson),`,
2115
+ ` base: import.meta.env.BASE_URL,`,
1490
2116
  ` resumeEntryPath,`,
1491
2117
  ` prerenderWakeEntryPath,`,
1492
2118
  ` navigationEntryPath,`,
2119
+ ` fragmentEntryPath,`,
2120
+ ` fragmentNavigationSource: ${JSON.stringify(minifiedFragmentNavigationSource())},`,
2121
+ ` fragmentNavigationRunSource: ${JSON.stringify(minifiedFragmentNavigationSource(runFragmentNavigation))},`,
2122
+ ...options.prefetch === false ? [" prefetch: false,"] : [],
1493
2123
  ` routeModulePreloads,`,
1494
2124
  ` routeSsrModulePreloads,`,
1495
2125
  ` routeStylesheets,`,
2126
+ ` documentStylesheets,`,
2127
+ ` importMap: documentImportMap,`,
1496
2128
  ` documentModuleLoader: documentModuleLoaders['/document.tsrx'],`,
2129
+ ` documentNavigation,`,
1497
2130
  ` pageModuleLoaders,`,
1498
2131
  ` routeFileIds,`,
1499
2132
  `});`,
@@ -1501,9 +2134,21 @@ function serverEntrySource(root) {
1501
2134
  `export default entry;`
1502
2135
  ].join("\n");
1503
2136
  }
2137
+ function minifiedFragmentNavigationSource(source = startFragmentNavigation) {
2138
+ const result = minifySync("markless-fragment-navigation.js", `export default (${source.toString()});`, {
2139
+ compress: true,
2140
+ mangle: true,
2141
+ module: true
2142
+ });
2143
+ const prefix = "export default";
2144
+ const code = result.code.trim();
2145
+ if (result.errors.length > 0 || !code.startsWith(prefix)) throw new Error(`MARKLESS_ROUTER_FRAGMENT_MINIFY_FAILED: ${result.errors.map((error) => error.message).join("\n")}`);
2146
+ return code.slice(14).replace(/;$/, "").trim();
2147
+ }
1504
2148
  function rootScopeQuery(root, id = "") {
2149
+ const parsed = parsePath(id);
1505
2150
  const query = stringifyQuery({
1506
- ...parseQuery(parsePath(id).search),
2151
+ ...parseQuery(parsed.search),
1507
2152
  "markless-router-root": root || "."
1508
2153
  });
1509
2154
  return query ? `?${query}&lang.ts` : "";
@@ -1513,10 +2158,16 @@ function routePreloadsSource(state, client) {
1513
2158
  navigation: state.routes.navigation,
1514
2159
  ssr: state.routes.ssr
1515
2160
  } : state.routes;
1516
- const routeStylesheetExport = client ? [] : [`export const routeStylesheets = ${state.persisted ? "routePreloadData.styles ?? {}" : "undefined"};`];
2161
+ const routeStylesheetExport = client ? [] : [
2162
+ `export const routeStylesheets = ${state.persisted ? "routePreloadData.styles ?? {}" : "undefined"};`,
2163
+ `const documentStylesheetsJson = globalThis.__marklessRouterDocumentStylesheetsJson ?? ${JSON.stringify(DOCUMENT_STYLESHEETS_PLACEHOLDER)};`,
2164
+ `export const documentStylesheets = documentStylesheetsJson === ${JSON.stringify(DOCUMENT_STYLESHEETS_PLACEHOLDER)} ? [] : JSON.parse(documentStylesheetsJson);`,
2165
+ `export const documentImportMap = ${JSON.stringify(state.persisted && state.importMap ? importMapScript(state.importMap) : "")};`
2166
+ ];
1517
2167
  return [
1518
2168
  `const routePreloadsJson = globalThis.__marklessRouterRoutePreloadsJson ?? ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)};`,
1519
- `const routePreloadData = routePreloadsJson === ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)} ? ${JSON.stringify(routes)} : JSON.parse(routePreloadsJson);`,
2169
+ `let routePreloadData = routePreloadsJson === ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)} ? ${JSON.stringify(routes)} : JSON.parse(routePreloadsJson);`,
2170
+ routePreloadDecoderSource(client && state.chunkSpecifiers),
1520
2171
  `export const routeModulePreloads = routePreloadData.navigation ?? {};`,
1521
2172
  `export const routeSsrModulePreloads = routePreloadData.ssr ?? {};`,
1522
2173
  ...routeStylesheetExport,
@@ -1540,21 +2191,51 @@ function routePreloadsSource(state, client) {
1540
2191
  ].join("\n");
1541
2192
  }
1542
2193
  function patchRoutePreloadsInBundle(bundle, routes) {
1543
- const replacement = jsStringLiteralContent(JSON.stringify({
1544
- navigation: routes.navigation,
1545
- ssr: routes.ssr
1546
- }));
2194
+ const replacement = jsStringLiteralContent(JSON.stringify(compactRoutePreloadData(routes)));
1547
2195
  for (const chunk of outputChunks(bundle)) {
1548
2196
  if (!chunk.code?.includes(ROUTE_PRELOADS_PLACEHOLDER)) continue;
1549
2197
  chunk.code = chunk.code.replace(ROUTE_PRELOADS_PLACEHOLDER, replacement);
1550
2198
  }
1551
2199
  }
2200
+ function documentStylesheetsFromBundle(bundle, root, base) {
2201
+ const chunks = outputChunks(bundle);
2202
+ const chunksByFileName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
2203
+ const documentFile = join(root, "document.tsrx");
2204
+ const documentChunks = chunks.filter((chunk) => [chunk.facadeModuleId, ...chunk.moduleIds ?? []].some((id) => id !== null && id !== void 0 && decodePath(parseURL(id).pathname) === documentFile));
2205
+ if (documentChunks.length === 0) return [];
2206
+ return routeStylesheetsForChunks(documentChunks, new Set(documentChunks.map((chunk) => chunk.fileName)), chunksByFileName, base);
2207
+ }
2208
+ function patchDocumentStylesheetsInBundle(bundle, stylesheets) {
2209
+ const replacement = jsStringLiteralContent(JSON.stringify(stylesheets));
2210
+ for (const chunk of outputChunks(bundle)) {
2211
+ if (!chunk.code?.includes(DOCUMENT_STYLESHEETS_PLACEHOLDER)) continue;
2212
+ chunk.code = chunk.code.replace(DOCUMENT_STYLESHEETS_PLACEHOLDER, replacement);
2213
+ }
2214
+ }
1552
2215
  function jsStringLiteralContent(value) {
1553
2216
  return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("'", "\\'").replaceAll("`", "\\`").replaceAll("${", "\\${");
1554
2217
  }
1555
2218
  function routeModulePreloadsFromBundle(input) {
1556
- const chunks = outputChunks(input.bundle);
1557
- const chunksByFileName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
2219
+ const allChunks = outputChunks(input.bundle).filter((chunk) => !isExecutionLogChunk(chunk));
2220
+ const deferred = marklessDeferredChunkFileNames(allChunks);
2221
+ const chunks = allChunks.filter((chunk) => !deferred.has(chunk.fileName));
2222
+ const deferredChunks = allChunks.filter((chunk) => deferred.has(chunk.fileName));
2223
+ const everyChunk = new Map(allChunks.map((chunk) => [chunk.fileName, chunk]));
2224
+ const plannedLanding = plannedLandingFiles(input.bundle);
2225
+ const deferredFileNames = new Set(deferredChunks.map((chunk) => chunk.fileName));
2226
+ const resumeRuntimeFileNames = /* @__PURE__ */ new Set();
2227
+ for (const entry of [input.resumeChunk, input.prerenderWakeChunk]) if (entry) includeChunk(resumeRuntimeFileNames, {
2228
+ chunks: everyChunk,
2229
+ everyChunk
2230
+ }, entry.fileName);
2231
+ const lazyRuntimePacks = new Set([...resumeRuntimeFileNames].flatMap((fileName) => {
2232
+ const name = everyChunk.get(fileName)?.name;
2233
+ return name && lazyStartupPackName(name) !== name ? [lazyStartupPackName(name)] : [];
2234
+ }));
2235
+ for (const chunk of allChunks) if (chunk.name && lazyRuntimePacks.has(chunk.name)) includeChunk(resumeRuntimeFileNames, {
2236
+ chunks: everyChunk,
2237
+ everyChunk
2238
+ }, chunk.fileName);
1558
2239
  const routeChunks = /* @__PURE__ */ new Map();
1559
2240
  const routeChunkFileNames = /* @__PURE__ */ new Set();
1560
2241
  for (const chunk of chunks) {
@@ -1566,40 +2247,86 @@ function routeModulePreloadsFromBundle(input) {
1566
2247
  routeChunkFileNames.add(chunk.fileName);
1567
2248
  }
1568
2249
  const symbolChunks = symbolChunksBySourceFile(chunks);
2250
+ const resolverSourceFiles = new Set(allChunks.flatMap((chunk) => (chunk.moduleIds ?? []).flatMap((moduleId) => {
2251
+ const sourceFile = resolverVirtualModuleSourceFile(moduleId.startsWith("\0") ? moduleId.slice(1) : moduleId);
2252
+ return sourceFile ? [sourceModulePathname(sourceFile)] : [];
2253
+ })));
2254
+ const polyfillFileNames = new Set(chunks.filter((chunk) => chunk.facadeModuleId && input.navigationPolyfillIds?.has(chunk.facadeModuleId)).map((chunk) => chunk.fileName));
1569
2255
  const navigation = {};
1570
2256
  const ssr = {};
1571
2257
  const styles = {};
1572
2258
  for (const [routeFile, routeFileChunks] of routeChunks) {
2259
+ const currentRouteFiles = new Set(routeFileChunks.map((chunk) => chunk.fileName));
2260
+ const chunksByFileName = new Map(chunks.filter((chunk) => !routeChunkFileNames.has(chunk.fileName) || currentRouteFiles.has(chunk.fileName)).map((chunk) => [chunk.fileName, chunk]));
2261
+ const scope = {
2262
+ chunks: chunksByFileName,
2263
+ everyChunk
2264
+ };
2265
+ const landingScope = {
2266
+ chunks: new Map([...chunksByFileName].filter(([, chunk]) => !isMarklessNavigationPack(chunk.name))),
2267
+ everyChunk
2268
+ };
1573
2269
  const routeChunk = primaryRouteChunk(input.root, routeFile, routeFileChunks);
2270
+ const landingChunks = routeFileChunks.filter((chunk) => !isMarklessNavigationPack(chunk.name) && !isNavigationOnlyRouteChunk(input.root, routeFile, chunk));
2271
+ const landingChunk = landingChunks.length ? primaryRouteChunk(input.root, routeFile, landingChunks) : routeChunk;
1574
2272
  const navigationFileNames = /* @__PURE__ */ new Set();
1575
2273
  if (input.navigationChunk) {
1576
- includeChunk(navigationFileNames, chunksByFileName, input.navigationChunk.fileName);
1577
- for (const fileName of input.navigationChunk.dynamicImports) if (!routeChunkFileNames.has(fileName)) includeChunk(navigationFileNames, chunksByFileName, fileName);
2274
+ includeChunk(navigationFileNames, scope, input.navigationChunk.fileName);
2275
+ const renderPathChunks = new Map(chunksByFileName);
2276
+ for (const chunk of deferredChunks) renderPathChunks.set(chunk.fileName, chunk);
2277
+ const renderPath = /* @__PURE__ */ new Set();
2278
+ for (const fileName of input.navigationChunk.dynamicImports) if (!routeChunkFileNames.has(fileName) && !polyfillFileNames.has(fileName)) includeChunk(renderPath, scope, fileName, true);
2279
+ for (const fileName of renderPath) {
2280
+ navigationFileNames.add(fileName);
2281
+ if (resumeRuntimeFileNames.has(fileName)) continue;
2282
+ const chunk = everyChunk.get(fileName);
2283
+ for (const target of [...chunk.dynamicImports, ...codeDynamicImports(chunk)]) if (deferredFileNames.has(target)) includeChunk(navigationFileNames, {
2284
+ chunks: renderPathChunks,
2285
+ everyChunk
2286
+ }, target, true);
2287
+ }
1578
2288
  }
1579
- includeChunk(navigationFileNames, chunksByFileName, routeChunk.fileName);
1580
- includeChunk(navigationFileNames, chunksByFileName, routeChunk.fileName, true);
1581
- if (input.navigationChunk) for (const fileName of routeScopedDynamicImports(input.navigationChunk, routeFile)) includeChunk(navigationFileNames, chunksByFileName, fileName, true);
2289
+ includeChunk(navigationFileNames, scope, routeChunk.fileName);
2290
+ includeChunk(navigationFileNames, scope, routeChunk.fileName, true);
2291
+ if (input.navigationChunk) for (const fileName of routeScopedDynamicImports(input.navigationChunk, routeFile)) includeChunk(navigationFileNames, scope, fileName, true);
1582
2292
  const ssrFileNames = /* @__PURE__ */ new Set();
1583
2293
  if (input.resumeChunk) {
1584
- includeChunk(ssrFileNames, chunksByFileName, input.resumeChunk.fileName);
1585
- for (const fileName of routeScopedDynamicImports(input.resumeChunk, routeFile)) includeChunk(ssrFileNames, chunksByFileName, fileName, true);
2294
+ includeChunk(ssrFileNames, landingScope, input.resumeChunk.fileName);
2295
+ for (const fileName of routeScopedDynamicImports(input.resumeChunk, routeFile)) includeChunk(ssrFileNames, landingScope, fileName, true);
1586
2296
  }
1587
2297
  if (input.prerenderWakeChunk) {
1588
- includeChunk(ssrFileNames, chunksByFileName, input.prerenderWakeChunk.fileName);
1589
- for (const fileName of routeScopedDynamicImports(input.prerenderWakeChunk, routeFile)) includeChunk(ssrFileNames, chunksByFileName, fileName, true);
2298
+ includeChunk(ssrFileNames, landingScope, input.prerenderWakeChunk.fileName);
2299
+ for (const fileName of routeScopedDynamicImports(input.prerenderWakeChunk, routeFile)) includeChunk(ssrFileNames, landingScope, fileName, true);
1590
2300
  }
1591
- includeChunk(ssrFileNames, chunksByFileName, routeChunk.fileName, true);
1592
- const routeSourceFiles = sourceFilesForChunks(new Set([...navigationFileNames, ...ssrFileNames]), chunksByFileName);
2301
+ includeChunk(ssrFileNames, landingScope, landingChunk.fileName, true);
2302
+ const routeSourceFiles = sourceFilesForChunks(/* @__PURE__ */ new Set([...navigationFileNames, ...ssrFileNames]), chunksByFileName);
2303
+ let routeHasSymbols = false;
1593
2304
  for (const [sourceFile, symbolFileNames] of symbolChunks) {
1594
2305
  if (!routeSourceFiles.has(sourceFile)) continue;
2306
+ routeHasSymbols = true;
1595
2307
  for (const fileName of symbolFileNames) {
1596
- includeChunk(navigationFileNames, chunksByFileName, fileName, true);
1597
- includeChunk(ssrFileNames, chunksByFileName, fileName, true);
2308
+ includeChunk(navigationFileNames, scope, fileName, true);
2309
+ includeChunk(ssrFileNames, landingScope, fileName, true);
1598
2310
  }
1599
2311
  }
2312
+ const styleFileNames = /* @__PURE__ */ new Set([...navigationFileNames, ...ssrFileNames]);
2313
+ const planned = plannedLanding?.get(routeFile);
2314
+ if (planned) narrowToPlanned(ssrFileNames, planned, landingScope, [input.resumeChunk, input.prerenderWakeChunk]);
2315
+ for (const fileName of ssrFileNames) for (const specifier of warmEvaluatedImports(everyChunk.get(fileName)?.code ?? "")) includeChunk(ssrFileNames, landingScope, normalize(join(dirname(fileName), specifier)));
1600
2316
  navigation[routeFile] = [...navigationFileNames].map((fileName) => joinURL(input.base, fileName));
1601
- ssr[routeFile] = [...ssrFileNames].map((fileName) => joinURL(input.base, fileName));
1602
- styles[routeFile] = routeStylesheetsForChunks(routeFileChunks, chunksByFileName, input.base);
2317
+ const firstEventPath = /* @__PURE__ */ new Set();
2318
+ const lazySiblings = [...ssrFileNames].filter((fileName) => {
2319
+ const name = everyChunk.get(fileName)?.name;
2320
+ return !!name && startupPackOfLazy(name) !== name;
2321
+ });
2322
+ for (const entry of lazySiblings.length ? [input.resumeChunk, input.prerenderWakeChunk] : []) {
2323
+ if (!entry) continue;
2324
+ includeChunk(firstEventPath, landingScope, entry.fileName);
2325
+ for (const fileName of routeScopedDynamicImports(entry, routeFile)) includeChunk(firstEventPath, landingScope, fileName);
2326
+ }
2327
+ for (const fileName of lazySiblings) includeChunk(firstEventPath, landingScope, fileName);
2328
+ ssr[routeFile] = !routeHasSymbols && [...routeSourceFiles].some((file) => resolverSourceFiles.has(file)) ? [] : [...[...firstEventPath].filter((fileName) => ssrFileNames.has(fileName)), ...[...ssrFileNames].filter((fileName) => !firstEventPath.has(fileName))].map((fileName) => joinURL(input.base, fileName));
2329
+ styles[routeFile] = routeStylesheetsForChunks(routeFileChunks, styleFileNames, chunksByFileName, input.base);
1603
2330
  }
1604
2331
  return {
1605
2332
  navigation,
@@ -1607,7 +2334,7 @@ function routeModulePreloadsFromBundle(input) {
1607
2334
  styles
1608
2335
  };
1609
2336
  }
1610
- function routeStylesheetsForChunks(routeFileChunks, chunksByFileName, base) {
2337
+ function routeStylesheetsForChunks(routeFileChunks, closureFileNames, chunksByFileName, base) {
1611
2338
  const styles = /* @__PURE__ */ new Set();
1612
2339
  const visited = /* @__PURE__ */ new Set();
1613
2340
  const visit = (chunk) => {
@@ -1619,18 +2346,25 @@ function routeStylesheetsForChunks(routeFileChunks, chunksByFileName, base) {
1619
2346
  }
1620
2347
  for (const stylesheet of chunk.viteMetadata?.importedCss ?? []) styles.add(joinURL(base, stylesheet));
1621
2348
  };
2349
+ for (const fileName of closureFileNames) {
2350
+ const chunk = chunksByFileName.get(fileName);
2351
+ if (chunk) visit(chunk);
2352
+ }
1622
2353
  for (const chunk of routeFileChunks) visit(chunk);
1623
2354
  return [...styles];
1624
2355
  }
1625
2356
  function symbolChunksBySourceFile(chunks) {
1626
2357
  const bySourceFile = /* @__PURE__ */ new Map();
1627
- for (const chunk of chunks) for (const moduleId of chunk.moduleIds ?? []) {
1628
- const sourceFile = symbolVirtualModuleSourceFile(moduleId);
1629
- if (!sourceFile) continue;
1630
- const key = sourceModulePathname(sourceFile);
1631
- const fileNames = bySourceFile.get(key) ?? [];
1632
- if (!fileNames.includes(chunk.fileName)) fileNames.push(chunk.fileName);
1633
- bySourceFile.set(key, fileNames);
2358
+ for (const chunk of chunks) {
2359
+ const moduleIds = chunk.facadeModuleId ? [...chunk.moduleIds ?? [], chunk.facadeModuleId] : chunk.moduleIds ?? [];
2360
+ for (const moduleId of moduleIds) {
2361
+ const sourceFile = symbolVirtualModuleSourceFile(moduleId);
2362
+ if (!sourceFile) continue;
2363
+ const key = sourceModulePathname(sourceFile);
2364
+ const fileNames = bySourceFile.get(key) ?? [];
2365
+ if (!fileNames.includes(chunk.fileName)) fileNames.push(chunk.fileName);
2366
+ bySourceFile.set(key, fileNames);
2367
+ }
1634
2368
  }
1635
2369
  return bySourceFile;
1636
2370
  }
@@ -1646,22 +2380,23 @@ function sourceFilesForChunks(fileNames, chunksByFileName) {
1646
2380
  function sourceModulePathname(moduleId) {
1647
2381
  return decodePath(parseURL(moduleId).pathname);
1648
2382
  }
1649
- function includeChunk(fileNames, chunksByFileName, fileName, includeDynamic = false, visitedDynamic = /* @__PURE__ */ new Set()) {
1650
- if (!fileNames.has(fileName)) fileNames.add(fileName);
1651
- const chunk = chunksByFileName.get(fileName);
2383
+ function includeChunk(fileNames, scope, fileName, includeDynamic = false, visited = /* @__PURE__ */ new Set(), requiredByStaticImport = false) {
2384
+ if (visited.has(fileName)) return;
2385
+ const chunk = (requiredByStaticImport ? scope.everyChunk : scope.chunks).get(fileName);
1652
2386
  if (!chunk) return;
1653
- for (const imported of chunk.imports) includeChunk(fileNames, chunksByFileName, imported, includeDynamic, visitedDynamic);
1654
- for (const imported of codeStaticImports(chunk)) {
1655
- if (!chunksByFileName.has(imported)) continue;
1656
- includeChunk(fileNames, chunksByFileName, imported, includeDynamic, visitedDynamic);
1657
- }
1658
- if (!includeDynamic || visitedDynamic.has(fileName)) return;
1659
- visitedDynamic.add(fileName);
1660
- for (const imported of chunk.dynamicImports) includeChunk(fileNames, chunksByFileName, imported, true, visitedDynamic);
1661
- for (const imported of codeDynamicImports(chunk)) {
1662
- if (!chunksByFileName.has(imported)) continue;
1663
- includeChunk(fileNames, chunksByFileName, imported, true, visitedDynamic);
1664
- }
2387
+ visited.add(fileName);
2388
+ fileNames.add(fileName);
2389
+ for (const imported of /* @__PURE__ */ new Set([...chunk.imports, ...codeStaticImports(chunk)])) includeChunk(fileNames, scope, imported, includeDynamic, visited, true);
2390
+ if (!includeDynamic) return;
2391
+ for (const imported of /* @__PURE__ */ new Set([...chunk.dynamicImports, ...codeDynamicImports(chunk)])) includeChunk(fileNames, scope, imported, true, visited);
2392
+ }
2393
+ function narrowToPlanned(fileNames, planned, scope, entries) {
2394
+ const kept = /* @__PURE__ */ new Set();
2395
+ for (const entry of entries) if (entry) includeChunk(kept, scope, entry.fileName);
2396
+ for (const fileName of fileNames) if (planned.has(fileName)) includeChunk(kept, scope, fileName, false, /* @__PURE__ */ new Set(), true);
2397
+ const ordered = [...fileNames].filter((fileName) => kept.has(fileName));
2398
+ fileNames.clear();
2399
+ for (const fileName of [...ordered, ...kept]) fileNames.add(fileName);
1665
2400
  }
1666
2401
  function routeScopedDynamicImports(chunk, routeFile) {
1667
2402
  if (!chunk.code) return [];
@@ -1675,10 +2410,23 @@ function routeScopedDynamicImports(chunk, routeFile) {
1675
2410
  return [...imports];
1676
2411
  }
1677
2412
  function codeStaticImports(chunk) {
1678
- return codeImportSpecifiers(chunk, /(?:import\s*(?:[^('"`]*?\bfrom\s*)?|export\s*[^('"`]*?\bfrom\s*)["'](\.\/[^"']+\.js)["']/g);
2413
+ return cachedCodeImports(chunk).static;
1679
2414
  }
1680
2415
  function codeDynamicImports(chunk) {
1681
- return codeDynamicImportSpecifiers(chunk.code ?? "").map((specifier) => normalize(join(dirname(chunk.fileName), specifier)));
2416
+ return cachedCodeImports(chunk).dynamic;
2417
+ }
2418
+ const chunkCodeImports = /* @__PURE__ */ new WeakMap();
2419
+ function cachedCodeImports(chunk) {
2420
+ const cached = chunkCodeImports.get(chunk);
2421
+ if (cached?.code === chunk.code && cached?.fileName === chunk.fileName) return cached;
2422
+ const imports = {
2423
+ code: chunk.code,
2424
+ fileName: chunk.fileName,
2425
+ static: codeImportSpecifiers(chunk, /(?:import\s*(?:[^('"`]*?\bfrom\s*)?|export\s*[^('"`]*?\bfrom\s*)["'](\.\/[^"']+\.js)["']/g),
2426
+ dynamic: codeDynamicImportSpecifiers(chunk.code ?? "").map((specifier) => normalize(join(dirname(chunk.fileName), specifier)))
2427
+ };
2428
+ chunkCodeImports.set(chunk, imports);
2429
+ return imports;
1682
2430
  }
1683
2431
  function codeDynamicImportSpecifiers(code) {
1684
2432
  const imports = /* @__PURE__ */ new Set();
@@ -1697,6 +2445,15 @@ function codeImportSpecifiers(chunk, pattern) {
1697
2445
  }
1698
2446
  return [...imports];
1699
2447
  }
2448
+ function chunkRenamesSince(captured, bundle) {
2449
+ const finalNames = new Map(outputChunks(bundle).map((chunk) => [chunk.preliminaryFileName, chunk.fileName]));
2450
+ const renames = /* @__PURE__ */ new Map();
2451
+ for (const [fileName, preliminary] of captured) {
2452
+ const next = preliminary === void 0 ? void 0 : finalNames.get(preliminary);
2453
+ if (next !== void 0 && next !== fileName) renames.set(fileName, next);
2454
+ }
2455
+ return renames;
2456
+ }
1700
2457
  function outputChunks(bundle) {
1701
2458
  return Object.values(bundle).filter((item) => {
1702
2459
  return isRecord(item) && item.type === "chunk" && typeof item.fileName === "string" && Array.isArray(item.imports) && Array.isArray(item.dynamicImports);
@@ -1709,24 +2466,26 @@ function routeFileForChunk(root, chunk) {
1709
2466
  }
1710
2467
  }
1711
2468
  function primaryRouteChunk(root, routeFile, chunks) {
1712
- let selected = chunks[0];
1713
- let selectedRank = routeChunkRank(root, routeFile, selected);
1714
- for (const chunk of chunks.slice(1)) {
1715
- const rank = routeChunkRank(root, routeFile, chunk);
1716
- if (rank <= selectedRank) continue;
1717
- selected = chunk;
1718
- selectedRank = rank;
1719
- }
1720
- return selected;
2469
+ const ranked = chunks.map((chunk) => ({
2470
+ chunk,
2471
+ rank: routeChunkRank(root, routeFile, chunk)
2472
+ }));
2473
+ const highestRank = Math.max(...ranked.map(({ rank }) => rank));
2474
+ const candidates = ranked.filter(({ rank }) => rank === highestRank);
2475
+ if (candidates.length !== 1) throw new Error(`Markless Router found ambiguous primary chunks for ${routeFile}: ${candidates.map(({ chunk }) => chunk.fileName).sort().join(", ")}`);
2476
+ return candidates[0].chunk;
2477
+ }
2478
+ function isNavigationOnlyRouteChunk(root, routeFile, chunk) {
2479
+ const owned = [chunk.facadeModuleId, ...chunk.moduleIds ?? []].filter((moduleId) => !!moduleId && routeFileForModuleId(root, moduleId) === routeFile);
2480
+ return owned.length > 0 && owned.every(isRouteNavigationSourceRequest);
1721
2481
  }
1722
2482
  function routeChunkRank(root, routeFile, chunk) {
1723
2483
  let rank = 0;
1724
2484
  for (const moduleId of [chunk.facadeModuleId, ...chunk.moduleIds ?? []]) {
1725
- if (routeFileForModuleId(root, moduleId) !== routeFile) continue;
1726
- const search = parsePath(moduleId).search;
1727
- if (/(?:^|[?&])markless-symbols(?:[=&]|$)/.test(search)) rank = Math.max(rank, 1);
1728
- else if (/(?:^|[?&])markless-resume(?:[=&]|$)/.test(search)) rank = Math.max(rank, 2);
1729
- else return 3;
2485
+ if (!moduleId || routeFileForModuleId(root, moduleId) !== routeFile) continue;
2486
+ const role = isClientPrimarySourceRequest(moduleId) ? 5 : isRenderDataSourceRequest(moduleId) ? 4 : isSymbolOnlySourceRequest(moduleId) ? 1 : isResumeSourceRequest(moduleId) ? 2 : 3;
2487
+ const ownsModule = chunk.moduleIds?.includes(moduleId) ? 1 : 0;
2488
+ rank = Math.max(rank, role * 2 + ownsModule);
1730
2489
  }
1731
2490
  return rank;
1732
2491
  }
@@ -1747,5 +2506,8 @@ function configRoot(config) {
1747
2506
  const root = config.root;
1748
2507
  return typeof root === "string" ? root : ".";
1749
2508
  }
2509
+ function escapeRegExp(value) {
2510
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2511
+ }
1750
2512
  //#endregion
1751
2513
  export { router };