@reckona/mreact-router 0.0.152 → 0.0.154

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 +85 -2
  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/module-runner.d.ts +6 -0
  50. package/dist/module-runner.d.ts.map +1 -1
  51. package/dist/module-runner.js +71 -0
  52. package/dist/module-runner.js.map +1 -1
  53. package/dist/navigation.d.ts +53 -0
  54. package/dist/navigation.d.ts.map +1 -1
  55. package/dist/navigation.js +48 -0
  56. package/dist/navigation.js.map +1 -1
  57. package/dist/render.d.ts +34 -0
  58. package/dist/render.d.ts.map +1 -1
  59. package/dist/render.js +161 -27
  60. package/dist/render.js.map +1 -1
  61. package/dist/route-module-loader.d.ts +6 -1
  62. package/dist/route-module-loader.d.ts.map +1 -1
  63. package/dist/route-module-loader.js +8 -1
  64. package/dist/route-module-loader.js.map +1 -1
  65. package/dist/serve.d.ts +5 -0
  66. package/dist/serve.d.ts.map +1 -1
  67. package/dist/serve.js +5 -0
  68. package/dist/serve.js.map +1 -1
  69. package/dist/session.d.ts +19 -0
  70. package/dist/session.d.ts.map +1 -1
  71. package/dist/session.js +19 -0
  72. package/dist/session.js.map +1 -1
  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 +1 -0
  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 +118 -1
  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/module-runner.ts +91 -0
  98. package/src/navigation.ts +53 -0
  99. package/src/render.ts +229 -25
  100. package/src/route-module-loader.ts +15 -0
  101. package/src/serve.ts +5 -0
  102. package/src/session.ts +19 -0
  103. package/src/typed-routes.ts +5 -0
  104. package/src/types.ts +15 -0
  105. package/src/vite.ts +1 -0
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
 
