@reckona/mreact-router 0.0.151 → 0.0.153

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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/actions.d.ts +10 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +6 -0
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +18 -0
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +18 -0
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +33 -0
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +33 -0
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +5 -0
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js +5 -0
  17. package/dist/adapters/edge.js.map +1 -1
  18. package/dist/adapters/node.d.ts +5 -0
  19. package/dist/adapters/node.d.ts.map +1 -1
  20. package/dist/adapters/node.js +5 -0
  21. package/dist/adapters/node.js.map +1 -1
  22. package/dist/adapters/static.d.ts +5 -0
  23. package/dist/adapters/static.d.ts.map +1 -1
  24. package/dist/adapters/static.js +5 -0
  25. package/dist/adapters/static.js.map +1 -1
  26. package/dist/build.d.ts +15 -0
  27. package/dist/build.d.ts.map +1 -1
  28. package/dist/build.js +71 -1
  29. package/dist/build.js.map +1 -1
  30. package/dist/cache.d.ts +15 -0
  31. package/dist/cache.d.ts.map +1 -1
  32. package/dist/cache.js +15 -0
  33. package/dist/cache.js.map +1 -1
  34. package/dist/cookies.d.ts +16 -0
  35. package/dist/cookies.d.ts.map +1 -1
  36. package/dist/cookies.js +16 -0
  37. package/dist/cookies.js.map +1 -1
  38. package/dist/dev-server.d.ts +5 -0
  39. package/dist/dev-server.d.ts.map +1 -1
  40. package/dist/dev-server.js +5 -0
  41. package/dist/dev-server.js.map +1 -1
  42. package/dist/link.d.ts +10 -0
  43. package/dist/link.d.ts.map +1 -1
  44. package/dist/link.js +5 -0
  45. package/dist/link.js.map +1 -1
  46. package/dist/middleware.d.ts.map +1 -1
  47. package/dist/middleware.js +39 -4
  48. package/dist/middleware.js.map +1 -1
  49. package/dist/navigation.d.ts +53 -0
  50. package/dist/navigation.d.ts.map +1 -1
  51. package/dist/navigation.js +48 -0
  52. package/dist/navigation.js.map +1 -1
  53. package/dist/render.d.ts +28 -0
  54. package/dist/render.d.ts.map +1 -1
  55. package/dist/render.js +127 -27
  56. package/dist/render.js.map +1 -1
  57. package/dist/route-module-loader.d.ts +6 -1
  58. package/dist/route-module-loader.d.ts.map +1 -1
  59. package/dist/route-module-loader.js +8 -1
  60. package/dist/route-module-loader.js.map +1 -1
  61. package/dist/serve.d.ts +5 -0
  62. package/dist/serve.d.ts.map +1 -1
  63. package/dist/serve.js +5 -0
  64. package/dist/serve.js.map +1 -1
  65. package/dist/session.d.ts +19 -0
  66. package/dist/session.d.ts.map +1 -1
  67. package/dist/session.js +19 -0
  68. package/dist/session.js.map +1 -1
  69. package/dist/tailwind-source.d.ts +6 -0
  70. package/dist/tailwind-source.d.ts.map +1 -0
  71. package/dist/tailwind-source.js +25 -0
  72. package/dist/tailwind-source.js.map +1 -0
  73. package/dist/typed-routes.d.ts +5 -0
  74. package/dist/typed-routes.d.ts.map +1 -1
  75. package/dist/typed-routes.js +5 -0
  76. package/dist/typed-routes.js.map +1 -1
  77. package/dist/types.d.ts +15 -0
  78. package/dist/types.d.ts.map +1 -1
  79. package/dist/types.js +5 -0
  80. package/dist/types.js.map +1 -1
  81. package/dist/vite.d.ts.map +1 -1
  82. package/dist/vite.js +3 -25
  83. package/dist/vite.js.map +1 -1
  84. package/package.json +11 -11
  85. package/src/actions.ts +18 -0
  86. package/src/adapters/aws-lambda.ts +18 -0
  87. package/src/adapters/cloudflare.ts +33 -0
  88. package/src/adapters/edge.ts +5 -0
  89. package/src/adapters/node.ts +5 -0
  90. package/src/adapters/static.ts +5 -0
  91. package/src/build.ts +92 -2
  92. package/src/cache.ts +15 -0
  93. package/src/cookies.ts +16 -0
  94. package/src/dev-server.ts +5 -0
  95. package/src/link.ts +10 -0
  96. package/src/middleware.ts +54 -4
  97. package/src/navigation.ts +53 -0
  98. package/src/render.ts +164 -25
  99. package/src/route-module-loader.ts +15 -0
  100. package/src/serve.ts +5 -0
  101. package/src/session.ts +19 -0
  102. package/src/tailwind-source.ts +37 -0
  103. package/src/typed-routes.ts +5 -0
  104. package/src/types.ts +15 -0
  105. package/src/vite.ts +3 -38
