@react-router/dev 7.11.0 → 7.12.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # `@react-router/dev`
2
2
 
3
+ ## 7.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add additional layer of CSRF protection by rejecting submissions to UI routes from external origins. If you need to permit access to specific external origins, you can specify them in the `react-router.config.ts` config `allowedActionOrigins` field. ([#14708](https://github.com/remix-run/react-router/pull/14708))
8
+
9
+ ### Patch Changes
10
+
11
+ - Fix `Maximum call stack size exceeded` errors when HMR is triggered against code with cyclic imports ([#14522](https://github.com/remix-run/react-router/pull/14522))
12
+
13
+ - fix(vite): Skip SSR middleware in preview server for SPA mode ([#14673](https://github.com/remix-run/react-router/pull/14673))
14
+
15
+ - \[UNSTABLE] Add a new `future.unstable_trailingSlashAwareDataRequests` flag to provide consistent behavior of `request.pathname` inside `middleware`, `loader`, and `action` functions on document and data requests when a trailing slash is present in the browser URL. ([#14644](https://github.com/remix-run/react-router/pull/14644))
16
+
17
+ Currently, your HTTP and `request` pathnames would be as follows for `/a/b/c` and `/a/b/c/`
18
+
19
+ | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\`** |
20
+ | ------------ | ----------------- | ------------------------ |
21
+ | **Document** | `/a/b/c` | `/a/b/c` ✅ |
22
+ | **Data** | `/a/b/c.data` | `/a/b/c` ✅ |
23
+
24
+ | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\`** |
25
+ | ------------- | ----------------- | ------------------------ |
26
+ | **Document** | `/a/b/c/` | `/a/b/c/` ✅ |
27
+ | **Data** | `/a/b/c.data` | `/a/b/c` ⚠️ |
28
+
29
+ With this flag enabled, these pathnames will be made consistent though a new `_.data` format for client-side `.data` requests:
30
+
31
+ | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\`** |
32
+ | ------------ | ----------------- | ------------------------ |
33
+ | **Document** | `/a/b/c` | `/a/b/c` ✅ |
34
+ | **Data** | `/a/b/c.data` | `/a/b/c` ✅ |
35
+
36
+ | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\`** |
37
+ | ------------- | ------------------ | ------------------------ |
38
+ | **Document** | `/a/b/c/` | `/a/b/c/` ✅ |
39
+ | **Data** | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅ |
40
+
41
+ This a bug fix but we are putting it behind an opt-in flag because it has the potential to be a "breaking bug fix" if you are relying on the URL format for any other application or caching logic.
42
+
43
+ Enabling this flag also changes the format of client side `.data` requests from `/_root.data` to `/_.data` when navigating to `/` to align with the new format. This does not impact the `request` pathname which is still `/` in all cases.
44
+
45
+ - Updated dependencies:
46
+ - `react-router@7.12.0`
47
+ - `@react-router/node@7.12.0`
48
+ - `@react-router/serve@7.12.0`
49
+
3
50
  ## 7.11.0
4
51
 
5
52
  ### Minor Changes
package/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @react-router/dev v7.11.0
3
+ * @react-router/dev v7.12.0
4
4
  *
5
5
  * Copyright (c) Remix Software Inc.
6
6
  *
@@ -506,10 +506,12 @@ async function resolveConfig({
506
506
  let future = {
507
507
  unstable_optimizeDeps: userAndPresetConfigs.future?.unstable_optimizeDeps ?? false,
508
508
  unstable_subResourceIntegrity: userAndPresetConfigs.future?.unstable_subResourceIntegrity ?? false,
509
+ unstable_trailingSlashAwareDataRequests: userAndPresetConfigs.future?.unstable_trailingSlashAwareDataRequests ?? false,
509
510
  v8_middleware: userAndPresetConfigs.future?.v8_middleware ?? false,
510
511
  v8_splitRouteModules: userAndPresetConfigs.future?.v8_splitRouteModules ?? false,
511
512
  v8_viteEnvironmentApi: userAndPresetConfigs.future?.v8_viteEnvironmentApi ?? false
512
513
  };
514
+ let allowedActionOrigins = userAndPresetConfigs.allowedActionOrigins ?? false;
513
515
  let reactRouterConfig = deepFreeze({
514
516
  appDirectory,
515
517
  basename: basename3,
@@ -523,6 +525,7 @@ async function resolveConfig({
523
525
  serverBundles,
524
526
  serverModuleFormat,
525
527
  ssr,
528
+ allowedActionOrigins,
526
529
  unstable_routeConfig: routeConfig
527
530
  });
528
531
  for (let preset of reactRouterUserConfig.presets ?? []) {
@@ -956,6 +959,7 @@ function generateServerBuild(ctx) {
956
959
  export const routeDiscovery: ServerBuild["routeDiscovery"];
957
960
  export const routes: ServerBuild["routes"];
958
961
  export const ssr: ServerBuild["ssr"];
962
+ export const allowedActionOrigins: ServerBuild["allowedActionOrigins"];
959
963
  export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
960
964
  }
961
965
  `;
package/dist/config.d.ts CHANGED
@@ -39,6 +39,7 @@ type ServerModuleFormat = "esm" | "cjs";
39
39
  interface FutureConfig {
40
40
  unstable_optimizeDeps: boolean;
41
41
  unstable_subResourceIntegrity: boolean;
42
+ unstable_trailingSlashAwareDataRequests: boolean;
42
43
  /**
43
44
  * Enable route middleware
44
45
  */
@@ -147,6 +148,11 @@ type ReactRouterConfig = {
147
148
  * SPA without server-rendering. Default's to `true`.
148
149
  */
149
150
  ssr?: boolean;
151
+ /**
152
+ * The allowed origins for actions / mutations. Does not apply to routes
153
+ * without a component. micromatch glob patterns are supported.
154
+ */
155
+ allowedActionOrigins?: string[];
150
156
  };
151
157
  type ResolvedReactRouterConfig = Readonly<{
152
158
  /**
@@ -212,6 +218,11 @@ type ResolvedReactRouterConfig = Readonly<{
212
218
  * SPA without server-rendering. Default's to `true`.
213
219
  */
214
220
  ssr: boolean;
221
+ /**
222
+ * The allowed origins for actions / mutations. Does not apply to routes
223
+ * without a component. micromatch glob patterns are supported.
224
+ */
225
+ allowedActionOrigins: string[] | false;
215
226
  /**
216
227
  * The resolved array of route config entries exported from `routes.ts`
217
228
  */
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v7.11.0
2
+ * @react-router/dev v7.12.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/routes.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v7.11.0
2
+ * @react-router/dev v7.12.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v7.11.0
2
+ * @react-router/dev v7.12.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -535,10 +535,12 @@ async function resolveConfig({
535
535
  let future = {
536
536
  unstable_optimizeDeps: userAndPresetConfigs.future?.unstable_optimizeDeps ?? false,
537
537
  unstable_subResourceIntegrity: userAndPresetConfigs.future?.unstable_subResourceIntegrity ?? false,
538
+ unstable_trailingSlashAwareDataRequests: userAndPresetConfigs.future?.unstable_trailingSlashAwareDataRequests ?? false,
538
539
  v8_middleware: userAndPresetConfigs.future?.v8_middleware ?? false,
539
540
  v8_splitRouteModules: userAndPresetConfigs.future?.v8_splitRouteModules ?? false,
540
541
  v8_viteEnvironmentApi: userAndPresetConfigs.future?.v8_viteEnvironmentApi ?? false
541
542
  };
543
+ let allowedActionOrigins = userAndPresetConfigs.allowedActionOrigins ?? false;
542
544
  let reactRouterConfig = deepFreeze({
543
545
  appDirectory,
544
546
  basename,
@@ -552,6 +554,7 @@ async function resolveConfig({
552
554
  serverBundles,
553
555
  serverModuleFormat,
554
556
  ssr,
557
+ allowedActionOrigins,
555
558
  unstable_routeConfig: routeConfig
556
559
  });
557
560
  for (let preset of reactRouterUserConfig.presets ?? []) {
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v7.11.0
2
+ * @react-router/dev v7.12.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -562,10 +562,12 @@ async function resolveConfig({
562
562
  let future = {
563
563
  unstable_optimizeDeps: userAndPresetConfigs.future?.unstable_optimizeDeps ?? false,
564
564
  unstable_subResourceIntegrity: userAndPresetConfigs.future?.unstable_subResourceIntegrity ?? false,
565
+ unstable_trailingSlashAwareDataRequests: userAndPresetConfigs.future?.unstable_trailingSlashAwareDataRequests ?? false,
565
566
  v8_middleware: userAndPresetConfigs.future?.v8_middleware ?? false,
566
567
  v8_splitRouteModules: userAndPresetConfigs.future?.v8_splitRouteModules ?? false,
567
568
  v8_viteEnvironmentApi: userAndPresetConfigs.future?.v8_viteEnvironmentApi ?? false
568
569
  };
570
+ let allowedActionOrigins = userAndPresetConfigs.allowedActionOrigins ?? false;
569
571
  let reactRouterConfig = deepFreeze({
570
572
  appDirectory,
571
573
  basename: basename3,
@@ -579,6 +581,7 @@ async function resolveConfig({
579
581
  serverBundles,
580
582
  serverModuleFormat,
581
583
  ssr,
584
+ allowedActionOrigins,
582
585
  unstable_routeConfig: routeConfig
583
586
  });
584
587
  for (let preset of reactRouterUserConfig.presets ?? []) {
@@ -966,6 +969,7 @@ function generateServerBuild(ctx) {
966
969
  export const routeDiscovery: ServerBuild["routeDiscovery"];
967
970
  export const routes: ServerBuild["routes"];
968
971
  export const ssr: ServerBuild["ssr"];
972
+ export const allowedActionOrigins: ServerBuild["allowedActionOrigins"];
969
973
  export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
970
974
  }
971
975
  `;
@@ -2921,7 +2925,9 @@ var reactRouterVitePlugin = () => {
2921
2925
  href: "${ctx.publicPath}@react-router/critical.css?pathname=" + pathname,
2922
2926
  };
2923
2927
  }
2924
- ` : ""}`;
2928
+ ` : ""}
2929
+ export const allowedActionOrigins = ${JSON.stringify(ctx.reactRouterConfig.allowedActionOrigins)};
2930
+ `;
2925
2931
  };
2926
2932
  let loadViteManifest = async (directory) => {
2927
2933
  let manifestContents = await (0, import_promises2.readFile)(
@@ -3536,6 +3542,9 @@ var reactRouterVitePlugin = () => {
3536
3542
  },
3537
3543
  configurePreviewServer(previewServer) {
3538
3544
  return () => {
3545
+ if (!ctx.reactRouterConfig.ssr) {
3546
+ return;
3547
+ }
3539
3548
  previewServer.middlewares.use(async (req, res, next) => {
3540
3549
  try {
3541
3550
  let serverBuildDirectory = getServerBuildDirectory(
@@ -4099,10 +4108,8 @@ var reactRouterVitePlugin = () => {
4099
4108
  if (this.environment.name !== "ssr" && modules.length <= 0) {
4100
4109
  return;
4101
4110
  }
4102
- let clientModules = uniqueNodes(
4103
- modules.flatMap(
4104
- (mod) => getParentClientNodes(server.environments.client.moduleGraph, mod)
4105
- )
4111
+ let clientModules = modules.flatMap(
4112
+ (mod) => getParentClientNodes(server.environments.client.moduleGraph, mod)
4106
4113
  );
4107
4114
  for (let clientModule of clientModules) {
4108
4115
  server.environments.client.reloadModule(clientModule);
@@ -4113,30 +4120,22 @@ var reactRouterVitePlugin = () => {
4113
4120
  warnOnClientSourceMaps()
4114
4121
  ];
4115
4122
  };
4116
- function getParentClientNodes(clientModuleGraph, module2) {
4123
+ function getParentClientNodes(clientModuleGraph, module2, seenNodes = /* @__PURE__ */ new Set()) {
4117
4124
  if (!module2.id) {
4118
4125
  return [];
4119
4126
  }
4127
+ if (seenNodes.has(module2.url)) {
4128
+ return [];
4129
+ }
4130
+ seenNodes.add(module2.url);
4120
4131
  let clientModule = clientModuleGraph.getModuleById(module2.id);
4121
4132
  if (clientModule) {
4122
4133
  return [clientModule];
4123
4134
  }
4124
4135
  return [...module2.importers].flatMap(
4125
- (importer) => getParentClientNodes(clientModuleGraph, importer)
4136
+ (importer) => getParentClientNodes(clientModuleGraph, importer, seenNodes)
4126
4137
  );
4127
4138
  }
4128
- function uniqueNodes(nodes) {
4129
- let nodeUrls = /* @__PURE__ */ new Set();
4130
- let unique = [];
4131
- for (let node of nodes) {
4132
- if (nodeUrls.has(node.url)) {
4133
- continue;
4134
- }
4135
- nodeUrls.add(node.url);
4136
- unique.push(node);
4137
- }
4138
- return unique;
4139
- }
4140
4139
  function addRefreshWrapper(reactRouterConfig, code, id) {
4141
4140
  let route = getRoute(reactRouterConfig, id);
4142
4141
  let acceptExports = route ? CLIENT_NON_COMPONENT_EXPORTS : [];
@@ -4411,7 +4410,21 @@ function getStaticPrerenderPaths(routes) {
4411
4410
  };
4412
4411
  }
4413
4412
  async function prerenderData(handler, prerenderPath, onlyRoutes, clientBuildDirectory, reactRouterConfig, viteConfig, requestInit) {
4414
- let normalizedPath = `${reactRouterConfig.basename}${prerenderPath === "/" ? "/_root.data" : `${prerenderPath.replace(/\/$/, "")}.data`}`.replace(/\/\/+/g, "/");
4413
+ let dataRequestPath;
4414
+ if (reactRouterConfig.future.unstable_trailingSlashAwareDataRequests) {
4415
+ if (prerenderPath.endsWith("/")) {
4416
+ dataRequestPath = `${prerenderPath}_.data`;
4417
+ } else {
4418
+ dataRequestPath = `${prerenderPath}.data`;
4419
+ }
4420
+ } else {
4421
+ if (prerenderPath === "/") {
4422
+ dataRequestPath = "/_root.data";
4423
+ } else {
4424
+ dataRequestPath = `${prerenderPath.replace(/\/$/, "")}.data`;
4425
+ }
4426
+ }
4427
+ let normalizedPath = `${reactRouterConfig.basename}${dataRequestPath}`.replace(/\/\/+/g, "/");
4415
4428
  let url2 = new URL(`http://localhost${normalizedPath}`);
4416
4429
  if (onlyRoutes?.length) {
4417
4430
  url2.searchParams.set("_routes", onlyRoutes.join(","));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-router/dev",
3
- "version": "7.11.0",
3
+ "version": "7.12.0",
4
4
  "description": "Dev tools and CLI for React Router",
5
5
  "homepage": "https://reactrouter.com",
6
6
  "bugs": {
@@ -92,7 +92,7 @@
92
92
  "tinyglobby": "^0.2.14",
93
93
  "valibot": "^1.2.0",
94
94
  "vite-node": "^3.2.2",
95
- "@react-router/node": "7.11.0"
95
+ "@react-router/node": "7.12.0"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@types/babel__core": "^7.20.5",
@@ -116,8 +116,8 @@
116
116
  "vite": "^6.3.0",
117
117
  "wireit": "0.14.9",
118
118
  "wrangler": "^4.23.0",
119
- "@react-router/serve": "7.11.0",
120
- "react-router": "^7.11.0"
119
+ "@react-router/serve": "7.12.0",
120
+ "react-router": "^7.12.0"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "@vitejs/plugin-rsc": "~0.5.7",
@@ -125,8 +125,8 @@
125
125
  "typescript": "^5.1.0",
126
126
  "vite": "^5.1.0 || ^6.0.0 || ^7.0.0",
127
127
  "wrangler": "^3.28.2 || ^4.0.0",
128
- "@react-router/serve": "^7.11.0",
129
- "react-router": "^7.11.0"
128
+ "@react-router/serve": "^7.12.0",
129
+ "react-router": "^7.12.0"
130
130
  },
131
131
  "peerDependenciesMeta": {
132
132
  "@vitejs/plugin-rsc": {