@reckona/mreact-router 0.0.208 → 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 (58) 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 +265 -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 +14 -4
  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/render.d.ts.map +1 -1
  31. package/dist/render.js +23 -23
  32. package/dist/render.js.map +1 -1
  33. package/dist/request-header-tracking.d.ts +19 -0
  34. package/dist/request-header-tracking.d.ts.map +1 -1
  35. package/dist/request-header-tracking.js +48 -0
  36. package/dist/request-header-tracking.js.map +1 -1
  37. package/dist/tailwind-source.d.ts.map +1 -1
  38. package/dist/tailwind-source.js +147 -1
  39. package/dist/tailwind-source.js.map +1 -1
  40. package/dist/types.d.ts +14 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/dist/vite.d.ts +1 -0
  44. package/dist/vite.d.ts.map +1 -1
  45. package/dist/vite.js.map +1 -1
  46. package/package.json +11 -11
  47. package/src/adapters/cloudflare.ts +14 -5
  48. package/src/boundaries.ts +224 -4
  49. package/src/build.ts +379 -24
  50. package/src/bundle-pipeline.ts +9 -4
  51. package/src/client.ts +18 -5
  52. package/src/config.ts +32 -0
  53. package/src/index.ts +10 -0
  54. package/src/render.ts +30 -16
  55. package/src/request-header-tracking.ts +61 -0
  56. package/src/tailwind-source.ts +164 -1
  57. package/src/types.ts +15 -2
  58. package/src/vite.ts +1 -0
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 {
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
+ }
@@ -14,7 +14,170 @@ export function prependTailwindSourceDirectives(options: {
14
14
  .map((sourceDir) => `${tailwindSourceDirective(cssDir, sourceDir)}\n`)
15
15
  .join("");
16
16
 
17
- return directives.length === 0 ? options.code : `${directives}${options.code}`;
17
+ if (directives.length === 0) {
18
+ return options.code;
19
+ }
20
+
21
+ const insertionIndex = leadingCssPreludeEnd(options.code);
22
+ const prefix = options.code.slice(0, insertionIndex);
23
+ const suffix = options.code.slice(insertionIndex);
24
+ const lineEnding = suffix.startsWith("\r\n") ? "\r\n" : "\n";
25
+ const existingLineEnding = suffix.startsWith(lineEnding) ? lineEnding : "";
26
+ const separator = insertionIndex === 0 ? "" : existingLineEnding || lineEnding;
27
+ const formattedDirectives = directives.replaceAll("\n", lineEnding);
28
+ return `${prefix}${separator}${formattedDirectives}${suffix.slice(existingLineEnding.length)}`;
29
+ }
30
+
31
+ function leadingCssPreludeEnd(code: string): number {
32
+ let cursor = 0;
33
+ let lastRuleEnd = 0;
34
+
35
+ for (;;) {
36
+ cursor = skipCssTrivia(code, cursor);
37
+ const ruleEnd = consumeLeadingCssRule(code, cursor);
38
+ if (ruleEnd === undefined) {
39
+ return lastRuleEnd;
40
+ }
41
+
42
+ lastRuleEnd = ruleEnd;
43
+ cursor = ruleEnd;
44
+ }
45
+ }
46
+
47
+ function skipCssTrivia(code: string, start: number): number {
48
+ let cursor = start;
49
+ while (cursor < code.length) {
50
+ if (/\s/u.test(code[cursor] ?? "")) {
51
+ cursor += 1;
52
+ continue;
53
+ }
54
+
55
+ if (code.startsWith("/*", cursor)) {
56
+ const commentEnd = code.indexOf("*/", cursor + 2);
57
+ if (commentEnd < 0) {
58
+ return code.length;
59
+ }
60
+ cursor = commentEnd + 2;
61
+ continue;
62
+ }
63
+
64
+ break;
65
+ }
66
+
67
+ return cursor;
68
+ }
69
+
70
+ function consumeLeadingCssRule(code: string, start: number): number | undefined {
71
+ const match = /^@(charset|import|layer)(?=\s|[;{]|\/\*)/iu.exec(code.slice(start));
72
+ const ruleName = match?.[1]?.toLowerCase();
73
+ if (match === null || ruleName === undefined) {
74
+ return undefined;
75
+ }
76
+
77
+ let quote: '"' | "'" | undefined;
78
+ let parentheses = 0;
79
+ for (let cursor = start + match[0].length; cursor < code.length; cursor += 1) {
80
+ const character = code[cursor];
81
+ if (character === undefined) {
82
+ return undefined;
83
+ }
84
+
85
+ if (quote !== undefined) {
86
+ if (character === "\\") {
87
+ cursor += 1;
88
+ } else if (character === quote) {
89
+ quote = undefined;
90
+ }
91
+ continue;
92
+ }
93
+
94
+ if (code.startsWith("/*", cursor)) {
95
+ const commentEnd = code.indexOf("*/", cursor + 2);
96
+ if (commentEnd < 0) {
97
+ return undefined;
98
+ }
99
+ cursor = commentEnd + 1;
100
+ continue;
101
+ }
102
+
103
+ if (character === '"' || character === "'") {
104
+ quote = character;
105
+ continue;
106
+ }
107
+ if (character === "(") {
108
+ parentheses += 1;
109
+ continue;
110
+ }
111
+ if (character === ")") {
112
+ parentheses = Math.max(0, parentheses - 1);
113
+ continue;
114
+ }
115
+ if (character === ";" && parentheses === 0) {
116
+ return cursor + 1;
117
+ }
118
+ if (character === "{" && parentheses === 0) {
119
+ if (ruleName !== "layer") {
120
+ return undefined;
121
+ }
122
+
123
+ const blockEnd = consumeCssBlock(code, cursor);
124
+ if (blockEnd === undefined || !isCssTrivia(code.slice(cursor + 1, blockEnd - 1))) {
125
+ return undefined;
126
+ }
127
+ return blockEnd;
128
+ }
129
+ if (character === "}" && parentheses === 0) {
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ return undefined;
135
+ }
136
+
137
+ function consumeCssBlock(code: string, start: number): number | undefined {
138
+ let depth = 1;
139
+ let quote: '"' | "'" | undefined;
140
+ for (let cursor = start + 1; cursor < code.length; cursor += 1) {
141
+ const character = code[cursor];
142
+ if (character === undefined) {
143
+ return undefined;
144
+ }
145
+
146
+ if (quote !== undefined) {
147
+ if (character === "\\") {
148
+ cursor += 1;
149
+ } else if (character === quote) {
150
+ quote = undefined;
151
+ }
152
+ continue;
153
+ }
154
+
155
+ if (code.startsWith("/*", cursor)) {
156
+ const commentEnd = code.indexOf("*/", cursor + 2);
157
+ if (commentEnd < 0) {
158
+ return undefined;
159
+ }
160
+ cursor = commentEnd + 1;
161
+ continue;
162
+ }
163
+
164
+ if (character === '"' || character === "'") {
165
+ quote = character;
166
+ } else if (character === "{") {
167
+ depth += 1;
168
+ } else if (character === "}") {
169
+ depth -= 1;
170
+ if (depth === 0) {
171
+ return cursor + 1;
172
+ }
173
+ }
174
+ }
175
+
176
+ return undefined;
177
+ }
178
+
179
+ function isCssTrivia(value: string): boolean {
180
+ return skipCssTrivia(value, 0) === value.length;
18
181
  }
19
182
 
20
183
  function isTailwindCssEntry(code: string): boolean {
package/src/types.ts CHANGED
@@ -62,13 +62,26 @@ export interface RouteHandlerContext<TParams extends RouteParams = RouteParams>
62
62
  request: Request;
63
63
  }
64
64
 
65
+ /**
66
+ * Serializable location data passed to shared page and layout components.
67
+ *
68
+ * Server-only request data such as headers, body, credentials, and abort
69
+ * signals is available through loader and handler contexts instead.
70
+ */
71
+ export interface RouteLocation {
72
+ readonly hash: string;
73
+ readonly pathname: string;
74
+ readonly search: string;
75
+ readonly url: string;
76
+ }
77
+
65
78
  /**
66
79
  * Provides loader data, params, and request context to page components.
67
80
  */
68
81
  export interface PageProps<TData = unknown, TParams extends RouteParams = RouteParams> {
69
82
  data: TData;
70
83
  params: TParams;
71
- request: Request;
84
+ request: RouteLocation;
72
85
  }
73
86
 
74
87
  /**
@@ -95,7 +108,7 @@ export function definePage<TLoader extends RouteLoader>(
95
108
  export interface LayoutProps<TParams extends RouteParams = RouteParams> {
96
109
  children: ReactCompatNode;
97
110
  params: TParams;
98
- request: Request;
111
+ request: RouteLocation;
99
112
  }
100
113
 
101
114
  /**
package/src/vite.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  type AppRouterProjectOptions,
25
25
  type ResolvedAppRouterProject,
26
26
  } from "./config.js";
27
+ export type { AppRouterExecutionContracts } from "./config.js";
27
28
  import type { AppRouterImportPolicy } from "./import-policy.js";
28
29
  import { dehydrateOptionsFromModule } from "./dehydrate-policy.js";
29
30
  import {