@reckona/mreact-router 0.0.81 → 0.0.83

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.
Files changed (81) hide show
  1. package/README.md +2 -0
  2. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  3. package/dist/adapters/aws-lambda.js +17 -22
  4. package/dist/adapters/aws-lambda.js.map +1 -1
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +3 -1
  7. package/dist/build.js.map +1 -1
  8. package/dist/client-route-inference.d.ts +2 -0
  9. package/dist/client-route-inference.d.ts.map +1 -0
  10. package/dist/client-route-inference.js +2 -0
  11. package/dist/client-route-inference.js.map +1 -0
  12. package/dist/client.d.ts +2 -1
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +89 -24
  15. package/dist/client.js.map +1 -1
  16. package/dist/layout-composer.d.ts +24 -0
  17. package/dist/layout-composer.d.ts.map +1 -0
  18. package/dist/layout-composer.js +83 -0
  19. package/dist/layout-composer.js.map +1 -0
  20. package/dist/metadata.d.ts +9 -0
  21. package/dist/metadata.d.ts.map +1 -0
  22. package/dist/metadata.js +419 -0
  23. package/dist/metadata.js.map +1 -0
  24. package/dist/middleware.d.ts +22 -0
  25. package/dist/middleware.d.ts.map +1 -0
  26. package/dist/middleware.js +94 -0
  27. package/dist/middleware.js.map +1 -0
  28. package/dist/module-runner.d.ts +2 -2
  29. package/dist/module-runner.d.ts.map +1 -1
  30. package/dist/module-runner.js +1 -1
  31. package/dist/module-runner.js.map +1 -1
  32. package/dist/navigation-runtime.d.ts +2 -0
  33. package/dist/navigation-runtime.d.ts.map +1 -0
  34. package/dist/navigation-runtime.js +2 -0
  35. package/dist/navigation-runtime.js.map +1 -0
  36. package/dist/preload-policy.d.ts +24 -0
  37. package/dist/preload-policy.d.ts.map +1 -0
  38. package/dist/preload-policy.js +45 -0
  39. package/dist/preload-policy.js.map +1 -0
  40. package/dist/render.d.ts +1 -3
  41. package/dist/render.d.ts.map +1 -1
  42. package/dist/render.js +32 -676
  43. package/dist/render.js.map +1 -1
  44. package/dist/route-hydration-contract.d.ts +11 -0
  45. package/dist/route-hydration-contract.d.ts.map +1 -0
  46. package/dist/route-hydration-contract.js +21 -0
  47. package/dist/route-hydration-contract.js.map +1 -0
  48. package/dist/route-module-loader.d.ts +10 -0
  49. package/dist/route-module-loader.d.ts.map +1 -0
  50. package/dist/route-module-loader.js +61 -0
  51. package/dist/route-module-loader.js.map +1 -0
  52. package/dist/routes.js +6 -0
  53. package/dist/routes.js.map +1 -1
  54. package/dist/serve.d.ts +13 -0
  55. package/dist/serve.d.ts.map +1 -1
  56. package/dist/serve.js +63 -28
  57. package/dist/serve.js.map +1 -1
  58. package/dist/server-action-inference.d.ts.map +1 -1
  59. package/dist/server-action-inference.js +37 -1
  60. package/dist/server-action-inference.js.map +1 -1
  61. package/dist/vite.d.ts.map +1 -1
  62. package/dist/vite.js +2 -1
  63. package/dist/vite.js.map +1 -1
  64. package/package.json +11 -11
  65. package/src/adapters/aws-lambda.ts +21 -28
  66. package/src/build.ts +9 -6
  67. package/src/client-route-inference.ts +18 -0
  68. package/src/client.ts +124 -24
  69. package/src/layout-composer.ts +142 -0
  70. package/src/metadata.ts +578 -0
  71. package/src/middleware.ts +153 -0
  72. package/src/module-runner.ts +3 -2
  73. package/src/navigation-runtime.ts +16 -0
  74. package/src/preload-policy.ts +89 -0
  75. package/src/render.ts +75 -986
  76. package/src/route-hydration-contract.ts +22 -0
  77. package/src/route-module-loader.ts +95 -0
  78. package/src/routes.ts +8 -0
  79. package/src/serve.ts +110 -29
  80. package/src/server-action-inference.ts +49 -0
  81. package/src/vite.ts +6 -4