package/src/render.ts CHANGED
@@ -137,6 +137,7 @@ import {
137
137
  prebuiltRouteLoaderModuleArtifact,
138
138
  prebuiltServerComponentModuleCode,
139
139
  prebuiltServerModuleOutputMatches,
140
+ prebuiltServerModuleOutputOptionsMatch,
140
141
  type BuiltServerModuleOutputLike,
141
142
  } from "./route-module-loader.js";
142
143
 
@@ -353,6 +354,8 @@ async function preloadBuiltPageRouteModules(options: {
353
354
  options.serverModuleCacheVersion,
354
355
  options.define,
355
356
  options.vitePlugins,
357
+ undefined,
358
+ { serverAwaitHydration: options.analysis.clientInference.client },
356
359
  );
357
360
  }
358
361
 
@@ -476,6 +479,7 @@ type StreamModuleExports = Record<string, unknown> & {
476
479
 
477
480
  const serverTransformCache = new Map<string, TransformOutput>();
478
481
  const serverTransformCacheCounters = createRouterRuntimeCacheCounters();
482
+ let productionDynamicServerTransformWarned = false;
479
483
  const serverSourceFileCache = new Map<string, Promise<string>>();
480
484
  const serverSourceFileCacheCounters = createRouterRuntimeCacheCounters();
481
485
  const routeSourceAnalysisCache = new Map<string, Promise<RouteSourceAnalysis>>();
@@ -640,6 +644,17 @@ function finishRenderTimingPhase(
640
644
  timing.phases[phaseName] = logDurationMs(startedAt);
641
645
  }
642
646
 
647
+ async function readSettledPromiseAtNextTask<T>(
648
+ promise: Promise<T>,
649
+ ): Promise<{ settled: true; value: T } | { settled: false }> {
650
+ return await Promise.race([
651
+ promise.then((value) => ({ settled: true as const, value })),
652
+ new Promise<{ settled: false }>((resolve) => {
653
+ setTimeout(() => resolve({ settled: false }), 0);
654
+ }),
655
+ ]);
656
+ }
657
+
643
658
  function addRenderTimingPhaseDuration(
644
659
  timing: RenderTiming | undefined,
645
660
  startedAt: number | undefined,
@@ -652,6 +667,12 @@ function addRenderTimingPhaseDuration(
652
667
  timing.phases[phaseName] = (timing.phases[phaseName] ?? 0) + logDurationMs(startedAt);
653
668
  }
654
669
 
670
+ function routeLoaderMayReturnControlResponse(code: string): boolean {
671
+ return /\b(?:redirect|redirectExternal|rewrite|notFound|throwNotFound)\s*\(/.test(code) ||
672
+ /\bnew\s+Response\s*\(/.test(code) ||
673
+ /\bResponse\.(?:redirect|json)\s*\(/.test(code);
674
+ }
675
+
655
676
  async function waitForRenderPreload(
656
677
  options: Pick<RenderAppRequestOptions, "preload">,
657
678
  timing: RenderTiming | undefined,
@@ -835,6 +856,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
835
856
  appDir: options.appDir,
836
857
  filename: "not-found.mreact.tsx",
837
858
  pathname: url.pathname,
859
+ serverSourceFiles: options.serverSourceFiles,
838
860
  });
839
861
 
840
862
  const response = await renderSpecialRoute({
@@ -1208,8 +1230,9 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1208
1230
  }
1209
1231
 
1210
1232
  let streamData: unknown;
1211
- if (loadingFile === undefined) {
1212
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1233
+ let streamDataPromise = dataPromise ?? Promise.resolve(undefined);
1234
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1235
+ if (loadingFile === undefined || routeLoaderMayReturnControlResponse(code)) {
1213
1236
  try {
1214
1237
  streamData = dataPromise === undefined ? undefined : await dataPromise;
1215
1238
  } finally {
@@ -1219,6 +1242,20 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1219
1242
  emitRenderTiming(options, timing, streamData.status);
1220
1243
  return streamData;
1221
1244
  }
1245
+ streamDataPromise = Promise.resolve(streamData);
1246
+ } else {
1247
+ try {
1248
+ const settledData = await readSettledPromiseAtNextTask(streamDataPromise);
1249
+ if (settledData.settled) {
1250
+ if (settledData.value instanceof Response) {
1251
+ emitRenderTiming(options, timing, settledData.value.status);
1252
+ return settledData.value;
1253
+ }
1254
+ streamDataPromise = Promise.resolve(settledData.value);
1255
+ }
1256
+ } finally {
1257
+ finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1258
+ }
1222
1259
  }
1223
1260
 
1224
1261
  await waitForRenderPreload(options, timing);
@@ -1249,7 +1286,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1249
1286
  appDir: options.appDir,
1250
1287
  assetBaseUrl: options.assetBaseUrl,
1251
1288
  clientRoute,
1252
- data: dataPromise ?? Promise.resolve(undefined),
1289
+ data: streamDataPromise,
1253
1290
  define: options.define,
1254
1291
  loadingFile,
1255
1292
  pageFile: matched.route.file,
@@ -1737,16 +1774,18 @@ async function nearestBoundaryFileForPath(options: {
1737
1774
  appDir: string;
1738
1775
  filename: string;
1739
1776
  pathname: string;
1777
+ serverSourceFiles?: ReadonlyMap<string, string> | undefined;
1740
1778
  }): Promise<string> {
1741
1779
  const parts = options.pathname
1742
1780
  .replace(/^\/+|\/+$/g, "")
1743
1781
  .split("/")
1744
- .filter((part) => part.length > 0);
1782
+ .filter((part) => part.length > 0 && part !== "." && part !== "..");
1745
1783
 
1746
- return nearestBoundaryFileFromParts({
1784
+ return await nearestBoundaryFileFromParts({
1747
1785
  appDir: options.appDir,
1748
1786
  filename: options.filename,
1749
1787
  parts,
1788
+ serverSourceFiles: options.serverSourceFiles,
1750
1789
  });
1751
1790
  }
1752
1791
 
@@ -1754,11 +1793,19 @@ async function nearestBoundaryFileFromParts(options: {
1754
1793
  appDir: string;
1755
1794
  filename: string;
1756
1795
  parts: string[];
1796
+ serverSourceFiles?: ReadonlyMap<string, string> | undefined;
1757
1797
  }): Promise<string> {
1758
1798
  for (let count = options.parts.length; count >= 0; count -= 1) {
1759
1799
  for (const filename of boundaryFilenameCandidates(options.filename)) {
1760
1800
  const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
1761
1801
 
1802
+ if (options.serverSourceFiles !== undefined) {
1803
+ if (options.serverSourceFiles.has(candidate)) {
1804
+ return candidate;
1805
+ }
1806
+ continue;
1807
+ }
1808
+
1762
1809
  try {
1763
1810
  await access(candidate);
1764
1811
  return candidate;
@@ -1835,10 +1882,16 @@ async function renderSpecialRoute(options: {
1835
1882
  textFallback: string;
1836
1883
  vitePlugins?: readonly PluginOption[] | undefined;
1837
1884
  }): Promise<Response> {
1838
- try {
1839
- await access(options.routeFile);
1840
- } catch {
1841
- return new Response(options.textFallback, { status: options.status });
1885
+ if (options.serverSourceFiles !== undefined) {
1886
+ if (!options.serverSourceFiles.has(options.routeFile)) {
1887
+ return new Response(options.textFallback, { status: options.status });
1888
+ }
1889
+ } else {
1890
+ try {
1891
+ await access(options.routeFile);
1892
+ } catch {
1893
+ return new Response(options.textFallback, { status: options.status });
1894
+ }
1842
1895
  }
1843
1896
 
1844
1897
  const props = {
@@ -1982,7 +2035,10 @@ async function dispatchServerRoute(options: {
1982
2035
  const handler = module[options.request.method] ?? module.ALL ?? module.default;
1983
2036
 
1984
2037
  if (typeof handler !== "function") {
1985
- return new Response("Method Not Allowed", { status: 405 });
2038
+ return new Response("Method Not Allowed", {
2039
+ headers: { allow: allowedServerRouteMethods(module).join(", ") },
2040
+ status: 405,
2041
+ });
1986
2042
  }
1987
2043
 
1988
2044
  let response: unknown;
@@ -2007,6 +2063,12 @@ async function dispatchServerRoute(options: {
2007
2063
  : new Response("Invalid route response", { status: 500 });
2008
2064
  }
2009
2065
 
2066
+ function allowedServerRouteMethods(module: Record<string, unknown>): string[] {
2067
+ return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].filter(
2068
+ (method) => typeof module[method] === "function",
2069
+ );
2070
+ }
2071
+
2010
2072
  async function dispatchConventionAssetRoute(options: {
2011
2073
  file: string;
2012
2074
  request: Request;
@@ -2544,7 +2606,10 @@ function transformServerModule(options: {
2544
2606
  if (
2545
2607
  artifact !== undefined &&
2546
2608
  artifact.sourceHash === sourceHash &&
2547
- options.serverAwaitHydration !== true
2609
+ prebuiltServerModuleOutputOptionsMatch(
2610
+ artifact,
2611
+ options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {},
2612
+ )
2548
2613
  ) {
2549
2614
  return {
2550
2615
  code: artifact.code,
@@ -2578,6 +2643,8 @@ function transformServerModule(options: {
2578
2643
  return cached;
2579
2644
  }
2580
2645
 
2646
+ warnProductionDynamicServerTransform();
2647
+
2581
2648
  const output = transform({
2582
2649
  code: options.code,
2583
2650
  ...(options.clientBoundaryImports === undefined
@@ -2605,6 +2672,19 @@ function transformServerModule(options: {
2605
2672
  return output;
2606
2673
  }
2607
2674
 
2675
+ export const __transformServerModuleForTesting = transformServerModule;
2676
+
2677
+ function warnProductionDynamicServerTransform(): void {
2678
+ if (process.env.NODE_ENV !== "production" || productionDynamicServerTransformWarned) {
2679
+ return;
2680
+ }
2681
+
2682
+ productionDynamicServerTransformWarned = true;
2683
+ console.warn(
2684
+ "mreact router: dynamic server transform path ran in production. Production deployments should provide prebuilt serverModules; otherwise request-time transforms use development compiler settings.",
2685
+ );
2686
+ }
2687
+
2608
2688
  async function analyzeRouteSource(options: {
2609
2689
  appDir: string;
2610
2690
  artifact?: BuiltRouteSourceAnalysisSummary | undefined;
@@ -3442,12 +3522,14 @@ async function loadServerStreamModule(
3442
3522
  define?: UserConfig["define"] | undefined,
3443
3523
  vitePlugins?: readonly PluginOption[] | undefined,
3444
3524
  importPolicy?: AppRouterImportPolicy | undefined,
3525
+ outputOptions: { serverAwaitHydration?: boolean } = {},
3445
3526
  ): Promise<StreamModuleExports> {
3446
3527
  const artifactCode = serverModules?.get(sourcefile)?.stream;
3447
3528
  const codeHash = memoizedHashText(code);
3448
- const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash);
3529
+ const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash, outputOptions);
3449
3530
  if (
3450
3531
  artifactCode !== undefined &&
3532
+ prebuiltServerModuleOutputOptionsMatch(artifactCode, outputOptions) &&
3451
3533
  prebuiltServerModuleOutputMatches(artifactCode, code, codeHash) &&
3452
3534
  artifactCode.moduleFile !== undefined
3453
3535
  ) {
@@ -4539,10 +4621,7 @@ async function loadComposedRouteMetadata(options: {
4539
4621
  serverSourceFiles?: ReadonlyMap<string, string> | undefined;
4540
4622
  vitePlugins?: readonly PluginOption[] | undefined;
4541
4623
  }): Promise<RouteMetadata | undefined> {
4542
- const cacheKey =
4543
- options.serverModuleCacheVersion === undefined
4544
- ? undefined
4545
- : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
4624
+ const cacheKey = composedRouteMetadataCacheKey(options);
4546
4625
  if (cacheKey !== undefined) {
4547
4626
  const cached = readRouterRuntimeCacheEntry(
4548
4627
  composedRouteMetadataCache,
@@ -4575,6 +4654,22 @@ async function loadComposedRouteMetadata(options: {
4575
4654
  }
4576
4655
  }
4577
4656
 
4657
+ function composedRouteMetadataCacheKey(options: {
4658
+ appDir: string;
4659
+ code: string;
4660
+ define?: UserConfig["define"] | undefined;
4661
+ filename: string;
4662
+ importPolicy?: AppRouterImportPolicy | undefined;
4663
+ serverModuleCacheVersion?: string | undefined;
4664
+ vitePlugins?: readonly PluginOption[] | undefined;
4665
+ }): string | undefined {
4666
+ return options.serverModuleCacheVersion === undefined
4667
+ ? undefined
4668
+ : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
4669
+ }
4670
+
4671
+ export const __composedRouteMetadataCacheKeyForTesting = composedRouteMetadataCacheKey;
4672
+
4578
4673
  async function loadComposedRouteMetadataUncached(options: {
4579
4674
  appDir: string;
4580
4675
  code: string;
@@ -4775,26 +4870,59 @@ interface InlineCspTag {
4775
4870
 
4776
4871
  function inlineCspTags(html: string): InlineCspTag[] {
4777
4872
  const tags: InlineCspTag[] = [];
4778
- const pattern = /<(script|style)\b([^>]*)>([\s\S]*?)<\/\1>/gi;
4779
- let match: RegExpExecArray | null;
4873
+ const lowerHtml = html.toLowerCase();
4874
+ let position = 0;
4875
+
4876
+ while (position < html.length) {
4877
+ const scriptIndex = lowerHtml.indexOf("<script", position);
4878
+ const styleIndex = lowerHtml.indexOf("<style", position);
4879
+ const openerIndex =
4880
+ scriptIndex === -1
4881
+ ? styleIndex
4882
+ : styleIndex === -1
4883
+ ? scriptIndex
4884
+ : Math.min(scriptIndex, styleIndex);
4885
+
4886
+ if (openerIndex === -1) {
4887
+ break;
4888
+ }
4780
4889
 
4781
- while ((match = pattern.exec(html)) !== null) {
4782
- const name = match[1]?.toLowerCase();
4890
+ const name = lowerHtml.startsWith("<script", openerIndex) ? "script" : "style";
4891
+ const tagEnd = html.indexOf(">", openerIndex + 1);
4892
+ if (tagEnd === -1) {
4893
+ break;
4894
+ }
4783
4895
 
4784
- if (name !== "script" && name !== "style") {
4896
+ const closeToken = `</${name}>`;
4897
+ const closeIndex = lowerHtml.indexOf(closeToken, tagEnd + 1);
4898
+ const nextScriptIndex = lowerHtml.indexOf("<script", tagEnd + 1);
4899
+ const nextStyleIndex = lowerHtml.indexOf("<style", tagEnd + 1);
4900
+ const nextOpenerIndex =
4901
+ nextScriptIndex === -1
4902
+ ? nextStyleIndex
4903
+ : nextStyleIndex === -1
4904
+ ? nextScriptIndex
4905
+ : Math.min(nextScriptIndex, nextStyleIndex);
4906
+
4907
+ if (closeIndex === -1 || (nextOpenerIndex !== -1 && nextOpenerIndex < closeIndex)) {
4908
+ position = tagEnd + 1;
4785
4909
  continue;
4786
4910
  }
4787
4911
 
4788
4912
  tags.push({
4789
- attributes: parseTagAttributes(match[2] ?? ""),
4790
- content: match[3] ?? "",
4913
+ attributes: parseTagAttributes(html.slice(openerIndex + name.length + 1, tagEnd)),
4914
+ content: html.slice(tagEnd + 1, closeIndex),
4791
4915
  name,
4792
4916
  });
4917
+
4918
+ position = closeIndex + closeToken.length;
4793
4919
  }
4794
4920
 
4795
4921
  return tags;
4796
4922
  }
4797
4923
 
4924
+ export const __inlineCspTagsForTesting = inlineCspTags;
4925
+
4798
4926
  function parseTagAttributes(source: string): ReadonlyMap<string, string> {
4799
4927
  const attributes = new Map<string, string>();
4800
4928
  const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
@@ -4842,7 +4970,7 @@ function injectQueryState(html: string, state: DehydratedQueryClient): string {
4842
4970
  )}</script>`;
4843
4971
 
4844
4972
  return /<\/body>/i.test(html)
4845
- ? html.replace(/<\/body>/i, () => `${script}</body>`)
4973
+ ? replaceFinalBodyCloseTag(html, script)
4846
4974
  : `${html}${script}`;
4847
4975
  }
4848
4976
 
@@ -4856,10 +4984,21 @@ function injectAuthSessionClaims(html: string, claims: unknown): string {
4856
4984
  )}</script>`;
4857
4985
 
4858
4986
  return /<\/body>/i.test(html)
4859
- ? html.replace(/<\/body>/i, () => `${script}</body>`)
4987
+ ? replaceFinalBodyCloseTag(html, script)
4860
4988
  : `${html}${script}`;
4861
4989
  }
4862
4990
 
4991
+ function replaceFinalBodyCloseTag(html: string, insertion: string): string {
4992
+ const matches = [...html.matchAll(/<\/body>/gi)];
4993
+ const last = matches.at(-1);
4994
+
4995
+ if (last?.index === undefined) {
4996
+ return `${html}${insertion}`;
4997
+ }
4998
+
4999
+ return `${html.slice(0, last.index)}${insertion}${html.slice(last.index)}`;
5000
+ }
5001
+
4863
5002
  function authIncludesClaims(code: string): boolean {
4864
5003
  return /\bexport\s+const\s+auth\s*=\s*["']include-claims["']\s*;?/.test(code);
4865
5004
  }
@@ -58,11 +58,16 @@ export function prebuiltServerComponentModuleCode(
58
58
  artifact: BuiltServerModuleArtifact["string"] | BuiltServerModuleArtifact["stream"] | undefined,
59
59
  code: string,
60
60
  codeHash: string,
61
+ options: { serverAwaitHydration?: boolean } = {},
61
62
  ): string | undefined {
62
63
  if (artifact === undefined) {
63
64
  return undefined;
64
65
  }
65
66
 
67
+ if (!prebuiltServerModuleOutputOptionsMatch(artifact, options)) {
68
+ return undefined;
69
+ }
70
+
66
71
  if (!prebuiltServerModuleOutputMatches(artifact, code, codeHash)) {
67
72
  return undefined;
68
73
  }
@@ -70,6 +75,16 @@ export function prebuiltServerComponentModuleCode(
70
75
  return artifact.bundleCode;
71
76
  }
72
77
 
78
+ export function prebuiltServerModuleOutputOptionsMatch(
79
+ artifact: NonNullable<BuiltServerModuleArtifact["string"] | BuiltServerModuleArtifact["stream"]>,
80
+ options: { serverAwaitHydration?: boolean },
81
+ ): boolean {
82
+ return (
83
+ (artifact.metadata?.serverAwaitHydration === true) ===
84
+ (options.serverAwaitHydration === true)
85
+ );
86
+ }
87
+
73
88
  export function prebuiltServerModuleOutputMatches(
74
89
  artifact: BuiltServerModuleOutputLike,
75
90
  code: string,
package/src/serve.ts CHANGED
@@ -577,6 +577,11 @@ async function materializeResponseAsBuffer(response: Response): Promise<Response
577
577
  });
578
578
  }
579
579
 
580
+ /**
581
+ * Starts a Node HTTP server for an already built app-router output directory.
582
+ *
583
+ * Use this for production-like local serving or custom Node deployments. It loads `.mreact/server` artifacts, applies the generated import policy when configured, enforces host trust options, and returns a `close()` method for orderly shutdown.
584
+ */
580
585
  export async function startServer(
581
586
  options: StartServerOptions,
582
587
  ): Promise<{ close(): Promise<void>; server: Server; url: string }> {
package/src/session.ts CHANGED
@@ -72,6 +72,11 @@ function createSessionId(): string {
72
72
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
73
73
  }
74
74
 
75
+ /**
76
+ * Creates a process-local session store backed by an in-memory LRU-like map.
77
+ *
78
+ * Use it for development, tests, or single-process deployments; production multi-instance deployments should provide a shared `SessionStore`.
79
+ */
75
80
  export function createMemorySessionStore<TData>(
76
81
  options: MemorySessionStoreOptions = {},
77
82
  ): SessionStore<TData> {
@@ -152,6 +157,9 @@ function nonNegativeIntegerOrDefault(value: number | undefined, fallback: number
152
157
  return value === undefined || !Number.isInteger(value) || value < 0 ? fallback : value;
153
158
  }
154
159
 
160
+ /**
161
+ * Reads the current session cookie, loads the matching session record, and deletes expired records.
162
+ */
155
163
  export async function getSession<TData>(
156
164
  request: Request,
157
165
  store: SessionStore<TData>,
@@ -177,6 +185,9 @@ export async function getSession<TData>(
177
185
  return record;
178
186
  }
179
187
 
188
+ /**
189
+ * Creates a new session record, stores it, and appends the session cookie to the response.
190
+ */
180
191
  export async function createSession<TData>(
181
192
  response: Response,
182
193
  store: SessionStore<TData>,
@@ -200,6 +211,9 @@ export async function createSession<TData>(
200
211
  return record;
201
212
  }
202
213
 
214
+ /**
215
+ * Deletes the current session record when present and appends an expiring session cookie to the response.
216
+ */
203
217
  export async function destroySession<TData>(
204
218
  request: Request,
205
219
  response: Response,
@@ -219,6 +233,11 @@ export async function destroySession<TData>(
219
233
  });
220
234
  }
221
235
 
236
+ /**
237
+ * Replaces the current session id while preserving the stored session data.
238
+ *
239
+ * Use this after authentication or privilege changes to reduce session fixation risk.
240
+ */
222
241
  export async function rotateSession<TData>(
223
242
  request: Request,
224
243
  response: Response,
@@ -0,0 +1,37 @@
1
+ import { dirname, relative, sep } from "node:path";
2
+
3
+ export function prependTailwindSourceDirectives(options: {
4
+ code: string;
5
+ cssFile: string;
6
+ sourceDirs: readonly string[];
7
+ }): string {
8
+ if (!isTailwindCssEntry(options.code)) {
9
+ return options.code;
10
+ }
11
+
12
+ const cssDir = dirname(options.cssFile);
13
+ const directives = [...new Set(options.sourceDirs)]
14
+ .map((sourceDir) => `${tailwindSourceDirective(cssDir, sourceDir)}\n`)
15
+ .join("");
16
+
17
+ return directives.length === 0 ? options.code : `${directives}${options.code}`;
18
+ }
19
+
20
+ function isTailwindCssEntry(code: string): boolean {
21
+ return (
22
+ /@import\s+(?:url\()?["']tailwindcss(?:\/[^"']*)?["']\)?/u.test(code) ||
23
+ /@tailwind\s+(?:base|components|utilities)\b/u.test(code)
24
+ );
25
+ }
26
+
27
+ function tailwindSourceDirective(cssDir: string, sourceDir: string): string {
28
+ const relativeSourceDir = relative(cssDir, sourceDir).split(sep).join("/");
29
+ const normalizedSourceDir =
30
+ relativeSourceDir === ""
31
+ ? "."
32
+ : relativeSourceDir.startsWith(".")
33
+ ? relativeSourceDir
34
+ : `./${relativeSourceDir}`;
35
+
36
+ return `@source ${JSON.stringify(`${normalizedSourceDir}/**/*.{js,jsx,ts,tsx,mdx}`)};`;
37
+ }
@@ -54,6 +54,11 @@ export type AppRouteLinkSegment<Segment extends string> = Segment extends `:${st
54
54
  ? string
55
55
  : Segment;
56
56
 
57
+ /**
58
+ * Builds an internal app-route URL from a route pattern and typed params/search options.
59
+ *
60
+ * Dynamic `:param` and `:...catchAll` segments are URL-encoded, search values are serialized with `URLSearchParams`, and protocol-relative or external paths are rejected.
61
+ */
57
62
  export function href<const Path extends `/${string}`>(
58
63
  path: Path,
59
64
  ...args: HasRouteParams<Path> extends true
package/src/types.ts CHANGED
@@ -25,6 +25,11 @@ export interface LoaderContext<TParams extends RouteParams = RouteParams> {
25
25
  request: Request;
26
26
  }
27
27
 
28
+ /**
29
+ * Context passed to a route `generateMetadata` export.
30
+ *
31
+ * Metadata is evaluated on the server after loader data is available, so it can derive head tags, robots data, sitemap entries, and security header directives from the same request and route params as the page.
32
+ */
28
33
  export interface GenerateMetadataContext<
29
34
  TData = unknown,
30
35
  TParams extends RouteParams = RouteParams,
@@ -49,6 +54,11 @@ export type PageComponent<TLoader extends RouteLoader> = (
49
54
  props: PageProps<InferLoaderData<TLoader>, InferLoaderParams<TLoader>>,
50
55
  ) => ReactCompatNode;
51
56
 
57
+ /**
58
+ * Preserves the relationship between a page component and its loader when authoring route files.
59
+ *
60
+ * Wrap a default page export with `definePage<typeof loader>(...)` to infer `data` and `params` props from the route loader without changing runtime behavior.
61
+ */
52
62
  export function definePage<TLoader extends RouteLoader>(
53
63
  component: PageComponent<TLoader>,
54
64
  ): PageComponent<TLoader> {
@@ -73,6 +83,11 @@ export interface MetadataImage {
73
83
  width?: MetadataScalar;
74
84
  }
75
85
 
86
+ /**
87
+ * Declarative metadata returned by route metadata exports.
88
+ *
89
+ * Values are rendered into the document head and response headers by the app router; use `generateMetadata` when the values depend on loader data, params, or the current request.
90
+ */
76
91
  export interface RouteMetadata {
77
92
  alternates?: {
78
93
  canonical?: MetadataScalar;
package/src/vite.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import type { ServerResponse } from "node:http";
3
- import { dirname, relative, sep } from "node:path";
3
+ import { dirname } from "node:path";
4
4
  import { formatDiagnostic } from "@reckona/mreact-compiler";
5
5
  import {
6
6
  createCompilerModuleContext,
@@ -49,6 +49,7 @@ import {
49
49
  import { createRouteMatcher, scanAppRoutes, type AppRoute } from "./routes.js";
50
50
  import { resolveRequestHost, type RequestHostPolicy } from "./serve.js";
51
51
  import { hasJsxSyntax } from "./source-jsx.js";
52
+ import { prependTailwindSourceDirectives } from "./tailwind-source.js";
52
53
  import { workspacePackageFile } from "./workspace-packages.js";
53
54
 
54
55
  export interface AppRouterViteMiddlewareOptions extends AppRouterProjectOptions {
@@ -743,49 +744,13 @@ async function loadDevCssSourceModule(options: {
743
744
  }): Promise<string | undefined> {
744
745
  const code = await readFile(options.cssFile, "utf8");
745
746
 
746
- return prependDevTailwindSourceDirectives({
747
+ return prependTailwindSourceDirectives({
747
748
  code,
748
749
  cssFile: options.cssFile,
749
750
  sourceDirs: options.sourceDirs,
750
751
  });
751
752
  }
752
753
 
753
- function prependDevTailwindSourceDirectives(options: {
754
- code: string;
755
- cssFile: string;
756
- sourceDirs: readonly string[];
757
- }): string {
758
- if (!isTailwindCssEntry(options.code)) {
759
- return options.code;
760
- }
761
-
762
- const cssDir = dirname(options.cssFile);
763
- const directives = [...new Set(options.sourceDirs)]
764
- .map((sourceDir) => `${devTailwindSourceDirective(cssDir, sourceDir)}\n`)
765
- .join("");
766
-
767
- return directives.length === 0 ? options.code : `${directives}${options.code}`;
768
- }
769
-
770
- function isTailwindCssEntry(code: string): boolean {
771
- return (
772
- /@import\s+(?:url\()?["']tailwindcss(?:\/[^"']*)?["']\)?/u.test(code) ||
773
- /@tailwind\s+(?:base|components|utilities)\b/u.test(code)
774
- );
775
- }
776
-
777
- function devTailwindSourceDirective(cssDir: string, sourceDir: string): string {
778
- const relativeSourceDir = relative(cssDir, sourceDir).split(sep).join("/");
779
- const normalizedSourceDir =
780
- relativeSourceDir === ""
781
- ? "."
782
- : relativeSourceDir.startsWith(".")
783
- ? relativeSourceDir
784
- : `./${relativeSourceDir}`;
785
-
786
- return `@source ${JSON.stringify(`${normalizedSourceDir}/**/*.{js,jsx,ts,tsx,mdx}`)};`;
787
- }
788
-
789
754
  function importerInRuntimePackage(
790
755
  importer: string | undefined,
791
756
  directories: readonly string[],