@netlify/vite-plugin-react-router 1.0.1 → 2.0.1

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.1](https://github.com/netlify/remix-compute/compare/vite-plugin-react-router-v2.0.0...vite-plugin-react-router-v2.0.1) (2025-10-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * populate value for Netlify React Router context in dev ([#550](https://github.com/netlify/remix-compute/issues/550)) ([8d2c9fd](https://github.com/netlify/remix-compute/commit/8d2c9fd21090ca063d99a9e7094393932d870801))
9
+
10
+ ## [2.0.0](https://github.com/netlify/remix-compute/compare/vite-plugin-react-router-v1.0.1...vite-plugin-react-router-v2.0.0) (2025-10-16)
11
+
12
+ ### ⚠ BREAKING CHANGES
13
+
14
+ * **@netlify/vite-plugin-react-router:** require React Router 7.9.0+ ([#546](https://github.com/netlify/remix-compute/issues/546))
15
+
16
+ Please make sure to upgrade `react-router` and other `@react-router/*` dependencies to 7.9.0 or later. Older versions are no longer supported.
17
+
18
+ ### Features
19
+
20
+ * **@netlify/vite-plugin-react-router:** support React Router middleware ([#546](https://github.com/netlify/remix-compute/issues/546)) ([435fa15](https://github.com/netlify/remix-compute/commit/435fa158d80d78e86b0259c3a668c774ef70c565))
21
+
3
22
  ## [1.0.1](https://github.com/netlify/remix-compute/compare/vite-plugin-react-router-v1.0.0...vite-plugin-react-router-v1.0.1) (2025-04-04)
4
23
 
5
24
 
package/README.md CHANGED
@@ -3,27 +3,125 @@
3
3
  The React Router Adapter for Netlify allows you to deploy your [React Router](https://reactrouter.com) app to
4
4
  [Netlify Functions](https://docs.netlify.com/functions/overview/).
5
5
 
6
- To deploy a React Router 7+ site to Netlify, install this package:
7
-
8
6
  ## How to use
9
7
 
8
+ To deploy a React Router 7+ site to Netlify, install this package:
9
+
10
10
  ```sh
11
11
  npm install --save-dev @netlify/vite-plugin-react-router
12
12
  ```
13
13
 
14
+ It's also recommended (but not required) to use the
15
+ [Netlify Vite plugin](https://www.npmjs.com/package/@netlify/vite-plugin), which provides full Netlify platform
16
+ emulation directly in your local dev server:
17
+
18
+ ```sh
19
+ npm install --save-dev @netlify/vite-plugin
20
+ ```
21
+
14
22
  and include the Netlify plugin in your `vite.config.ts`:
15
23
 
16
24
  ```typescript
17
25
  import { reactRouter } from '@react-router/dev/vite'
18
26
  import { defineConfig } from 'vite'
19
27
  import tsconfigPaths from 'vite-tsconfig-paths'
20
- import netlifyPlugin from '@netlify/vite-plugin-react-router' // <- add this
28
+ import netlifyReactRouter from '@netlify/vite-plugin-react-router' // <- add this
29
+ import netlify from '@netlify/vite-plugin' // <- add this (optional)
21
30
 
22
31
  export default defineConfig({
23
32
  plugins: [
24
33
  reactRouter(),
25
34
  tsconfigPaths(),
26
- netlifyPlugin(), // <- add this
35
+ netlifyReactRouter(), // <- add this
36
+ netlify(), // <- add this (optional)
27
37
  ],
28
38
  })
29
39
  ```
40
+
41
+ ### Load context
42
+
43
+ This plugin automatically includes all
44
+ [Netlify context](https://docs.netlify.com/build/functions/api/#netlify-specific-context-object) fields on loader and
45
+ action context.
46
+
47
+ If you're using TypeScript, `AppLoadContext` is automatically aware of these fields
48
+ ([via module augmentation](https://reactrouter.com/upgrading/remix#9-update-types-for-apploadcontext)).
49
+
50
+ For example:
51
+
52
+ ```tsx
53
+ import { useLoaderData } from 'react-router'
54
+ import type { Route } from './+types/example'
55
+
56
+ export async function loader({ context }: Route.LoaderArgs) {
57
+ return {
58
+ country: context.geo?.country?.name ?? 'an unknown country',
59
+ }
60
+ }
61
+ export default function Example() {
62
+ const { country } = useLoaderData<typeof loader>()
63
+ return <div>You are visiting from {country}</div>
64
+ }
65
+ ```
66
+
67
+ If you've [opted in to the `future.v8_middleware` flag](https://reactrouter.com/how-to/middleware), you can still use
68
+ the above access pattern for backwards compatibility, but loader and action context will now be an instance of the
69
+ type-safe `RouterContextProvider`. Note that this requires requires v2.0.0+ of `@netlify/vite-plugin-react-router`.
70
+
71
+ For example:
72
+
73
+ ```tsx
74
+ import { netlifyRouterContext } from '@netlify/vite-plugin-react-router'
75
+ import { useLoaderData } from 'react-router'
76
+ import type { Route } from './+types/example'
77
+
78
+ export async function loader({ context }: Route.LoaderArgs) {
79
+ return {
80
+ country: context.get(netlifyRouterContext).geo?.country?.name ?? 'an unknown country',
81
+ }
82
+ }
83
+ export default function Example() {
84
+ const { country } = useLoaderData<typeof loader>()
85
+ return <div>You are visiting from {country}</div>
86
+ }
87
+ ```
88
+
89
+ > [!IMPORTANT]
90
+ >
91
+ > Note that in local development, `netlifyRouterContext` requires Netlify platform emulation, which is provided
92
+ > seamlessly by [`@netlify/vite-plugin`](https://www.npmjs.com/package/@netlify/vite-plugin) (or Netlify CLI - up to
93
+ > you).
94
+
95
+ ### Middleware context
96
+
97
+ React Router introduced a stable middleware feature in 7.9.0.
98
+
99
+ To use middleware,
100
+ [opt in to the feature via `future.v8_middleware` and follow the docs](https://reactrouter.com/how-to/middleware). Note
101
+ that this requires requires v2.0.0+ of `@netlify/vite-plugin-react-router`.
102
+
103
+ To access the [Netlify context](https://docs.netlify.com/build/functions/api/#netlify-specific-context-object)
104
+ specifically, you must import our `RouterContextProvider` instance:
105
+
106
+ ```tsx
107
+ import { netlifyRouterContext } from '@netlify/vite-plugin-react-router'
108
+
109
+ import type { Route } from './+types/home'
110
+
111
+ const logMiddleware: Route.MiddlewareFunction = async ({ request, context }) => {
112
+ const country = context.get(netlifyRouterContext).geo?.country?.name ?? 'unknown'
113
+ console.log(`Handling ${request.method} request to ${request.url} from ${country}`)
114
+ }
115
+
116
+ export const middleware: Route.MiddlewareFunction[] = [logMiddleware]
117
+
118
+ export default function Home() {
119
+ return <h1>Hello world</h1>
120
+ }
121
+ ```
122
+
123
+ > [!IMPORTANT]
124
+ >
125
+ > Note that in local development, `netlifyRouterContext` requires Netlify platform emulation, which is provided
126
+ > seamlessly by [`@netlify/vite-plugin`](https://www.npmjs.com/package/@netlify/vite-plugin) (or Netlify CLI - up to
127
+ > you).
package/dist/index.d.mts CHANGED
@@ -1,17 +1,34 @@
1
- import { AppLoadContext, ServerBuild } from 'react-router';
1
+ import * as react_router from 'react-router';
2
+ import { AppLoadContext, RouterContextProvider, ServerBuild } from 'react-router';
2
3
  import { Context } from '@netlify/functions';
3
4
  import { Plugin } from 'vite';
4
5
 
5
- type LoadContext = AppLoadContext & Context;
6
+ declare module 'react-router' {
7
+ interface AppLoadContext extends Context {
8
+ }
9
+ }
6
10
  /**
7
- * A function that returns the value to use as `context` in route `loader` and
8
- * `action` functions.
11
+ * A function that returns the value to use as `context` in route `loader` and `action` functions.
9
12
  *
10
- * You can think of this as an escape hatch that allows you to pass
11
- * environment/platform-specific values through to your loader/action.
13
+ * You can think of this as an escape hatch that allows you to pass environment/platform-specific
14
+ * values through to your loader/action.
15
+ *
16
+ * NOTE: v7.9.0 introduced a breaking change when the user opts in to `future.v8_middleware`. This
17
+ * requires returning an instance of `RouterContextProvider` instead of a plain object. We have a
18
+ * peer dependency on >=7.9.0 so we can safely *import* these, but we cannot assume the user has
19
+ * opted in to the flag.
20
+ */
21
+ type GetLoadContextFunction = GetLoadContextFunction_V7 | GetLoadContextFunction_V8;
22
+ type GetLoadContextFunction_V7 = (request: Request, context: Context) => Promise<AppLoadContext> | AppLoadContext;
23
+ type GetLoadContextFunction_V8 = (request: Request, context: Context) => Promise<RouterContextProvider> | RouterContextProvider;
24
+ type RequestHandler = (request: Request, context: Context) => Promise<Response>;
25
+ /**
26
+ * An instance of `ReactContextProvider` providing access to
27
+ * [Netlify request context]{@link https://docs.netlify.com/build/functions/api/#netlify-specific-context-object}
28
+ *
29
+ * @example context.get(netlifyRouterContext).geo?.country?.name
12
30
  */
13
- type GetLoadContextFunction = (request: Request, context: Context) => Promise<LoadContext> | LoadContext;
14
- type RequestHandler = (request: Request, context: LoadContext) => Promise<Response | void>;
31
+ declare const netlifyRouterContext: react_router.RouterContext<Partial<Context>>;
15
32
  /**
16
33
  * Given a build and a callback to get the base loader context, this returns
17
34
  * a Netlify Function handler (https://docs.netlify.com/functions/overview/) which renders the
@@ -26,4 +43,4 @@ declare function createRequestHandler({ build, mode, getLoadContext, }: {
26
43
 
27
44
  declare function netlifyPlugin(): Plugin;
28
45
 
29
- export { type GetLoadContextFunction, type RequestHandler, createRequestHandler, netlifyPlugin as default };
46
+ export { type GetLoadContextFunction, type RequestHandler, createRequestHandler, netlifyPlugin as default, netlifyRouterContext };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,34 @@
1
- import { AppLoadContext, ServerBuild } from 'react-router';
1
+ import * as react_router from 'react-router';
2
+ import { AppLoadContext, RouterContextProvider, ServerBuild } from 'react-router';
2
3
  import { Context } from '@netlify/functions';
3
4
  import { Plugin } from 'vite';
4
5
 
5
- type LoadContext = AppLoadContext & Context;
6
+ declare module 'react-router' {
7
+ interface AppLoadContext extends Context {
8
+ }
9
+ }
6
10
  /**
7
- * A function that returns the value to use as `context` in route `loader` and
8
- * `action` functions.
11
+ * A function that returns the value to use as `context` in route `loader` and `action` functions.
9
12
  *
10
- * You can think of this as an escape hatch that allows you to pass
11
- * environment/platform-specific values through to your loader/action.
13
+ * You can think of this as an escape hatch that allows you to pass environment/platform-specific
14
+ * values through to your loader/action.
15
+ *
16
+ * NOTE: v7.9.0 introduced a breaking change when the user opts in to `future.v8_middleware`. This
17
+ * requires returning an instance of `RouterContextProvider` instead of a plain object. We have a
18
+ * peer dependency on >=7.9.0 so we can safely *import* these, but we cannot assume the user has
19
+ * opted in to the flag.
20
+ */
21
+ type GetLoadContextFunction = GetLoadContextFunction_V7 | GetLoadContextFunction_V8;
22
+ type GetLoadContextFunction_V7 = (request: Request, context: Context) => Promise<AppLoadContext> | AppLoadContext;
23
+ type GetLoadContextFunction_V8 = (request: Request, context: Context) => Promise<RouterContextProvider> | RouterContextProvider;
24
+ type RequestHandler = (request: Request, context: Context) => Promise<Response>;
25
+ /**
26
+ * An instance of `ReactContextProvider` providing access to
27
+ * [Netlify request context]{@link https://docs.netlify.com/build/functions/api/#netlify-specific-context-object}
28
+ *
29
+ * @example context.get(netlifyRouterContext).geo?.country?.name
12
30
  */
13
- type GetLoadContextFunction = (request: Request, context: Context) => Promise<LoadContext> | LoadContext;
14
- type RequestHandler = (request: Request, context: LoadContext) => Promise<Response | void>;
31
+ declare const netlifyRouterContext: react_router.RouterContext<Partial<Context>>;
15
32
  /**
16
33
  * Given a build and a callback to get the base loader context, this returns
17
34
  * a Netlify Function handler (https://docs.netlify.com/functions/overview/) which renders the
@@ -26,4 +43,4 @@ declare function createRequestHandler({ build, mode, getLoadContext, }: {
26
43
 
27
44
  declare function netlifyPlugin(): Plugin;
28
45
 
29
- export { type GetLoadContextFunction, type RequestHandler, createRequestHandler, netlifyPlugin as default };
46
+ export { type GetLoadContextFunction, type RequestHandler, createRequestHandler, netlifyPlugin as default, netlifyRouterContext };
package/dist/index.js CHANGED
@@ -21,12 +21,44 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  createRequestHandler: () => createRequestHandler,
24
- default: () => netlifyPlugin
24
+ default: () => netlifyPlugin,
25
+ netlifyRouterContext: () => netlifyRouterContext
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
27
28
 
28
29
  // src/server.ts
29
30
  var import_react_router = require("react-router");
31
+ var netlifyRouterContext = (
32
+ // We must use a singleton because Remix contexts rely on referential equality.
33
+ // We can't hook into the request lifecycle in dev mode, so we use a Proxy to always read from the
34
+ // current `Netlify.context` value, which is always contextual to the in-flight request.
35
+ (0, import_react_router.createContext)(
36
+ new Proxy(
37
+ // Can't reference `Netlify.context` here because it isn't set outside of a request lifecycle
38
+ {},
39
+ {
40
+ get(_target, prop, receiver) {
41
+ return Reflect.get(Netlify.context ?? {}, prop, receiver);
42
+ },
43
+ set(_target, prop, value, receiver) {
44
+ return Reflect.set(Netlify.context ?? {}, prop, value, receiver);
45
+ },
46
+ has(_target, prop) {
47
+ return Reflect.has(Netlify.context ?? {}, prop);
48
+ },
49
+ deleteProperty(_target, prop) {
50
+ return Reflect.deleteProperty(Netlify.context ?? {}, prop);
51
+ },
52
+ ownKeys(_target) {
53
+ return Reflect.ownKeys(Netlify.context ?? {});
54
+ },
55
+ getOwnPropertyDescriptor(_target, prop) {
56
+ return Reflect.getOwnPropertyDescriptor(Netlify.context ?? {}, prop);
57
+ }
58
+ }
59
+ )
60
+ )
61
+ );
30
62
  function createRequestHandler({
31
63
  build,
32
64
  mode,
@@ -37,8 +69,14 @@ function createRequestHandler({
37
69
  const start = Date.now();
38
70
  console.log(`[${request.method}] ${request.url}`);
39
71
  try {
40
- const mergedLoadContext = await getLoadContext?.(request, netlifyContext) || netlifyContext;
41
- const response = await reactRouterHandler(request, mergedLoadContext);
72
+ const getDefaultReactRouterContext = () => {
73
+ const ctx = new import_react_router.RouterContextProvider();
74
+ ctx.set(netlifyRouterContext, netlifyContext);
75
+ Object.assign(ctx, netlifyContext);
76
+ return ctx;
77
+ };
78
+ const reactRouterContext = await getLoadContext?.(request, netlifyContext) ?? getDefaultReactRouterContext();
79
+ const response = await reactRouterHandler(request, reactRouterContext);
42
80
  response.headers.set("x-nf-runtime", "Node");
43
81
  console.log(`[${response.status}] ${request.url} (${Date.now() - start}ms)`);
44
82
  return response;
@@ -56,7 +94,7 @@ var import_posix = require("path/posix");
56
94
 
57
95
  // package.json
58
96
  var name = "@netlify/vite-plugin-react-router";
59
- var version = "1.0.1";
97
+ var version = "2.0.1";
60
98
 
61
99
  // src/plugin.ts
62
100
  var NETLIFY_FUNCTIONS_DIR = ".netlify/v1/functions";
@@ -72,7 +110,6 @@ import { createRequestHandler } from "@netlify/vite-plugin-react-router";
72
110
  import * as build from "virtual:react-router/server-build";
73
111
  export default createRequestHandler({
74
112
  build,
75
- getLoadContext: async (_req, ctx) => ctx,
76
113
  });
77
114
  `
78
115
  );
@@ -142,5 +179,6 @@ function netlifyPlugin() {
142
179
  }
143
180
  // Annotate the CommonJS export names for ESM import in node:
144
181
  0 && (module.exports = {
145
- createRequestHandler
182
+ createRequestHandler,
183
+ netlifyRouterContext
146
184
  });
package/dist/index.mjs CHANGED
@@ -1,5 +1,40 @@
1
1
  // src/server.ts
2
- import { createRequestHandler as createReactRouterRequestHandler } from "react-router";
2
+ import {
3
+ createContext,
4
+ RouterContextProvider,
5
+ createRequestHandler as createReactRouterRequestHandler
6
+ } from "react-router";
7
+ var netlifyRouterContext = (
8
+ // We must use a singleton because Remix contexts rely on referential equality.
9
+ // We can't hook into the request lifecycle in dev mode, so we use a Proxy to always read from the
10
+ // current `Netlify.context` value, which is always contextual to the in-flight request.
11
+ createContext(
12
+ new Proxy(
13
+ // Can't reference `Netlify.context` here because it isn't set outside of a request lifecycle
14
+ {},
15
+ {
16
+ get(_target, prop, receiver) {
17
+ return Reflect.get(Netlify.context ?? {}, prop, receiver);
18
+ },
19
+ set(_target, prop, value, receiver) {
20
+ return Reflect.set(Netlify.context ?? {}, prop, value, receiver);
21
+ },
22
+ has(_target, prop) {
23
+ return Reflect.has(Netlify.context ?? {}, prop);
24
+ },
25
+ deleteProperty(_target, prop) {
26
+ return Reflect.deleteProperty(Netlify.context ?? {}, prop);
27
+ },
28
+ ownKeys(_target) {
29
+ return Reflect.ownKeys(Netlify.context ?? {});
30
+ },
31
+ getOwnPropertyDescriptor(_target, prop) {
32
+ return Reflect.getOwnPropertyDescriptor(Netlify.context ?? {}, prop);
33
+ }
34
+ }
35
+ )
36
+ )
37
+ );
3
38
  function createRequestHandler({
4
39
  build,
5
40
  mode,
@@ -10,8 +45,14 @@ function createRequestHandler({
10
45
  const start = Date.now();
11
46
  console.log(`[${request.method}] ${request.url}`);
12
47
  try {
13
- const mergedLoadContext = await getLoadContext?.(request, netlifyContext) || netlifyContext;
14
- const response = await reactRouterHandler(request, mergedLoadContext);
48
+ const getDefaultReactRouterContext = () => {
49
+ const ctx = new RouterContextProvider();
50
+ ctx.set(netlifyRouterContext, netlifyContext);
51
+ Object.assign(ctx, netlifyContext);
52
+ return ctx;
53
+ };
54
+ const reactRouterContext = await getLoadContext?.(request, netlifyContext) ?? getDefaultReactRouterContext();
55
+ const response = await reactRouterHandler(request, reactRouterContext);
15
56
  response.headers.set("x-nf-runtime", "Node");
16
57
  console.log(`[${response.status}] ${request.url} (${Date.now() - start}ms)`);
17
58
  return response;
@@ -29,7 +70,7 @@ import { sep as posixSep } from "node:path/posix";
29
70
 
30
71
  // package.json
31
72
  var name = "@netlify/vite-plugin-react-router";
32
- var version = "1.0.1";
73
+ var version = "2.0.1";
33
74
 
34
75
  // src/plugin.ts
35
76
  var NETLIFY_FUNCTIONS_DIR = ".netlify/v1/functions";
@@ -45,7 +86,6 @@ import { createRequestHandler } from "@netlify/vite-plugin-react-router";
45
86
  import * as build from "virtual:react-router/server-build";
46
87
  export default createRequestHandler({
47
88
  build,
48
- getLoadContext: async (_req, ctx) => ctx,
49
89
  });
50
90
  `
51
91
  );
@@ -115,5 +155,6 @@ function netlifyPlugin() {
115
155
  }
116
156
  export {
117
157
  createRequestHandler,
118
- netlifyPlugin as default
158
+ netlifyPlugin as default,
159
+ netlifyRouterContext
119
160
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/vite-plugin-react-router",
3
- "version": "1.0.1",
3
+ "version": "2.0.1",
4
4
  "description": "React Router 7+ Vite plugin for Netlify",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",
@@ -40,20 +40,20 @@
40
40
  },
41
41
  "homepage": "https://github.com/netlify/remix-compute#readme",
42
42
  "dependencies": {
43
- "@react-router/node": "^7.0.1",
44
- "isbot": "^5.0.0",
45
- "react-router": "^7.0.1"
43
+ "isbot": "^5.0.0"
46
44
  },
47
45
  "devDependencies": {
48
- "@netlify/functions": "^3.0.0",
46
+ "@netlify/functions": "^3.1.9",
49
47
  "@types/react": "^18.0.27",
50
48
  "@types/react-dom": "^18.0.10",
51
49
  "react": "^18.2.0",
52
50
  "react-dom": "^18.2.0",
51
+ "react-router": "^7.9.4",
53
52
  "tsup": "^8.0.2",
54
53
  "vite": "^6.2.5"
55
54
  },
56
55
  "peerDependencies": {
56
+ "react-router": ">=7.9.0",
57
57
  "vite": ">=5.0.0"
58
58
  },
59
59
  "engines": {