@reckona/mreact-router 0.0.207 → 0.0.209

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 (62) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/cloudflare.d.ts +3 -3
  3. package/dist/adapters/cloudflare.d.ts.map +1 -1
  4. package/dist/adapters/cloudflare.js +11 -4
  5. package/dist/adapters/cloudflare.js.map +1 -1
  6. package/dist/boundaries.d.ts +37 -1
  7. package/dist/boundaries.d.ts.map +1 -1
  8. package/dist/boundaries.js +125 -4
  9. package/dist/boundaries.js.map +1 -1
  10. package/dist/build.d.ts +6 -0
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/build.js +269 -24
  13. package/dist/build.js.map +1 -1
  14. package/dist/bundle-pipeline.d.ts +1 -0
  15. package/dist/bundle-pipeline.d.ts.map +1 -1
  16. package/dist/bundle-pipeline.js +4 -1
  17. package/dist/bundle-pipeline.js.map +1 -1
  18. package/dist/client.d.ts +1 -0
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +67 -7
  21. package/dist/client.js.map +1 -1
  22. package/dist/config.d.ts +7 -0
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +17 -0
  25. package/dist/config.js.map +1 -1
  26. package/dist/index.d.ts +5 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/module-runner.js +9 -1
  31. package/dist/module-runner.js.map +1 -1
  32. package/dist/render.d.ts.map +1 -1
  33. package/dist/render.js +23 -23
  34. package/dist/render.js.map +1 -1
  35. package/dist/request-header-tracking.d.ts +19 -0
  36. package/dist/request-header-tracking.d.ts.map +1 -1
  37. package/dist/request-header-tracking.js +48 -0
  38. package/dist/request-header-tracking.js.map +1 -1
  39. package/dist/tailwind-source.d.ts.map +1 -1
  40. package/dist/tailwind-source.js +147 -1
  41. package/dist/tailwind-source.js.map +1 -1
  42. package/dist/types.d.ts +14 -2
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/dist/vite.d.ts +1 -0
  46. package/dist/vite.d.ts.map +1 -1
  47. package/dist/vite.js +4 -0
  48. package/dist/vite.js.map +1 -1
  49. package/package.json +11 -11
  50. package/src/adapters/cloudflare.ts +14 -5
  51. package/src/boundaries.ts +224 -4
  52. package/src/build.ts +383 -24
  53. package/src/bundle-pipeline.ts +9 -4
  54. package/src/client.ts +104 -10
  55. package/src/config.ts +32 -0
  56. package/src/index.ts +10 -0
  57. package/src/module-runner.ts +9 -1
  58. package/src/render.ts +30 -16
  59. package/src/request-header-tracking.ts +61 -0
  60. package/src/tailwind-source.ts +164 -1
  61. package/src/types.ts +15 -2
  62. package/src/vite.ts +5 -0