@@ -0,0 +1,18 @@
1
+ export {
2
+ collectClientRouteReferences,
3
+ compilerModuleContextForSource,
4
+ createClientRouteInferenceCache,
5
+ detectClientNavigationHint,
6
+ detectNavigationRuntimeHint,
7
+ formatClientRouteInferenceDiagnostic,
8
+ inferClientRouteModule,
9
+ isClientRouteModule,
10
+ isClientRouteSource,
11
+ routeToClientManifestEntry,
12
+ type ClientReferenceImport,
13
+ type ClientRouteInferenceCache,
14
+ type ClientRouteInferenceDiagnostic,
15
+ type ClientRouteInferenceResult,
16
+ type ClientRouteManifestEntry,
17
+ type ClientRouteReferenceResult,
18
+ } from "./client.js";
package/src/client.ts CHANGED
@@ -3,15 +3,17 @@ import { readFile, stat } from "node:fs/promises";
3
3
  import { builtinModules } from "node:module";
4
4
  import { dirname, extname, join, relative, sep } from "node:path";
5
5
  import {
6
+ analyzeBoundaryGraph,
6
7
  collectClientRouteModuleAnalysis,
7
8
  formatDiagnostic,
8
9
  type ComponentMetadata,
10
+ type BoundaryGraphResult,
9
11
  type ClientRouteModuleAnalysis,
10
12
  type ClientRouteStaticImportReference,
11
- type ClientReferenceMetadata,
12
13
  type StaticImportReference,
13
14
  type TopLevelExportRenderInfo,
14
15
  } from "@reckona/mreact-compiler";