@@ -184,6 +185,9 @@ export interface RenderAppRequestOptions {
184
185
  routes?: readonly AppRoute[] | undefined;
185
186
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
186
187
  serverModuleCacheVersion?: string | undefined;
188
+ // True when serving through a dev server: request-time server transforms
189
+ // are expected there and must not trigger the production prebuild warning.
190
+ dev?: boolean | undefined;
187
191
  serverSourceFiles?: ReadonlyMap<string, string> | undefined;
188
192
  serverActions?: AppRouterServerActionOptions | undefined;
189
193
  serverActionReferencesByFile?:
@@ -353,6 +357,8 @@ async function preloadBuiltPageRouteModules(options: {
353
357
  options.serverModuleCacheVersion,
354
358
  options.define,
355
359
  options.vitePlugins,
360
+ undefined,
361
+ { serverAwaitHydration: options.analysis.clientInference.client },
356
362
  );
357
363
  }
358
364
 
@@ -476,6 +482,7 @@ type StreamModuleExports = Record<string, unknown> & {
476
482
 
477
483
  const serverTransformCache = new Map<string, TransformOutput>();
478
484
  const serverTransformCacheCounters = createRouterRuntimeCacheCounters();
485
+ const productionDynamicServerTransformWarned = new Set<string>();
479
486
  const serverSourceFileCache = new Map<string, Promise<string>>();
480
487
  const serverSourceFileCacheCounters = createRouterRuntimeCacheCounters();
481
488
  const routeSourceAnalysisCache = new Map<string, Promise<RouteSourceAnalysis>>();
@@ -640,6 +647,17 @@ function finishRenderTimingPhase(
640
647
  timing.phases[phaseName] = logDurationMs(startedAt);
641
648
  }
642
649
 
650
+ async function readSettledPromiseAtNextTask<T>(
651
+ promise: Promise<T>,
652
+ ): Promise<{ settled: true; value: T } | { settled: false }> {
653
+ return await Promise.race([
654
+ promise.then((value) => ({ settled: true as const, value })),
655
+ new Promise<{ settled: false }>((resolve) => {
656
+ setTimeout(() => resolve({ settled: false }), 0);
657
+ }),
658
+ ]);
659
+ }
660
+
643
661
  function addRenderTimingPhaseDuration(
644
662
  timing: RenderTiming | undefined,
645
663
  startedAt: number | undefined,
@@ -652,6 +670,12 @@ function addRenderTimingPhaseDuration(
652
670
  timing.phases[phaseName] = (timing.phases[phaseName] ?? 0) + logDurationMs(startedAt);
653
671
  }
654
672
 
673
+ function routeLoaderMayReturnControlResponse(code: string): boolean {
674
+ return /\b(?:redirect|redirectExternal|rewrite|notFound|throwNotFound)\s*\(/.test(code) ||
675
+ /\bnew\s+Response\s*\(/.test(code) ||
676
+ /\bResponse\.(?:redirect|json)\s*\(/.test(code);
677
+ }
678
+
655
679
  async function waitForRenderPreload(
656
680
  options: Pick<RenderAppRequestOptions, "preload">,
657
681
  timing: RenderTiming | undefined,
@@ -745,6 +769,7 @@ export async function resolveAppRouterMiddleware(options: {
745
769
  }
746
770
 
747
771
  async function renderAppRequestInternal(options: RenderAppRequestOptions): Promise<Response> {
772
+ warnProductionRenderWithoutPrebuiltModules(options);
748
773
  const timing = createRenderTiming(options.logger);
749
774
  const clientRouteInferenceCache =
750
775
  options.clientRouteInferenceCache ?? createClientRouteInferenceCache();
@@ -835,6 +860,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
835
860
  appDir: options.appDir,
836
861
  filename: "not-found.mreact.tsx",
837
862
  pathname: url.pathname,
863
+ serverSourceFiles: options.serverSourceFiles,
838
864
  });
839
865
 
840
866
  const response = await renderSpecialRoute({
@@ -1208,8 +1234,9 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1208
1234
  }
1209
1235
 
1210
1236
  let streamData: unknown;
1211
- if (loadingFile === undefined) {
1212
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1237
+ let streamDataPromise = dataPromise ?? Promise.resolve(undefined);
1238
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1239
+ if (loadingFile === undefined || routeLoaderMayReturnControlResponse(code)) {
1213
1240
  try {
1214
1241
  streamData = dataPromise === undefined ? undefined : await dataPromise;
1215
1242
  } finally {
@@ -1219,6 +1246,20 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1219
1246
  emitRenderTiming(options, timing, streamData.status);
1220
1247
  return streamData;
1221
1248
  }
1249
+ streamDataPromise = Promise.resolve(streamData);
1250
+ } else {
1251
+ try {
1252
+ const settledData = await readSettledPromiseAtNextTask(streamDataPromise);
1253
+ if (settledData.settled) {
1254
+ if (settledData.value instanceof Response) {
1255
+ emitRenderTiming(options, timing, settledData.value.status);
1256
+ return settledData.value;
1257
+ }
1258
+ streamDataPromise = Promise.resolve(settledData.value);
1259
+ }
1260
+ } finally {
1261
+ finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1262
+ }
1222
1263
  }
1223
1264
 
1224
1265
  await waitForRenderPreload(options, timing);
@@ -1249,7 +1290,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1249
1290
  appDir: options.appDir,
1250
1291
  assetBaseUrl: options.assetBaseUrl,
1251
1292
  clientRoute,
1252
- data: dataPromise ?? Promise.resolve(undefined),
1293
+ data: streamDataPromise,
1253
1294
  define: options.define,
1254
1295
  loadingFile,
1255
1296
  pageFile: matched.route.file,
@@ -1737,16 +1778,18 @@ async function nearestBoundaryFileForPath(options: {
1737
1778
  appDir: string;
1738
1779
  filename: string;
1739
1780
  pathname: string;
1781
+ serverSourceFiles?: ReadonlyMap<string, string> | undefined;
1740
1782
  }): Promise<string> {
1741
1783
  const parts = options.pathname
1742
1784
  .replace(/^\/+|\/+$/g, "")
1743
1785
  .split("/")
1744
- .filter((part) => part.length > 0);
1786
+ .filter((part) => part.length > 0 && part !== "." && part !== "..");
1745
1787
 
1746
- return nearestBoundaryFileFromParts({
1788
+ return await nearestBoundaryFileFromParts({
1747
1789
  appDir: options.appDir,
1748
1790
  filename: options.filename,
1749
1791
  parts,
1792
+ serverSourceFiles: options.serverSourceFiles,
1750
1793
  });
1751
1794
  }
1752
1795
 
@@ -1754,11 +1797,19 @@ async function nearestBoundaryFileFromParts(options: {
1754
1797
  appDir: string;
1755
1798
  filename: string;
1756
1799
  parts: string[];
1800
+ serverSourceFiles?: ReadonlyMap<string, string> | undefined;
1757
1801
  }): Promise<string> {
1758
1802
  for (let count = options.parts.length; count >= 0; count -= 1) {
1759
1803
  for (const filename of boundaryFilenameCandidates(options.filename)) {
1760
1804
  const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
1761
1805
 
1806
+ if (options.serverSourceFiles !== undefined) {
1807
+ if (options.serverSourceFiles.has(candidate)) {
1808
+ return candidate;
1809
+ }
1810
+ continue;
1811
+ }
1812
+
1762
1813
  try {
1763
1814
  await access(candidate);
1764
1815
  return candidate;
@@ -1835,10 +1886,16 @@ async function renderSpecialRoute(options: {
1835
1886
  textFallback: string;
1836
1887
  vitePlugins?: readonly PluginOption[] | undefined;
1837
1888
  }): Promise<Response> {
1838
- try {
1839
- await access(options.routeFile);
1840
- } catch {
1841
- return new Response(options.textFallback, { status: options.status });
1889
+ if (options.serverSourceFiles !== undefined) {
1890
+ if (!options.serverSourceFiles.has(options.routeFile)) {
1891
+ return new Response(options.textFallback, { status: options.status });
1892
+ }
1893
+ } else {
1894
+ try {
1895
+ await access(options.routeFile);
1896
+ } catch {
1897
+ return new Response(options.textFallback, { status: options.status });
1898
+ }
1842
1899
  }
1843
1900
 
1844
1901
  const props = {
@@ -1982,7 +2039,10 @@ async function dispatchServerRoute(options: {
1982
2039
  const handler = module[options.request.method] ?? module.ALL ?? module.default;
1983
2040
 
1984
2041
  if (typeof handler !== "function") {
1985
- return new Response("Method Not Allowed", { status: 405 });
2042
+ return new Response("Method Not Allowed", {
2043
+ headers: { allow: allowedServerRouteMethods(module).join(", ") },
2044
+ status: 405,
2045
+ });
1986
2046
  }
1987
2047
 
1988
2048
  let response: unknown;
@@ -2007,6 +2067,12 @@ async function dispatchServerRoute(options: {
2007
2067
  : new Response("Invalid route response", { status: 500 });
2008
2068
  }
2009
2069
 
2070
+ function allowedServerRouteMethods(module: Record<string, unknown>): string[] {
2071
+ return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].filter(
2072
+ (method) => typeof module[method] === "function",
2073
+ );
2074
+ }
2075
+
2010
2076
  async function dispatchConventionAssetRoute(options: {
2011
2077
  file: string;
2012
2078
  request: Request;
@@ -2544,7 +2610,10 @@ function transformServerModule(options: {
2544
2610
  if (
2545
2611
  artifact !== undefined &&
2546
2612
  artifact.sourceHash === sourceHash &&
2547
- options.serverAwaitHydration !== true
2613
+ prebuiltServerModuleOutputOptionsMatch(
2614
+ artifact,
2615
+ options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {},
2616
+ )
2548
2617
  ) {
2549
2618
  return {
2550
2619
  code: artifact.code,
@@ -2578,6 +2647,23 @@ function transformServerModule(options: {
2578
2647
  return cached;
2579
2648
  }
2580
2649
 
2650
+ // Only renders that were handed a prebuild map can have a prebuild gap;
2651
+ // dev servers pass no serverModules and transform at request time by design.
2652
+ if (options.serverModules !== undefined) {
2653
+ warnProductionDynamicServerTransform(
2654
+ options.filename,
2655
+ options.serverOutput,
2656
+ artifact === undefined
2657
+ ? "no prebuilt server module for this output mode"
2658
+ : !prebuiltServerModuleOutputOptionsMatch(
2659
+ artifact,
2660
+ options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {},
2661
+ )
2662
+ ? `serverAwaitHydration mismatch (artifact=${artifact.metadata?.serverAwaitHydration === true}, requested=${options.serverAwaitHydration === true})`
2663
+ : "stale prebuilt server module (source hash changed)",
2664
+ );
2665
+ }
2666
+
2581
2667
  const output = transform({
2582
2668
  code: options.code,
2583
2669
  ...(options.clientBoundaryImports === undefined
@@ -2605,6 +2691,65 @@ function transformServerModule(options: {
2605
2691
  return output;
2606
2692
  }
2607
2693
 
2694
+ export const __transformServerModuleForTesting = transformServerModule;
2695
+
2696
+ let productionRenderWithoutPrebuiltModulesWarned = false;
2697
+
2698
+ export function __resetProductionRenderWarningsForTesting(): void {
2699
+ productionRenderWithoutPrebuiltModulesWarned = false;
2700
+ productionDynamicServerTransformWarned.clear();
2701
+ }
2702
+
2703
+ // Covers the deployment that forgot prebuilt modules entirely; per-module
2704
+ // gaps inside a provided prebuild map are reported by
2705
+ // warnProductionDynamicServerTransform with the affected file instead.
2706
+ export function __warnProductionRenderWithoutPrebuiltModulesForTesting(options: {
2707
+ dev?: boolean | undefined;
2708
+ serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
2709
+ }): void {
2710
+ warnProductionRenderWithoutPrebuiltModules(options);
2711
+ }
2712
+
2713
+ function warnProductionRenderWithoutPrebuiltModules(options: {
2714
+ dev?: boolean | undefined;
2715
+ serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
2716
+ }): void {
2717
+ if (
2718
+ process.env.NODE_ENV !== "production" ||
2719
+ options.dev === true ||
2720
+ options.serverModules !== undefined ||
2721
+ productionRenderWithoutPrebuiltModulesWarned
2722
+ ) {
2723
+ return;
2724
+ }
2725
+
2726
+ productionRenderWithoutPrebuiltModulesWarned = true;
2727
+ console.warn(
2728
+ "mreact router: production render without prebuilt serverModules. Request-time transforms use development compiler settings; pass the build output's serverModules (or set dev: true for dev servers).",
2729
+ );
2730
+ }
2731
+
2732
+ function warnProductionDynamicServerTransform(
2733
+ filename: string,
2734
+ serverOutput: ServerOutputMode,
2735
+ reason: string,
2736
+ ): void {
2737
+ if (process.env.NODE_ENV !== "production") {
2738
+ return;
2739
+ }
2740
+
2741
+ const warnedKey = `${filename}\0${serverOutput}`;
2742
+
2743
+ if (productionDynamicServerTransformWarned.has(warnedKey)) {
2744
+ return;
2745
+ }
2746
+
2747
+ productionDynamicServerTransformWarned.add(warnedKey);
2748
+ console.warn(
2749
+ `mreact router: dynamic server transform path ran in production for ${filename} (serverOutput=${serverOutput}): ${reason}. Production deployments should provide prebuilt serverModules; otherwise request-time transforms use development compiler settings.`,
2750
+ );
2751
+ }
2752
+
2608
2753
  async function analyzeRouteSource(options: {
2609
2754
  appDir: string;
2610
2755
  artifact?: BuiltRouteSourceAnalysisSummary | undefined;
@@ -3442,12 +3587,14 @@ async function loadServerStreamModule(
3442
3587
  define?: UserConfig["define"] | undefined,
3443
3588
  vitePlugins?: readonly PluginOption[] | undefined,
3444
3589
  importPolicy?: AppRouterImportPolicy | undefined,
3590
+ outputOptions: { serverAwaitHydration?: boolean } = {},
3445
3591
  ): Promise<StreamModuleExports> {
3446
3592
  const artifactCode = serverModules?.get(sourcefile)?.stream;
3447
3593
  const codeHash = memoizedHashText(code);
3448
- const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash);
3594
+ const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash, outputOptions);
3449
3595
  if (
3450
3596
  artifactCode !== undefined &&
3597
+ prebuiltServerModuleOutputOptionsMatch(artifactCode, outputOptions) &&
3451
3598
  prebuiltServerModuleOutputMatches(artifactCode, code, codeHash) &&
3452
3599
  artifactCode.moduleFile !== undefined
3453
3600
  ) {
@@ -4539,10 +4686,7 @@ async function loadComposedRouteMetadata(options: {
4539
4686
  serverSourceFiles?: ReadonlyMap<string, string> | undefined;
4540
4687
  vitePlugins?: readonly PluginOption[] | undefined;
4541
4688
  }): 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)}`;
4689
+ const cacheKey = composedRouteMetadataCacheKey(options);
4546
4690
  if (cacheKey !== undefined) {
4547
4691
  const cached = readRouterRuntimeCacheEntry(
4548
4692
  composedRouteMetadataCache,
@@ -4575,6 +4719,22 @@ async function loadComposedRouteMetadata(options: {
4575
4719
  }
4576
4720
  }
4577
4721
 
4722
+ function composedRouteMetadataCacheKey(options: {
4723
+ appDir: string;
4724
+ code: string;
4725
+ define?: UserConfig["define"] | undefined;
4726
+ filename: string;
4727
+ importPolicy?: AppRouterImportPolicy | undefined;
4728
+ serverModuleCacheVersion?: string | undefined;
4729
+ vitePlugins?: readonly PluginOption[] | undefined;
4730
+ }): string | undefined {
4731
+ return options.serverModuleCacheVersion === undefined
4732
+ ? undefined
4733
+ : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
4734
+ }
4735
+
4736
+ export const __composedRouteMetadataCacheKeyForTesting = composedRouteMetadataCacheKey;
4737
+
4578
4738
  async function loadComposedRouteMetadataUncached(options: {
4579
4739
  appDir: string;
4580
4740
  code: string;
@@ -4775,26 +4935,59 @@ interface InlineCspTag {
4775
4935
 
4776
4936
  function inlineCspTags(html: string): InlineCspTag[] {
4777
4937
  const tags: InlineCspTag[] = [];
4778
- const pattern = /<(script|style)\b([^>]*)>([\s\S]*?)<\/\1>/gi;
4779
- let match: RegExpExecArray | null;
4938
+ const lowerHtml = html.toLowerCase();
4939
+ let position = 0;
4940
+
4941
+ while (position < html.length) {
4942
+ const scriptIndex = lowerHtml.indexOf("<script", position);
4943
+ const styleIndex = lowerHtml.indexOf("<style", position);
4944
+ const openerIndex =
4945
+ scriptIndex === -1
4946
+ ? styleIndex
4947
+ : styleIndex === -1
4948
+ ? scriptIndex
4949
+ : Math.min(scriptIndex, styleIndex);
4950
+
4951
+ if (openerIndex === -1) {
4952
+ break;
4953
+ }
4780
4954
 
4781
- while ((match = pattern.exec(html)) !== null) {
4782
- const name = match[1]?.toLowerCase();
4955
+ const name = lowerHtml.startsWith("<script", openerIndex) ? "script" : "style";
4956
+ const tagEnd = html.indexOf(">", openerIndex + 1);
4957
+ if (tagEnd === -1) {
4958
+ break;
4959
+ }
4783
4960
 
4784
- if (name !== "script" && name !== "style") {
4961
+ const closeToken = `</${name}>`;
4962
+ const closeIndex = lowerHtml.indexOf(closeToken, tagEnd + 1);
4963
+ const nextScriptIndex = lowerHtml.indexOf("<script", tagEnd + 1);
4964
+ const nextStyleIndex = lowerHtml.indexOf("<style", tagEnd + 1);
4965
+ const nextOpenerIndex =
4966
+ nextScriptIndex === -1
4967
+ ? nextStyleIndex
4968
+ : nextStyleIndex === -1
4969
+ ? nextScriptIndex
4970
+ : Math.min(nextScriptIndex, nextStyleIndex);
4971
+
4972
+ if (closeIndex === -1 || (nextOpenerIndex !== -1 && nextOpenerIndex < closeIndex)) {
4973
+ position = tagEnd + 1;
4785
4974
  continue;
4786
4975
  }
4787
4976
 
4788
4977
  tags.push({
4789
- attributes: parseTagAttributes(match[2] ?? ""),
4790
- content: match[3] ?? "",
4978
+ attributes: parseTagAttributes(html.slice(openerIndex + name.length + 1, tagEnd)),
4979
+ content: html.slice(tagEnd + 1, closeIndex),
4791
4980
  name,
4792
4981
  });
4982
+
4983
+ position = closeIndex + closeToken.length;
4793
4984
  }
4794
4985
 
4795
4986
  return tags;
4796
4987
  }
4797
4988
 
4989
+ export const __inlineCspTagsForTesting = inlineCspTags;
4990
+
4798
4991
  function parseTagAttributes(source: string): ReadonlyMap<string, string> {
4799
4992
  const attributes = new Map<string, string>();
4800
4993
  const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
@@ -4842,7 +5035,7 @@ function injectQueryState(html: string, state: DehydratedQueryClient): string {
4842
5035
  )}</script>`;
4843
5036
 
4844
5037
  return /<\/body>/i.test(html)
4845
- ? html.replace(/<\/body>/i, () => `${script}</body>`)
5038
+ ? replaceFinalBodyCloseTag(html, script)
4846
5039
  : `${html}${script}`;
4847
5040
  }
4848
5041
 
@@ -4856,10 +5049,21 @@ function injectAuthSessionClaims(html: string, claims: unknown): string {
4856
5049
  )}</script>`;
4857
5050
 
4858
5051
  return /<\/body>/i.test(html)
4859
- ? html.replace(/<\/body>/i, () => `${script}</body>`)
5052
+ ? replaceFinalBodyCloseTag(html, script)
4860
5053
  : `${html}${script}`;
4861
5054
  }
4862
5055
 
5056
+ function replaceFinalBodyCloseTag(html: string, insertion: string): string {
5057
+ const matches = [...html.matchAll(/<\/body>/gi)];
5058
+ const last = matches.at(-1);
5059
+
5060
+ if (last?.index === undefined) {
5061
+ return `${html}${insertion}`;
5062
+ }
5063
+
5064
+ return `${html.slice(0, last.index)}${insertion}${html.slice(last.index)}`;
5065
+ }
5066
+
4863
5067
  function authIncludesClaims(code: string): boolean {
4864
5068
  return /\bexport\s+const\s+auth\s*=\s*["']include-claims["']\s*;?/.test(code);
4865
5069
  }
@@ -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,
@@ -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
@@ -549,6 +549,7 @@ async function handleAppRouterViteRequest(
549
549
  await renderAppRequest({
550
550
  appDir: project.routesDir,
551
551
  define: options.define,
552
+ dev: true,
552
553
  importPolicy: {
553
554
  ...options.importPolicy,
554
555
  allowedSourceDirs: project.allowedSourceDirs,