package/src/client.ts CHANGED
@@ -53,6 +53,7 @@ const nodeBuiltinPackages = new Set(builtinModules.flatMap((name) => [name, `nod
53
53
  export interface ClientRouteManifestEntry {
54
54
  bytes?: number;
55
55
  css?: readonly string[];
56
+ dynamicImports?: readonly string[];
56
57
  path: string;
57
58
  kind: AppRoute["kind"];
58
59
  client: boolean;
@@ -842,14 +843,14 @@ function clientRouteComponentOrigin(options: {
842
843
  return "server-only-import";
843
844
  }
844
845
 
845
- if (options.analysis.hasUseClientDirective) {
846
- return "use-client-directive";
847
- }
848
-
849
846
  if (/\.compat(?:\.mreact)?\.[cm]?[jt]sx?$/.test(options.filename)) {
850
847
  return "compat-filename";
851
848
  }
852
849
 
850
+ if (options.analysis.hasUseClientDirective) {
851
+ return "use-client-directive";
852
+ }
853
+
853
854
  if (/\.client(?:\.mreact)?\.[cm]?[jt]sx?$/.test(options.filename)) {
854
855
  return "client-filename";
855
856
  }
@@ -1853,6 +1854,11 @@ function isClientBoundaryFallbackEligibleSource(source: string, filename?: strin
1853
1854
  }
1854
1855
  }
1855
1856
 
1857
+ sourceWithoutGuardedUndefinedCallbacks = removeSelfContainedIntrinsicHandlerAttributes(
1858
+ sourceWithoutGuardedUndefinedCallbacks,
1859
+ callbackPropNames,
1860
+ );
1861
+
1856
1862
  return (
1857
1863
  !/\bon[A-Z][A-Za-z0-9_$]*\s*=/u.test(sourceWithoutGuardedUndefinedCallbacks) &&
1858
1864
  !/\bglobalThis\b/u.test(source)
@@ -1896,6 +1902,20 @@ function propsCallbackAliasNames(source: string): Set<string> {
1896
1902
  }
1897
1903
  }
1898
1904
 
1905
+ for (const match of source.matchAll(
1906
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*props\s*\[\s*["']([A-Za-z_$][\w$]*)["']\s*\]/gu,
1907
+ )) {
1908
+ const localName = match[1];
1909
+ const propName = match[2];
1910
+
1911
+ if (
1912
+ localName !== undefined &&
1913
+ (isCallbackPropName(propName) || isCallbackPropName(localName))
1914
+ ) {
1915
+ names.add(localName);
1916
+ }
1917
+ }
1918
+
1899
1919
  for (const match of source.matchAll(
1900
1920
  /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*\(\s*props\s+as\s+[^)]+\)\.([A-Za-z_$][\w$]*)\b/gu,
1901
1921
  )) {
@@ -1978,6 +1998,14 @@ function memberCallbackNames(source: string): Set<string> {
1978
1998
  }
1979
1999
  }
1980
2000
 
2001
+ for (const match of source.matchAll(/\[\s*["']([A-Za-z_$][\w$]*)["']\s*\]/gu)) {
2002
+ const memberName = match[1];
2003
+
2004
+ if (memberName !== undefined && isCallbackPropName(memberName)) {
2005
+ names.add(memberName);
2006
+ }
2007
+ }
2008
+
1981
2009
  return names;
1982
2010
  }
1983
2011
 
@@ -2009,6 +2037,56 @@ function removeSafeCallbackHandlerAttributes(
2009
2037
  return cursor === 0 ? source : result + source.slice(cursor);
2010
2038
  }
2011
2039
 
2040
+ function removeSelfContainedIntrinsicHandlerAttributes(
2041
+ source: string,
2042
+ callbackNames: ReadonlySet<string>,
2043
+ ): string {
2044
+ const attributePattern = /\bon[A-Z][A-Za-z0-9_$]*\s*=\s*\{/gu;
2045
+ let result = "";
2046
+ let cursor = 0;
2047
+
2048
+ for (const match of source.matchAll(attributePattern)) {
2049
+ const start = match.index;
2050
+ const expressionStart = start + match[0].length;
2051
+ const end = matchingBraceEnd(source, expressionStart - 1);
2052
+
2053
+ if (end === undefined || !isIntrinsicJsxAttribute(source, start)) {
2054
+ continue;
2055
+ }
2056
+
2057
+ const expression = source.slice(expressionStart, end);
2058
+ if (
2059
+ !isInlineFunctionExpression(expression) ||
2060
+ Array.from(callbackNames).some((name) =>
2061
+ new RegExp(String.raw`\b${escapeRegExp(name)}\b`, "u").test(expression),
2062
+ )
2063
+ ) {
2064
+ continue;
2065
+ }
2066
+
2067
+ result += source.slice(cursor, start);
2068
+ cursor = end + 1;
2069
+ }
2070
+
2071
+ return cursor === 0 ? source : result + source.slice(cursor);
2072
+ }
2073
+
2074
+ function isIntrinsicJsxAttribute(source: string, attributeStart: number): boolean {
2075
+ const tagStart = source.lastIndexOf("<", attributeStart);
2076
+ if (tagStart === -1 || source.lastIndexOf(">", attributeStart) > tagStart) {
2077
+ return false;
2078
+ }
2079
+
2080
+ return /^<\s*[a-z][A-Za-z0-9:_-]*(?:\s|$)/u.test(source.slice(tagStart, attributeStart));
2081
+ }
2082
+
2083
+ function isInlineFunctionExpression(expression: string): boolean {
2084
+ return (
2085
+ /^\s*(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/u.test(expression) ||
2086
+ /^\s*(?:async\s+)?function(?:\s+[A-Za-z_$][\w$]*)?\s*\(/u.test(expression)
2087
+ );
2088
+ }
2089
+
2012
2090
  function matchingBraceEnd(source: string, openBraceIndex: number): number | undefined {
2013
2091
  let depth = 0;
2014
2092
  let quote: '"' | "'" | "`" | undefined;
@@ -3372,10 +3450,10 @@ function __mreactResolveRouteNode(value) {
3372
3450
  `
3373
3451
  : `function __mreactSyncDomRefBindings() {}
3374
3452
  `;
3375
- const boundaryOnlyHydrationBlock = routeRequiresFullHydration
3376
- || clientReferenceManifest.length === 0
3377
- ? ""
3378
- : `${routeCellHydrationIndent}if (!__mreactHasNonSerializableClientBoundaries(__mreactMarker) && __mreactHydrateClientBoundaries(document, __mreactClientReferences, __mreactClientReferenceComponents)) {
3453
+ const boundaryOnlyHydrationBlock =
3454
+ routeRequiresFullHydration || clientReferenceManifest.length === 0
3455
+ ? ""
3456
+ : `${routeCellHydrationIndent}if (!__mreactHasNonSerializableClientBoundaries(__mreactMarker) && __mreactHydrateClientBoundaries(document, __mreactClientReferences, __mreactClientReferenceComponents)) {
3379
3457
  ${routeCellHydrationIndent} __mreactMarker.setAttribute(${JSON.stringify(routeHydrationContract.hydratedAttribute)}, "true");
3380
3458
  ${routeCellHydrationIndent} __mreactMarkRouteHydrated();
3381
3459
  ${routeCellHydrationIndent} return;
@@ -3414,7 +3492,18 @@ ${routeOutOfOrderFragmentHydration} const __mreactMarker = document.querySelect
3414
3492
  const __mreactProps = __mreactPropsText === undefined
3415
3493
  ? {}
3416
3494
  : JSON.parse(__mreactPropsText);
3417
- ${restoreRequestUrl ? " if (__mreactProps.request) __mreactProps.request.url = document.URL;\n" : ""}
3495
+ ${
3496
+ restoreRequestUrl
3497
+ ? ` if (__mreactProps.request) {
3498
+ const __mreactRouteUrl = new URL(document.URL);
3499
+ __mreactProps.request.url = __mreactRouteUrl.href;
3500
+ __mreactProps.request.pathname = __mreactRouteUrl.pathname;
3501
+ __mreactProps.request.search = __mreactRouteUrl.search;
3502
+ __mreactProps.request.hash = __mreactRouteUrl.hash;
3503
+ }
3504
+ `
3505
+ : ""
3506
+ }
3418
3507
  const __mreactClientReferences = __mreactClientReferencesElement?.textContent === undefined
3419
3508
  ? []
3420
3509
  : JSON.parse(__mreactClientReferencesElement.textContent);
@@ -5400,6 +5489,10 @@ function workspaceRuntimePlugin(options: {
5400
5489
  "@reckona/mreact-compat/server",
5401
5490
  packageFile("react-compat", "@reckona/mreact-compat", "server"),
5402
5491
  ],
5492
+ [
5493
+ "@reckona/mreact-shared/server-render-value-internal",
5494
+ packageFile("shared", "@reckona/mreact-shared", "server-render-value-internal"),
5495
+ ],
5403
5496
  ["@reckona/mreact-reactive-dom", reactiveDomPath],
5404
5497
  ]);
5405
5498
 
@@ -5428,7 +5521,7 @@ function workspaceRuntimePlugin(options: {
5428
5521
  buildApi.onResolve(
5429
5522
  {
5430
5523
  filter:
5431
- /^(?:react(?:\/jsx-(?:dev-)?runtime)?|react-dom(?:\/(?:client|server))?|@reckona\/mreact-(?:compat|reactive-core|reactive-dom)(?:\/(?:event-priority|flight|internal|jsx-dev-runtime|jsx-runtime|scheduler))?)$/,
5524
+ /^(?:react(?:\/jsx-(?:dev-)?runtime)?|react-dom(?:\/(?:client|server))?|@reckona\/mreact-(?:compat|reactive-core|reactive-dom)(?:\/(?:event-priority|flight|internal|jsx-dev-runtime|jsx-runtime|scheduler))?|@reckona\/mreact-shared\/server-render-value-internal)$/,
5432
5525
  },
5433
5526
  (args) => {
5434
5527
  const path = runtimePaths.get(args.path);
@@ -5452,6 +5545,7 @@ export function emitReactiveEffectRunDevtoolsEvent() {}
5452
5545
  export function hasReactiveDevtoolsEmitter() { return false; }
5453
5546
  export function currentDevtoolsEmitter() { return undefined; }
5454
5547
  export function currentReactiveDevtools() { return undefined; }
5548
+ export function registerReactiveDevtoolsResource() { return { dispose() {}, update() {} }; }
5455
5549
  export function invalidateReactiveDevtoolsCache() {}
5456
5550
  export function prepareReactiveEffectRunDevtoolsEvent() { return undefined; }`,
5457
5551
  loader: "ts",
package/src/config.ts CHANGED
@@ -32,6 +32,12 @@ export interface AppRouterProductionOptions {
32
32
  dropClientConsole?: boolean | readonly AppRouterClientConsoleMethod[] | undefined;
33
33
  }
34
34
 
35
+ /** Opt-in compile-time constraints for route and component execution modes. */
36
+ export interface AppRouterExecutionContracts {
37
+ noCompatComponents?: readonly string[] | undefined;
38
+ serverOnlyRoutes?: readonly string[] | undefined;
39
+ }
40
+
35
41
  export interface AppRouterProjectOptions {
36
42
  assetBaseUrl?: string | undefined;
37
43
  buildConcurrency?: number | undefined;
@@ -39,6 +45,7 @@ export interface AppRouterProjectOptions {
39
45
  clientSourceMaps?: AppRouterClientSourceMapOption | undefined;
40
46
  /** Project-local server module exporting a named `dehydrateOptions` policy. */
41
47
  dehydratePolicyModule?: string | undefined;
48
+ executionContracts?: AppRouterExecutionContracts | undefined;
42
49
  /**
43
50
  * Legacy route root. When provided without routesDir/projectRoot, this keeps
44
51
  * the historical "appDir is the whole app boundary" behavior.
@@ -64,6 +71,7 @@ export interface ResolvedAppRouterProject {
64
71
  clientSourceMaps: AppRouterClientSourceMapMode;
65
72
  clientConsolePureFunctions?: readonly string[] | undefined;
66
73
  dehydratePolicyModule?: string | undefined;
74
+ executionContracts?: AppRouterExecutionContracts | undefined;
67
75
  projectRoot: string;
68
76
  publicAssetBaseUrl?: string | undefined;
69
77
  publicDir: string;
@@ -105,6 +113,9 @@ export function resolveAppRouterProjectOptions(
105
113
  allowedSourceDirs,
106
114
  ),
107
115
  }),
116
+ ...(options.executionContracts === undefined
117
+ ? {}
118
+ : { executionContracts: normalizeExecutionContracts(options.executionContracts) }),
108
119
  projectRoot: appDir,
109
120
  ...(options.publicAssetBaseUrl === undefined
110
121
  ? {}
@@ -140,6 +151,9 @@ export function resolveAppRouterProjectOptions(
140
151
  allowedSourceDirs,
141
152
  ),
142
153
  }),
154
+ ...(options.executionContracts === undefined
155
+ ? {}
156
+ : { executionContracts: normalizeExecutionContracts(options.executionContracts) }),
143
157
  projectRoot,
144
158
  ...(options.publicAssetBaseUrl === undefined
145
159
  ? {}
@@ -149,6 +163,24 @@ export function resolveAppRouterProjectOptions(
149
163
  };
150
164
  }
151
165
 
166
+ function normalizeExecutionContracts(
167
+ contracts: AppRouterExecutionContracts,
168
+ ): AppRouterExecutionContracts {
169
+ const noCompatComponents = uniquePatterns(contracts.noCompatComponents);
170
+ const serverOnlyRoutes = uniquePatterns(contracts.serverOnlyRoutes);
171
+
172
+ return {
173
+ ...(noCompatComponents.length === 0 ? {} : { noCompatComponents }),
174
+ ...(serverOnlyRoutes.length === 0 ? {} : { serverOnlyRoutes }),
175
+ };
176
+ }
177
+
178
+ function uniquePatterns(patterns: readonly string[] | undefined): string[] {
179
+ return [...new Set((patterns ?? []).filter((pattern) => pattern.length > 0))].sort(
180
+ (left, right) => left.localeCompare(right),
181
+ );
182
+ }
183
+
152
184
  function resolvePolicyModule(
153
185
  projectRoot: string,
154
186
  modulePath: string,
package/src/index.ts CHANGED
@@ -5,13 +5,20 @@ export {
5
5
  createBoundaryReport,
6
6
  formatBoundaryReport,
7
7
  formatBoundaryReportJson,
8
+ validateBoundaryExecutionContracts,
8
9
  } from "./boundaries.js";
9
10
  export type {
10
11
  AnalyzeAppBoundariesOptions,
12
+ BoundaryExecutionMode,
13
+ BoundaryReportByteCost,
11
14
  BoundaryReport,
12
15
  BoundaryReportComponent,
16
+ BoundaryReportCost,
17
+ BoundaryReportDecision,
13
18
  BoundaryReportRoute,
14
19
  BoundaryReportSummary,
20
+ BoundarySourcePosition,
21
+ BoundarySourceRange,
15
22
  CreateBoundaryReportInput,
16
23
  CreateBoundaryReportRouteInput,
17
24
  } from "./boundaries.js";
@@ -142,6 +149,7 @@ export async function rotateSession<TData>(
142
149
  export type {
143
150
  AwsLambdaArtifactManifest,
144
151
  AwsLambdaGeneratedHandlerPreloadMode,
152
+ BoundaryCostOptions,
145
153
  BuildAppPhase,
146
154
  BuildAppPhaseTiming,
147
155
  BuildAppOptions,
@@ -195,6 +203,7 @@ export type {
195
203
  RobotsRule,
196
204
  RouteHeadDescriptor,
197
205
  RouteHandlerContext,
206
+ RouteLocation,
198
207
  RouteLoader,
199
208
  RouteMetadata,
200
209
  RouteParams,
@@ -208,6 +217,7 @@ export type {
208
217
  AppRouterClientConsoleMethod,
209
218
  AppRouterClientSourceMapMode,
210
219
  AppRouterClientSourceMapOption,
220
+ AppRouterExecutionContracts,
211
221
  AppRouterProductionOptions,
212
222
  } from "./config.js";
213
223
  export type {
@@ -1381,6 +1381,14 @@ function workspacePackageResolutionPlugin() {
1381
1381
  "@reckona/mreact-server",
1382
1382
  { entry: "index", monorepoDir: "server", packageName: "@reckona/mreact-server" },
1383
1383
  ],
1384
+ [
1385
+ "@reckona/mreact-shared/server-render-value-internal",
1386
+ {
1387
+ entry: "server-render-value-internal",
1388
+ monorepoDir: "shared",
1389
+ packageName: "@reckona/mreact-shared",
1390
+ },
1391
+ ],
1384
1392
  ]);
1385
1393
  const routerEntries = new Map([
1386
1394
  ["@reckona/mreact-router", join(packageRoot, sourceOrDist)],
@@ -1400,7 +1408,7 @@ function workspacePackageResolutionPlugin() {
1400
1408
  buildApi.onResolve(
1401
1409
  {
1402
1410
  filter:
1403
- /^(?:react(?:\/jsx-(?:dev-)?runtime)?|react-dom(?:\/(?:client|server))?|@reckona\/(?:mreact(?:\/(?:jsx-dev-runtime|jsx-runtime))?|mreact-(?:auth|query|reactive-core|server|router|compat)(?:\/(?:event-priority|flight|internal|jsx-dev-runtime|jsx-runtime|scheduler|app-router-globals|link|native-escape|navigation-state|session|stream-list|internal\/native-escape|internal\/session))?))$/,
1411
+ /^(?:react(?:\/jsx-(?:dev-)?runtime)?|react-dom(?:\/(?:client|server))?|@reckona\/(?:mreact(?:\/(?:jsx-dev-runtime|jsx-runtime))?|mreact-shared\/server-render-value-internal|mreact-(?:auth|query|reactive-core|server|router|compat)(?:\/(?:event-priority|flight|internal|jsx-dev-runtime|jsx-runtime|scheduler|app-router-globals|link|native-escape|navigation-state|session|stream-list|internal\/native-escape|internal\/session))?))$/,
1404
1412
  },
1405
1413
  (args) => {
1406
1414
  const routerPath = routerEntries.get(args.path);
package/src/render.ts CHANGED
@@ -64,7 +64,11 @@ import {
64
64
  } from "./cache.js";
65
65
  import {
66
66
  trackRequestHeaderReads,
67
+ routeLocationFromRequest,
68
+ routeLocationForHydration,
69
+ routeLocationFromUrl,
67
70
  type TrackedHeaderRequest,
71
+ withTrackedRouteLocation,
68
72
  withTrackedRequest,
69
73
  } from "./request-header-tracking.js";
70
74
  import { configuredHstsHeader, hasInvalidConfiguredHsts } from "./security-headers.js";
@@ -108,6 +112,7 @@ import { viteDefineCacheKey, vitePluginsCacheKey } from "./vite-plugin-cache-key
108
112
  import type {
109
113
  ManifestDescriptor,
110
114
  RobotsManifest,
115
+ RouteLocation,
111
116
  RouteParams,
112
117
  RouteMetadata,
113
118
  SitemapEntry,
@@ -543,7 +548,7 @@ interface ServerComponentProps {
543
548
  data: unknown;
544
549
  params: RouteParams;
545
550
  queryClient: QueryClient;
546
- request: Request;
551
+ request: RouteLocation;
547
552
  }
548
553
 
549
554
  type ServerComponent = (props: ServerComponentProps) => string | PromiseLike<string>;
@@ -1185,7 +1190,7 @@ async function renderAppRequestInternal(
1185
1190
  clientRoute,
1186
1191
  props: {
1187
1192
  params: matched.params,
1188
- request: { url: url.pathname },
1193
+ request: routeLocationForHydration(url),
1189
1194
  },
1190
1195
  routePath: matched.route.path,
1191
1196
  script: clientScript,
@@ -1252,7 +1257,7 @@ async function renderAppRequestInternal(
1252
1257
  const renderedPage = await runWithQueryClient(queryClient, () =>
1253
1258
  runServerModuleWithSlots(
1254
1259
  stringOutput.code,
1255
- withTrackedRequest(
1260
+ withTrackedRouteLocation(
1256
1261
  {
1257
1262
  data,
1258
1263
  params: matched.params,
@@ -1260,6 +1265,7 @@ async function renderAppRequestInternal(
1260
1265
  },
1261
1266
  appRequest,
1262
1267
  trackedRequest,
1268
+ routeLocationFromUrl(url),
1263
1269
  ),
1264
1270
  matched.route.file,
1265
1271
  options.serverModules,
@@ -1281,7 +1287,7 @@ async function renderAppRequestInternal(
1281
1287
  script: clientScript,
1282
1288
  props: {
1283
1289
  params: matched.params,
1284
- request: { url: url.pathname },
1290
+ request: routeLocationForHydration(url),
1285
1291
  data,
1286
1292
  },
1287
1293
  })
@@ -1296,7 +1302,7 @@ async function renderAppRequestInternal(
1296
1302
  appDir: options.appDir,
1297
1303
  pageFile: matched.route.file,
1298
1304
  html: pageHtmlForLayout,
1299
- props: withTrackedRequest(
1305
+ props: withTrackedRouteLocation(
1300
1306
  {
1301
1307
  data,
1302
1308
  params: matched.params,
@@ -1304,6 +1310,7 @@ async function renderAppRequestInternal(
1304
1310
  },
1305
1311
  appRequest,
1306
1312
  trackedRequest,
1313
+ routeLocationFromUrl(url),
1307
1314
  ),
1308
1315
  slots: renderedPage.slots,
1309
1316
  serverModules: options.serverModules,
@@ -1437,7 +1444,7 @@ async function renderAppRequestInternal(
1437
1444
  params: matched.params,
1438
1445
  queryClient,
1439
1446
  request: appRequest,
1440
- requestUrl: url.pathname,
1447
+ requestUrl: url.href,
1441
1448
  trackedRequest,
1442
1449
  routePath: matched.route.path,
1443
1450
  routeScripts: options.clientScripts,
@@ -1470,7 +1477,7 @@ async function renderAppRequestInternal(
1470
1477
  }
1471
1478
 
1472
1479
  const data = streamData;
1473
- const props = withTrackedRequest(
1480
+ const props = withTrackedRouteLocation(
1474
1481
  {
1475
1482
  data,
1476
1483
  params: matched.params,
@@ -1478,6 +1485,7 @@ async function renderAppRequestInternal(
1478
1485
  },
1479
1486
  appRequest,
1480
1487
  trackedRequest,
1488
+ routeLocationFromUrl(url),
1481
1489
  );
1482
1490
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1483
1491
  const stream = runServerStreamModule(output.code, {
@@ -1487,7 +1495,7 @@ async function renderAppRequestInternal(
1487
1495
  markerRequest: options.request,
1488
1496
  prerenderVariantCapture: options.prerenderVariantCapture,
1489
1497
  props,
1490
- requestUrl: url.pathname,
1498
+ requestUrl: url.href,
1491
1499
  routePath: matched.route.path,
1492
1500
  routeScripts: options.clientScripts,
1493
1501
  serverModules: options.serverModules,
@@ -1557,7 +1565,7 @@ async function renderAppRequestInternal(
1557
1565
  const renderedPage = await runWithQueryClient(queryClient, () =>
1558
1566
  runServerModuleWithSlots(
1559
1567
  output.code,
1560
- withTrackedRequest(
1568
+ withTrackedRouteLocation(
1561
1569
  {
1562
1570
  data,
1563
1571
  params: matched.params,
@@ -1565,6 +1573,7 @@ async function renderAppRequestInternal(
1565
1573
  },
1566
1574
  appRequest,
1567
1575
  trackedRequest,
1576
+ routeLocationFromUrl(url),
1568
1577
  ),
1569
1578
  matched.route.file,
1570
1579
  options.serverModules,
@@ -1592,7 +1601,7 @@ async function renderAppRequestInternal(
1592
1601
  script: clientScript,
1593
1602
  props: {
1594
1603
  params: matched.params,
1595
- request: { url: url.pathname },
1604
+ request: routeLocationForHydration(url),
1596
1605
  data,
1597
1606
  },
1598
1607
  })
@@ -1608,7 +1617,7 @@ async function renderAppRequestInternal(
1608
1617
  appDir: options.appDir,
1609
1618
  pageFile: matched.route.file,
1610
1619
  html: pageHtmlForLayout,
1611
- props: withTrackedRequest(
1620
+ props: withTrackedRouteLocation(
1612
1621
  {
1613
1622
  data,
1614
1623
  params: matched.params,
@@ -1616,6 +1625,7 @@ async function renderAppRequestInternal(
1616
1625
  },
1617
1626
  appRequest,
1618
1627
  trackedRequest,
1628
+ routeLocationFromUrl(url),
1619
1629
  ),
1620
1630
  slots: renderedPage.slots,
1621
1631
  serverModules: options.serverModules,
@@ -2300,7 +2310,7 @@ function errorBoundaryProps(error: unknown, request: Request, routePath: string
2300
2310
  error: normalizeErrorForProps(error),
2301
2311
  params: {},
2302
2312
  queryClient: createQueryClient(),
2303
- request,
2313
+ request: routeLocationFromRequest(request),
2304
2314
  requestId: requestIdForErrorContext(request),
2305
2315
  routeId: routeIdForPath(routePath ?? new URL(request.url).pathname),
2306
2316
  traceId: traceContextFromRequest(request)?.traceId,
@@ -3550,7 +3560,9 @@ function runServerStreamModule(
3550
3560
  script: options.script,
3551
3561
  props: {
3552
3562
  params: options.props.params,
3553
- request: { url: options.requestUrl },
3563
+ request: routeLocationForHydration(
3564
+ new URL(options.requestUrl, options.markerRequest.url),
3565
+ ),
3554
3566
  data: options.props.data,
3555
3567
  },
3556
3568
  })
@@ -3797,7 +3809,7 @@ async function runServerStreamModuleWithLoading(
3797
3809
  importPolicy?: AppRouterImportPolicy | undefined;
3798
3810
  },
3799
3811
  ): Promise<ReadableStream<Uint8Array> | Response> {
3800
- const loadingProps = withTrackedRequest(
3812
+ const loadingProps = withTrackedRouteLocation(
3801
3813
  {
3802
3814
  data: undefined,
3803
3815
  params: options.params,
@@ -3805,6 +3817,7 @@ async function runServerStreamModuleWithLoading(
3805
3817
  },
3806
3818
  options.request,
3807
3819
  options.trackedRequest,
3820
+ routeLocationFromUrl(new URL(options.requestUrl)),
3808
3821
  );
3809
3822
  const layoutShells = await layoutShellsForPage(
3810
3823
  options.appDir,
@@ -3836,7 +3849,7 @@ async function runServerStreamModuleWithLoading(
3836
3849
  script: options.script,
3837
3850
  props: {
3838
3851
  params: options.params,
3839
- request: { url: options.requestUrl },
3852
+ request: routeLocationForHydration(new URL(options.requestUrl)),
3840
3853
  },
3841
3854
  })
3842
3855
  : serverOnlyRouteMarkerParts(
@@ -3879,7 +3892,7 @@ async function runServerStreamModuleWithLoading(
3879
3892
  await appendServerStreamModule(
3880
3893
  code,
3881
3894
  boundarySink,
3882
- withTrackedRequest(
3895
+ withTrackedRouteLocation(
3883
3896
  {
3884
3897
  data,
3885
3898
  params: options.params,
@@ -3887,6 +3900,7 @@ async function runServerStreamModuleWithLoading(
3887
3900
  },
3888
3901
  options.request,
3889
3902
  options.trackedRequest,
3903
+ routeLocationFromUrl(new URL(options.requestUrl)),
3890
3904
  ),
3891
3905
  options.pageFile,
3892
3906
  options.serverModules,
@@ -1,3 +1,5 @@
1
+ import type { RouteLocation } from "./types.js";
2
+
1
3
  /**
2
4
  * Observes request inputs read by application code during a render.
3
5
  *
@@ -159,3 +161,62 @@ export function withTrackedRequest<T extends object>(
159
161
 
160
162
  return context;
161
163
  }
164
+
165
+ /**
166
+ * Adds the serializable location view used by shared page and layout props.
167
+ * The full Request remains available to loaders, handlers, and metadata code.
168
+ */
169
+ export function withTrackedRouteLocation<T extends object>(
170
+ values: T,
171
+ request: Request,
172
+ tracked: TrackedHeaderRequest | undefined,
173
+ location?: RouteLocation,
174
+ ): T & { request: RouteLocation } {
175
+ const context = values as T & { request: RouteLocation };
176
+ let currentLocation = location ?? routeLocationFromRequest(request);
177
+
178
+ Object.defineProperty(context, "request", {
179
+ configurable: true,
180
+ enumerable: true,
181
+ get() {
182
+ tracked?.markRequestAccess();
183
+ return currentLocation;
184
+ },
185
+ set(value: RouteLocation) {
186
+ tracked?.markRequestAccess();
187
+ currentLocation = value;
188
+ },
189
+ });
190
+
191
+ return context;
192
+ }
193
+
194
+ /** Returns the URL fields that are safe to serialize into route HTML. */
195
+ export function routeLocationFromRequest(request: Request): RouteLocation {
196
+ return routeLocationFromUrl(new URL(request.url));
197
+ }
198
+
199
+ /** Returns a serializable route location from a URL without request metadata. */
200
+ export function routeLocationFromUrl(url: URL): RouteLocation {
201
+ return {
202
+ hash: url.hash,
203
+ pathname: url.pathname,
204
+ search: url.search,
205
+ url: url.href,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Returns the route location placeholder embedded in cached HTML. The client
211
+ * hydration runtime replaces it with document.URL before invoking shared code.
212
+ * Query values stay out of reusable HTML because they can contain credentials
213
+ * or other request-specific data.
214
+ */
215
+ export function routeLocationForHydration(url: URL): RouteLocation {
216
+ return {
217
+ hash: "",
218
+ pathname: url.pathname,
219
+ search: "",
220
+ url: url.pathname,
221
+ };
222
+ }