16
+ import type { ClientReferenceMetadata } from "@reckona/mreact-shared/compiler-contract";
15
17
  import {
16
18
  collectClientRouteModuleAnalysisFromContext,
17
19
  createCompilerModuleContext,
@@ -29,6 +31,11 @@ import {
29
31
  } from "./bundle-pipeline.js";
30
32
  import type { AppRoute } from "./routes.js";
31
33
  import { existingRouteShellCandidates } from "./route-shells.js";
34
+ import {
35
+ routeDataScriptIds,
36
+ routeDataScriptSelector,
37
+ routeHydrationContract,
38
+ } from "./route-hydration-contract.js";
32
39
  import { stripRouteClientSource } from "./route-source.js";
33
40
  import { hasJsxSyntax } from "./source-jsx.js";
34
41
  import { sourceModuleCandidates } from "./source-modules.js";
@@ -103,6 +110,8 @@ export interface ClientRouteInferenceResult {
103
110
  }
104
111
 
105
112
  interface ClientRouteModuleInferenceResult extends ClientRouteInferenceResult {
113
+ boundaryGraphFallbackCandidate: boolean;
114
+ boundaryGraphFallbackRequired: boolean;
106
115
  clientBoundaryExportNames: string[];
107
116
  clientBoundaryModule: boolean;
108
117
  nestedClientExportNames: string[];
@@ -211,9 +220,20 @@ export async function inferClientRouteModule(options: {
211
220
  seen: new Set(),
212
221
  sourceTransform,
213
222
  });
223
+ const mergedRouteInference = routeInference.boundaryGraphFallbackRequired
224
+ ? mergeClientRouteInference(
225
+ routeInference,
226
+ await inferClientRouteModuleBoundaryGraph({
227
+ cache,
228
+ code,
229
+ filename: options.filename,
230
+ sourceTransform,
231
+ }),
232
+ )
233
+ : routeInference;
214
234
 
215
235
  if (options.appDir === undefined) {
216
- return routeInference;
236
+ return mergedRouteInference;
217
237
  }
218
238
 
219
239
  const shellInferences = await inferClientRouteShellModules({
@@ -224,10 +244,10 @@ export async function inferClientRouteModule(options: {
224
244
  });
225
245
 
226
246
  return {
227
- client: routeInference.client || shellInferences.some((inference) => inference.client),
228
- clientBoundaryImports: routeInference.clientBoundaryImports,
247
+ client: mergedRouteInference.client || shellInferences.some((inference) => inference.client),
248
+ clientBoundaryImports: mergedRouteInference.clientBoundaryImports,
229
249
  diagnostics: [
230
- ...routeInference.diagnostics,
250
+ ...mergedRouteInference.diagnostics,
231
251
  ...shellInferences.flatMap((inference) => inference.diagnostics),
232
252
  ],
233
253
  };
@@ -239,6 +259,68 @@ export async function inferClientRouteModule(options: {
239
259
  }
240
260
  }
241
261
 
262
+ async function inferClientRouteModuleBoundaryGraph(options: {
263
+ cache: ClientRouteInferenceCache;
264
+ code: string;
265
+ filename: string;
266
+ sourceTransform?: ClientRouteSourceTransform | undefined;
267
+ }): Promise<ClientRouteInferenceResult> {
268
+ const graph = await analyzeBoundaryGraph({
269
+ entries: [{ file: options.filename, kind: "route-page" }],
270
+ readModule: async (file) => {
271
+ if (file === options.filename) {
272
+ return options.code;
273
+ }
274
+
275
+ return await readClientRouteSource({
276
+ cache: options.cache,
277
+ filename: file,
278
+ sourceTransform: options.sourceTransform,
279
+ });
280
+ },
281
+ resolveModule: async ({ importer, source }) =>
282
+ await resolveAppLocalModule({
283
+ allowExplicitNonSource: options.sourceTransform !== undefined,
284
+ cache: options.cache,
285
+ importer,
286
+ specifier: source,
287
+ }),
288
+ });
289
+
290
+ return clientRouteInferenceFromBoundaryGraph(graph, options.filename);
291
+ }
292
+
293
+ function clientRouteInferenceFromBoundaryGraph(
294
+ graph: BoundaryGraphResult,
295
+ filename: string,
296
+ ): ClientRouteInferenceResult {
297
+ const clientBoundaryImports = graph.clientBoundaries
298
+ .filter((boundary) => boundary.importerFile === filename)
299
+ .map((boundary) => boundary.source);
300
+ const clientRoute = graph.modules.some(
301
+ (module) => module.file === filename && module.classification === "client-route",
302
+ );
303
+
304
+ return {
305
+ client: clientRoute || clientBoundaryImports.length > 0,
306
+ clientBoundaryImports,
307
+ diagnostics: [],
308
+ };
309
+ }
310
+
311
+ function mergeClientRouteInference(
312
+ left: ClientRouteInferenceResult,
313
+ right: ClientRouteInferenceResult,
314
+ ): ClientRouteInferenceResult {
315
+ return {
316
+ client: left.client || right.client,
317
+ clientBoundaryImports: Array.from(
318
+ new Set([...left.clientBoundaryImports, ...right.clientBoundaryImports]),
319
+ ),
320
+ diagnostics: [...left.diagnostics, ...right.diagnostics],
321
+ };
322
+ }
323
+
242
324
  export async function collectClientRouteReferences(options: {
243
325
  appDir?: string | undefined;
244
326
  cache?: ClientRouteInferenceCache | undefined;
@@ -534,6 +616,7 @@ async function inferClientRouteModuleSource(options: {
534
616
  const nestedClientExportNames = new Set<string>();
535
617
  const clientReferenceSourceFiles: string[] = [];
536
618
  const diagnostics: ClientRouteInferenceDiagnostic[] = [];
619
+ let boundaryGraphFallbackRequired = false;
537
620
  let clientProxy = false;
538
621
  let nestedClient = false;
539
622
  const exportInfo = analysis.topLevelExportRenderInfo;
@@ -605,6 +688,9 @@ async function inferClientRouteModuleSource(options: {
605
688
  diagnostics.push(...imported.diagnostics);
606
689
 
607
690
  if (!imported.client) {
691
+ if (rendered && imported.boundaryGraphFallbackCandidate) {
692
+ boundaryGraphFallbackRequired = true;
693
+ }
608
694
  if (imported.serverOnlyClientRuntime && rendered) {
609
695
  diagnostics.push(
610
696
  serverOnlyClientImportReferenceDiagnostic({
@@ -706,11 +792,16 @@ async function inferClientRouteModuleSource(options: {
706
792
  } else if (exported.client) {
707
793
  nestedClient = true;
708
794
  clientReferenceSourceFiles.push(resolved);
795
+ } else if (exported.boundaryGraphFallbackCandidate) {
796
+ boundaryGraphFallbackRequired = true;
709
797
  }
710
798
  }
711
799
  }
712
800
 
713
801
  return {
802
+ boundaryGraphFallbackCandidate:
803
+ analysis.staticExports.length > 0 || boundaryGraphFallbackRequired,
804
+ boundaryGraphFallbackRequired,
714
805
  client:
715
806
  clientBoundaryImports.length > 0 ||
716
807
  clientBoundaryExportNames.size > 0 ||
@@ -735,6 +826,8 @@ function emptyClientRouteModuleInferenceResult(
735
826
  overrides: Partial<ClientRouteModuleInferenceResult> = {},
736
827
  ): ClientRouteModuleInferenceResult {
737
828
  return {
829
+ boundaryGraphFallbackCandidate: false,
830
+ boundaryGraphFallbackRequired: false,
738
831
  client: false,
739
832
  clientBoundaryImports: [],
740
833
  clientBoundaryExportNames: [],
@@ -1287,7 +1380,7 @@ export function withHydrationMarkers(options: {
1287
1380
  export function withRouteMarkers(options: { html: string; routePath: string }): string {
1288
1381
  const routeId = routeIdForPath(options.routePath);
1289
1382
 
1290
- return `<div data-mreact-route-id="${escapeHtmlAttribute(routeId)}">${options.html}</div>`;
1383
+ return `<div ${routeHydrationContract.routeMarkerAttribute}="${escapeHtmlAttribute(routeId)}">${options.html}</div>`;
1291
1384
  }
1292
1385
 
1293
1386
  export function hydrationMarkerParts(options: {
@@ -1299,6 +1392,9 @@ export function hydrationMarkerParts(options: {
1299
1392
  }): { prefix: string; suffix: string } {
1300
1393
  const routeId = routeIdForPath(options.routePath);
1301
1394
  const escapedRouteId = escapeHtmlAttribute(routeId);
1395
+ const [propsScriptId, clientReferencesScriptId] = routeDataScriptIds(routeId).map((id) =>
1396
+ escapeHtmlAttribute(id),
1397
+ );
1302
1398
  const propsJson = escapeScriptJson(JSON.stringify(options.props));
1303
1399
  const script = options.script ?? clientScriptForPath(options.routePath);
1304
1400
  const scriptSrc = assetPath(script, options.assetBaseUrl ?? "/_mreact/client/");
@@ -1308,13 +1404,13 @@ export function hydrationMarkerParts(options: {
1308
1404
  : escapeScriptJson(JSON.stringify(options.clientReferenceManifest));
1309
1405
 
1310
1406
  return {
1311
- prefix: `<div data-mreact-route-id="${escapedRouteId}">`,
1407
+ prefix: `<div ${routeHydrationContract.routeMarkerAttribute}="${escapedRouteId}">`,
1312
1408
  suffix: [
1313
1409
  "</div>",
1314
- `<script type="application/json" id="mreact-props-${escapedRouteId}">${propsJson}</script>`,
1410
+ `<script type="application/json" id="${propsScriptId}">${propsJson}</script>`,
1315
1411
  clientReferencesJson === undefined
1316
1412
  ? undefined
1317
- : `<script type="application/json" id="mreact-client-references-${escapedRouteId}">${clientReferencesJson}</script>`,
1413
+ : `<script type="application/json" id="${clientReferencesScriptId}">${clientReferencesJson}</script>`,
1318
1414
  `<script type="module" src="${escapeHtmlAttribute(scriptSrc)}"></script>`,
1319
1415
  ]
1320
1416
  .filter((part): part is string => part !== undefined)
@@ -1670,7 +1766,7 @@ function __mreactResolveRouteNode(value) {
1670
1766
  const boundaryOnlyHydrationBlock = routeRequiresFullHydration
1671
1767
  ? ""
1672
1768
  : `${routeCellHydrationIndent}if (!__mreactHasNonSerializableClientBoundaries(__mreactMarker) && __mreactHydrateClientBoundaries(document, __mreactClientReferences, __mreactClientReferenceComponents)) {
1673
- ${routeCellHydrationIndent} __mreactMarker.setAttribute("data-mreact-hydrated", "true");
1769
+ ${routeCellHydrationIndent} __mreactMarker.setAttribute(${JSON.stringify(routeHydrationContract.hydratedAttribute)}, "true");
1674
1770
  ${routeCellHydrationIndent} return;
1675
1771
  ${routeCellHydrationIndent}}
1676
1772
  `;
@@ -1681,8 +1777,12 @@ ${routeCellHydrationIndent}}
1681
1777
  const entry = `${routeCellEffectImport}${routeCleanupScopeImport}${emitCompatClientReferenceImportBlock(compatClientReferenceNames)}${clientReferenceImportBlock}${routeHydrationCode}
1682
1778
 
1683
1779
  const __mreactRouteId = ${JSON.stringify(routeId)};
1684
- const __mreactRouteStateSignature = ${JSON.stringify(routeStateSignature)};
1685
- const __mreactGlobal = globalThis;
1780
+ const __mreactRouteStateSignature = ${JSON.stringify(routeStateSignature)};
1781
+ const __mreactRouteMarkerAttribute = ${JSON.stringify(routeHydrationContract.routeMarkerAttribute)};
1782
+ const __mreactRouteHydratedAttribute = ${JSON.stringify(routeHydrationContract.hydratedAttribute)};
1783
+ const __mreactPropsScriptPrefix = ${JSON.stringify(routeHydrationContract.propsScriptPrefix)};
1784
+ const __mreactClientReferencesScriptPrefix = ${JSON.stringify(routeHydrationContract.clientReferencesScriptPrefix)};
1785
+ const __mreactGlobal = globalThis;
1686
1786
  ${navigationStateDeclaration}
1687
1787
  ${routeCellStateDeclaration}
1688
1788
  ${routeCleanupStateDeclaration}
@@ -1692,9 +1792,9 @@ ${routeNodeResolver}
1692
1792
 
1693
1793
  export function __mreactHydrateRoute() {
1694
1794
  __mreactApplyOutOfOrderFragments(document);
1695
- const __mreactMarker = document.querySelector(\`[data-mreact-route-id="\${__mreactRouteId}"]\`);
1696
- const __mreactPropsElement = document.getElementById(\`mreact-props-\${__mreactRouteId}\`);
1697
- const __mreactClientReferencesElement = document.getElementById(\`mreact-client-references-\${__mreactRouteId}\`);
1795
+ const __mreactMarker = document.querySelector(\`[\${__mreactRouteMarkerAttribute}="\${__mreactRouteId}"]\`);
1796
+ const __mreactPropsElement = document.getElementById(\`\${__mreactPropsScriptPrefix}\${__mreactRouteId}\`);
1797
+ const __mreactClientReferencesElement = document.getElementById(\`\${__mreactClientReferencesScriptPrefix}\${__mreactRouteId}\`);
1698
1798
  const __mreactProps = __mreactPropsElement?.textContent === undefined
1699
1799
  ? {}
1700
1800
  : JSON.parse(__mreactPropsElement.textContent);
@@ -1711,7 +1811,7 @@ export function __mreactHydrateRoute() {
1711
1811
  ${routeCellHydrationStart}${routeCleanupHydrationStart}${boundaryOnlyHydrationBlock}${routeComponentGuard}${routeCellHydrationIndent}const __mreactNode = ${routeNodeExpression};
1712
1812
  ${routeCellHydrationIndent}__mreactResumeRoute(__mreactMarker, __mreactNode);
1713
1813
  ${routeCellHydrationIndent}__mreactHydrateClientBoundaries(document, __mreactClientReferences, __mreactClientReferenceComponents);
1714
- ${routeCellHydrationIndent}__mreactMarker.setAttribute("data-mreact-hydrated", "true");
1814
+ ${routeCellHydrationIndent}__mreactMarker.setAttribute(__mreactRouteHydratedAttribute, "true");
1715
1815
  ${routeCellHydrationEnd}}
1716
1816
  ${routeCellDropFunction}
1717
1817
  ${routeCleanupFunction}
@@ -2098,15 +2198,15 @@ function __mreactApplyNavigationHtml(html, url) {
2098
2198
  const template = document.createElement("template");
2099
2199
  template.innerHTML = html.replace(/^\\s*<!doctype html>/i, "");
2100
2200
  __mreactApplyOutOfOrderFragments(template.content);
2101
- const nextMarker = template.content.querySelector("[data-mreact-route-id]");
2102
- const currentMarker = document.querySelector("[data-mreact-route-id]");
2201
+ const nextMarker = template.content.querySelector("[${routeHydrationContract.routeMarkerAttribute}]");
2202
+ const currentMarker = document.querySelector("[${routeHydrationContract.routeMarkerAttribute}]");
2103
2203
 
2104
2204
  if (nextMarker === null || currentMarker === null) {
2105
2205
  return false;
2106
2206
  }
2107
2207
 
2108
- const currentRouteId = currentMarker.getAttribute("data-mreact-route-id");
2109
- const nextRouteId = nextMarker.getAttribute("data-mreact-route-id");
2208
+ const currentRouteId = currentMarker.getAttribute("${routeHydrationContract.routeMarkerAttribute}");
2209
+ const nextRouteId = nextMarker.getAttribute("${routeHydrationContract.routeMarkerAttribute}");
2110
2210
 
2111
2211
  __mreactSyncHeadMetadata(template.content, html);
2112
2212
  __mreactResumeNode(currentMarker, nextMarker);
@@ -2114,7 +2214,7 @@ ${routeCleanupNavigationDispose} __mreactSyncRouteDataScripts(template.content,
2114
2214
 
2115
2215
  const script = template.content.querySelector('script[type="module"][src]')?.getAttribute("src");
2116
2216
  if (script !== null && script !== undefined) {
2117
- void import(/* @vite-ignore */ script).then((module) => module.__mreactHydrateRoute?.());
2217
+ void import(/* @vite-ignore */ script).then((module) => module.${routeHydrationContract.routeHydrateExport}?.());
2118
2218
  }
2119
2219
 
2120
2220
  __mreactApplyOutOfOrderFragments(document);
@@ -2207,15 +2307,15 @@ function __mreactRouteDataScriptIds(...routeIds) {
2207
2307
  continue;
2208
2308
  }
2209
2309
 
2210
- ids.add(\`mreact-props-\${routeId}\`);
2211
- ids.add(\`mreact-client-references-\${routeId}\`);
2310
+ ids.add(\`${routeHydrationContract.propsScriptPrefix}\${routeId}\`);
2311
+ ids.add(\`${routeHydrationContract.clientReferencesScriptPrefix}\${routeId}\`);
2212
2312
  }
2213
2313
 
2214
2314
  return ids;
2215
2315
  }
2216
2316
 
2217
2317
  function __mreactRouteDataScriptSelector() {
2218
- return 'script[type="application/json"][id^="mreact-props-"], script[type="application/json"][id^="mreact-client-references-"]';
2318
+ return ${JSON.stringify(routeDataScriptSelector())};
2219
2319
  }
2220
2320
 
2221
2321
  function __mreactCurrentHistoryState(url) {
@@ -0,0 +1,142 @@
1
+ import { relative, sep } from "node:path";
2
+ import { escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
3
+
4
+ export interface SlotRenderContext {
5
+ consumedSlots: Set<string>;
6
+ namedSlots: Readonly<Record<string, string>>;
7
+ }
8
+
9
+ export interface ShellFile {
10
+ file: string;
11
+ id: string;
12
+ kind: "layout" | "template";
13
+ }
14
+
15
+ export interface RenderedShellParts {
16
+ prefix: string;
17
+ suffix: string;
18
+ }
19
+
20
+ export function createSlotRenderContext(
21
+ namedSlots: Readonly<Record<string, string>> = {},
22
+ ): SlotRenderContext {
23
+ return {
24
+ consumedSlots: new Set(),
25
+ namedSlots,
26
+ };
27
+ }
28
+
29
+ export function splitLayoutSlot(
30
+ layoutHtml: string,
31
+ slotContext: SlotRenderContext = createSlotRenderContext(),
32
+ ): RenderedShellParts {
33
+ const html = replaceNamedLayoutSlots(layoutHtml, slotContext);
34
+ const match = findDefaultLayoutSlot(html);
35
+
36
+ if (match === null) {
37
+ return { prefix: html, suffix: "" };
38
+ }
39
+
40
+ return {
41
+ prefix: html.slice(0, match.index),
42
+ suffix: html.slice(match.index + match[0].length),
43
+ };
44
+ }
45
+
46
+ export function markShellBoundary(html: string, shell: ShellFile): string {
47
+ const attributeName =
48
+ shell.kind === "layout" ? "data-mreact-layout-boundary" : "data-mreact-template-boundary";
49
+
50
+ if (html.includes(`${attributeName}=`)) {
51
+ return html;
52
+ }
53
+
54
+ return html.replace(
55
+ /<([A-Za-z][^\s/>]*)([^>]*)>/,
56
+ `<$1$2 ${attributeName}="${escapeHtmlAttribute(shell.id)}">`,
57
+ );
58
+ }
59
+
60
+ export function shellBoundaryId(appDir: string, directory: string): string {
61
+ const relativeDirectory = relative(appDir, directory);
62
+
63
+ return relativeDirectory === ""
64
+ ? "root"
65
+ : relativeDirectory.replaceAll(sep, "/").replace(/[^A-Za-z0-9_$/-]/g, "_");
66
+ }
67
+
68
+ export function warnUnconsumedRouteSlots(options: {
69
+ appDir: string;
70
+ pageFile: string;
71
+ serverModuleCacheVersion: string | undefined;
72
+ slotContext: SlotRenderContext;
73
+ }): void {
74
+ if (options.serverModuleCacheVersion !== undefined) {
75
+ return;
76
+ }
77
+
78
+ const slotNames = Object.keys(options.slotContext.namedSlots);
79
+ if (slotNames.length === 0) {
80
+ return;
81
+ }
82
+
83
+ const routeLabel = relative(options.appDir, options.pageFile).replaceAll(sep, "/");
84
+
85
+ for (const name of slotNames) {
86
+ if (name === "default") {
87
+ console.warn(
88
+ `[mreact] ${routeLabel}: slots.default does not target <Slot />; use the page body for default slot content.`,
89
+ );
90
+ continue;
91
+ }
92
+
93
+ if (!options.slotContext.consumedSlots.has(name)) {
94
+ console.warn(
95
+ `[mreact] ${routeLabel}: slots.{${name}} is not consumed by any ancestor layout or template.`,
96
+ );
97
+ }
98
+ }
99
+ }
100
+
101
+ function replaceNamedLayoutSlots(layoutHtml: string, slotContext: SlotRenderContext): string {
102
+ return layoutHtml.replace(SLOT_TAG_PATTERN, (source, openAttributes: string) => {
103
+ const name = readSlotName(openAttributes);
104
+
105
+ if (name === undefined || name === "default") {
106
+ return source;
107
+ }
108
+
109
+ if (Object.hasOwn(slotContext.namedSlots, name)) {
110
+ slotContext.consumedSlots.add(name);
111
+ return slotContext.namedSlots[name] ?? "";
112
+ }
113
+
114
+ return "";
115
+ });
116
+ }
117
+
118
+ const SLOT_TAG_PATTERN = /<slot\b([^>]*)>(?:<\/slot\s*>)?/g;
119
+
120
+ function findDefaultLayoutSlot(html: string): RegExpExecArray | null {
121
+ SLOT_TAG_PATTERN.lastIndex = 0;
122
+
123
+ for (;;) {
124
+ const match = SLOT_TAG_PATTERN.exec(html);
125
+
126
+ if (match === null) {
127
+ return null;
128
+ }
129
+
130
+ const name = readSlotName(match[1] ?? "");
131
+
132
+ if (name === undefined || name === "default") {
133
+ return match;
134
+ }
135
+ }
136
+ }
137
+
138
+ function readSlotName(attributes: string): string | undefined {
139
+ const match = /\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(attributes);
140
+
141
+ return match?.[1] ?? match?.[2];
142
+ }