@netlify/vite-plugin-react-router 2.0.0 → 2.1.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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.0](https://github.com/netlify/remix-compute/compare/vite-plugin-react-router-v2.0.1...vite-plugin-react-router-v2.1.0) (2025-11-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **@netlify/vite-plugin-react-router:** add edge support ([#562](https://github.com/netlify/remix-compute/issues/562)) ([bbcb41e](https://github.com/netlify/remix-compute/commit/bbcb41e9d6ffdb5483759fbb94fb5258092adf87))
9
+
10
+ ## [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)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * 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))
16
+
3
17
  ## [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)
4
18
 
5
19
  ### ⚠ BREAKING CHANGES
package/README.md CHANGED
@@ -1,14 +1,21 @@
1
1
  # React Router Adapter for Netlify
2
2
 
3
- The React Router Adapter for Netlify allows you to deploy your [React Router](https://reactrouter.com) app to
4
- [Netlify Functions](https://docs.netlify.com/functions/overview/).
3
+ The React Router Adapter for Netlify allows you to deploy your [React Router](https://reactrouter.com) app to Netlify.
4
+
5
+ ## How to use
5
6
 
6
7
  To deploy a React Router 7+ site to Netlify, install this package:
7
8
 
8
- ## How to use
9
+ ```sh
10
+ npm install @netlify/vite-plugin-react-router
11
+ ```
12
+
13
+ It's also recommended (but not required) to use the
14
+ [Netlify Vite plugin](https://www.npmjs.com/package/@netlify/vite-plugin), which provides full Netlify platform
15
+ emulation directly in your local dev server:
9
16
 
10
17
  ```sh
11
- npm install --save-dev @netlify/vite-plugin-react-router
18
+ npm install --save-dev @netlify/vite-plugin
12
19
  ```
13
20
 
14
21
  and include the Netlify plugin in your `vite.config.ts`:
@@ -17,17 +24,128 @@ and include the Netlify plugin in your `vite.config.ts`:
17
24
  import { reactRouter } from '@react-router/dev/vite'
18
25
  import { defineConfig } from 'vite'
19
26
  import tsconfigPaths from 'vite-tsconfig-paths'
20
- import netlifyPlugin from '@netlify/vite-plugin-react-router' // <- add this
27
+ import netlifyReactRouter from '@netlify/vite-plugin-react-router' // <- add this
28
+ import netlify from '@netlify/vite-plugin' // <- add this (optional)
29
+
30
+ export default defineConfig({
31
+ plugins: [
32
+ reactRouter(),
33
+ tsconfigPaths(),
34
+ netlifyReactRouter(), // <- add this
35
+ netlify(), // <- add this (optional)
36
+ ],
37
+ })
38
+ ```
39
+
40
+ Your app is ready to [deploy to Netlify](https://docs.netlify.com/deploy/create-deploys/).
41
+
42
+ ### Deploying to Edge Functions
43
+
44
+ By default, this plugin deploys your React Router app to
45
+ [Netlify Functions](https://docs.netlify.com/functions/overview/) (Node.js runtime). You can optionally deploy to
46
+ [Netlify Edge Functions](https://docs.netlify.com/edge-functions/overview/) (Deno runtime) instead.
47
+
48
+ First, toggle the `edge` option:
49
+
50
+ ```typescript
51
+ export default defineConfig({
52
+ plugins: [
53
+ reactRouter(),
54
+ tsconfigPaths(),
55
+ netlifyReactRouter({ edge: true }), // <- deploy to Edge Functions
56
+ netlify(),
57
+ ],
58
+ })
59
+ ```
60
+
61
+ Second, you **must** provide an `app/entry.server.tsx` (or `.jsx`) file that uses web-standard APIs compatible with the
62
+ Deno runtime. Create a file with the following content:
63
+
64
+ > [!IMPORTANT]
65
+ >
66
+ > This file uses `renderToReadableStream` (Web Streams API) instead of `renderToPipeableStream` (Node.js API), which is
67
+ > required for the Deno runtime. You may customize your server entry file, but see below for important edge runtime
68
+ > constraints.
69
+
70
+ ```tsx
71
+ import type { AppLoadContext, EntryContext } from 'react-router'
72
+ import { ServerRouter } from 'react-router'
73
+ import { isbot } from 'isbot'
74
+ import { renderToReadableStream } from 'react-dom/server'
75
+
76
+ export default async function handleRequest(
77
+ request: Request,
78
+ responseStatusCode: number,
79
+ responseHeaders: Headers,
80
+ routerContext: EntryContext,
81
+ _loadContext: AppLoadContext,
82
+ ) {
83
+ let shellRendered = false
84
+ const userAgent = request.headers.get('user-agent')
85
+
86
+ const body = await renderToReadableStream(<ServerRouter context={routerContext} url={request.url} />, {
87
+ onError(error: unknown) {
88
+ responseStatusCode = 500
89
+ // Log streaming rendering errors from inside the shell. Don't log
90
+ // errors encountered during initial shell rendering since they'll
91
+ // reject and get logged in handleDocumentRequest.
92
+ if (shellRendered) {
93
+ console.error(error)
94
+ }
95
+ },
96
+ })
97
+ shellRendered = true
98
+
99
+ // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
100
+ // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
101
+ if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
102
+ await body.allReady
103
+ }
104
+
105
+ responseHeaders.set('Content-Type', 'text/html')
106
+ return new Response(body, {
107
+ headers: responseHeaders,
108
+ status: responseStatusCode,
109
+ })
110
+ }
111
+ ```
21
112
 
113
+ You may need to `npm install isbot` if you do not have this dependency.
114
+
115
+ Finally, if you have your own Netlify Functions (typically in `netlify/functions`) for which you've configured a `path`,
116
+ you must exclude those paths to avoid conflicts with the generated React Router SSR handler:
117
+
118
+ ```typescript
22
119
  export default defineConfig({
23
120
  plugins: [
24
121
  reactRouter(),
25
122
  tsconfigPaths(),
26
- netlifyPlugin(), // <- add this
123
+ netlifyReactRouter({
124
+ edge: true,
125
+ excludedPaths: ['/ping', '/api/*', '/webhooks/*'],
126
+ }),
127
+ netlify(),
27
128
  ],
28
129
  })
29
130
  ```
30
131
 
132
+ #### Moving back from Edge Functions to Functions
133
+
134
+ To switch from Edge Functions back to Functions, you must:
135
+
136
+ 1. Remove the `edge: true` option from your `vite.config.ts`
137
+ 2. **Delete the `app/entry.server.tsx` file** (React Router will use its default Node.js-compatible entry)
138
+
139
+ #### Edge runtime
140
+
141
+ Before deploying to Edge Functions, review the Netlify Edge Functions documentation for important details:
142
+
143
+ - [Runtime environment](https://docs.netlify.com/build/edge-functions/api/#runtime-environment) - Understand the Deno
144
+ runtime
145
+ - [Supported Web APIs](https://docs.netlify.com/build/edge-functions/api/#supported-web-apis) - Check which APIs are
146
+ available
147
+ - [Limitations](https://docs.netlify.com/build/edge-functions/limits/) - Be aware of resource limits and constraints
148
+
31
149
  ### Load context
32
150
 
33
151
  This plugin automatically includes all
@@ -61,7 +179,8 @@ type-safe `RouterContextProvider`. Note that this requires requires v2.0.0+ of `
61
179
  For example:
62
180
 
63
181
  ```tsx
64
- import { netlifyRouterContext } from '@netlify/vite-plugin-react-router'
182
+ import { netlifyRouterContext } from '@netlify/vite-plugin-react-router/serverless'
183
+ // NOTE: if setting `edge: true`, import from /edge ^ instead here
65
184
  import { useLoaderData } from 'react-router'
66
185
  import type { Route } from './+types/example'
67
186
 
@@ -76,6 +195,12 @@ export default function Example() {
76
195
  }
77
196
  ```
78
197
 
198
+ > [!IMPORTANT]
199
+ >
200
+ > Note that in local development, `netlifyRouterContext` requires Netlify platform emulation, which is provided
201
+ > seamlessly by [`@netlify/vite-plugin`](https://www.npmjs.com/package/@netlify/vite-plugin) (or Netlify CLI - up to
202
+ > you).
203
+
79
204
  ### Middleware context
80
205
 
81
206
  React Router introduced a stable middleware feature in 7.9.0.
@@ -85,10 +210,11 @@ To use middleware,
85
210
  that this requires requires v2.0.0+ of `@netlify/vite-plugin-react-router`.
86
211
 
87
212
  To access the [Netlify context](https://docs.netlify.com/build/functions/api/#netlify-specific-context-object)
88
- specifically, you must import our `RouterContextProvider` instance:
213
+ specifically, you must import our `RouterContext` instance:
89
214
 
90
215
  ```tsx
91
- import { netlifyRouterContext } from '@netlify/vite-plugin-react-router'
216
+ import { netlifyRouterContext } from '@netlify/vite-plugin-react-router/serverless'
217
+ // NOTE: if setting `edge: true`, import from /edge ^ instead here
92
218
 
93
219
  import type { Route } from './+types/home'
94
220
 
@@ -103,3 +229,9 @@ export default function Home() {
103
229
  return <h1>Hello world</h1>
104
230
  }
105
231
  ```
232
+
233
+ > [!IMPORTANT]
234
+ >
235
+ > Note that in local development, `netlifyRouterContext` requires Netlify platform emulation, which is provided
236
+ > seamlessly by [`@netlify/vite-plugin`](https://www.npmjs.com/package/@netlify/vite-plugin) (or Netlify CLI - up to
237
+ > you).
@@ -0,0 +1,25 @@
1
+ import {
2
+ createNetlifyRequestHandler,
3
+ createNetlifyRouterContext
4
+ } from "./chunk-J5PMA2AP.mjs";
5
+
6
+ // src/runtimes/netlify-functions.ts
7
+ var netlifyRouterContext = createNetlifyRouterContext();
8
+ function createRequestHandler({
9
+ build,
10
+ mode,
11
+ getLoadContext
12
+ }) {
13
+ return createNetlifyRequestHandler({
14
+ build,
15
+ mode,
16
+ getLoadContext,
17
+ netlifyRouterContext,
18
+ runtimeName: "Node"
19
+ });
20
+ }
21
+
22
+ export {
23
+ netlifyRouterContext,
24
+ createRequestHandler
25
+ };
@@ -0,0 +1,70 @@
1
+ // src/lib/handler.ts
2
+ import {
3
+ RouterContextProvider,
4
+ createRequestHandler as createReactRouterRequestHandler
5
+ } from "react-router";
6
+ function createNetlifyRequestHandler({
7
+ build,
8
+ mode,
9
+ getLoadContext,
10
+ netlifyRouterContext,
11
+ runtimeName
12
+ }) {
13
+ const reactRouterHandler = createReactRouterRequestHandler(build, mode);
14
+ return async (request, netlifyContext) => {
15
+ const start = Date.now();
16
+ console.log(`[${request.method}] ${request.url}`);
17
+ try {
18
+ const getDefaultReactRouterContext = () => {
19
+ const ctx = new RouterContextProvider();
20
+ ctx.set(netlifyRouterContext, netlifyContext);
21
+ Object.assign(ctx, netlifyContext);
22
+ return ctx;
23
+ };
24
+ const reactRouterContext = await getLoadContext?.(request, netlifyContext) ?? getDefaultReactRouterContext();
25
+ const response = await reactRouterHandler(request, reactRouterContext);
26
+ response.headers.set("x-nf-runtime", runtimeName);
27
+ console.log(`[${response.status}] ${request.url} (${Date.now() - start}ms)`);
28
+ return response;
29
+ } catch (error) {
30
+ console.error(error);
31
+ return new Response("Internal Error", { status: 500 });
32
+ }
33
+ };
34
+ }
35
+
36
+ // src/lib/context.ts
37
+ import { createContext } from "react-router";
38
+ function createNetlifyRouterContext() {
39
+ return createContext(
40
+ new Proxy(
41
+ // Can't reference `Netlify.context` here because it isn't set outside of a request lifecycle
42
+ {},
43
+ {
44
+ get(_target, prop, receiver) {
45
+ return Reflect.get(Netlify.context ?? {}, prop, receiver);
46
+ },
47
+ set(_target, prop, value, receiver) {
48
+ return Reflect.set(Netlify.context ?? {}, prop, value, receiver);
49
+ },
50
+ has(_target, prop) {
51
+ return Reflect.has(Netlify.context ?? {}, prop);
52
+ },
53
+ deleteProperty(_target, prop) {
54
+ return Reflect.deleteProperty(Netlify.context ?? {}, prop);
55
+ },
56
+ ownKeys(_target) {
57
+ return Reflect.ownKeys(Netlify.context ?? {});
58
+ },
59
+ getOwnPropertyDescriptor(_target, prop) {
60
+ return Reflect.getOwnPropertyDescriptor(Netlify.context ?? {}, prop);
61
+ }
62
+ }
63
+ )
64
+ );
65
+ }
66
+
67
+ export {
68
+ createNetlifyRequestHandler,
69
+ createNetlifyRouterContext
70
+ };
@@ -0,0 +1,29 @@
1
+ import * as react_router from 'react-router';
2
+ import { ServerBuild } from 'react-router';
3
+ import { Context } from '@netlify/edge-functions';
4
+ import { G as GetLoadContextFunction, R as RequestHandler } from './handler-4e_lz5jw.mjs';
5
+
6
+ declare module 'react-router' {
7
+ interface AppLoadContext extends Context {
8
+ }
9
+ }
10
+ /**
11
+ * An instance of `RouterContext` providing access to
12
+ * [Netlify request context]{@link https://docs.netlify.com/build/edge-functions/api/#netlify-specific-context-object}
13
+ *
14
+ * @example context.get(netlifyRouterContext).geo?.country?.name
15
+ */
16
+ declare const netlifyRouterContext: react_router.RouterContext<Partial<Context>>;
17
+ /**
18
+ * Given a build and a callback to get the base loader context, this returns
19
+ * a Netlify Edge Function handler (https://docs.netlify.com/edge-functions/overview/) which renders the
20
+ * requested path. The loader context in this lifecycle will contain the Netlify Edge Functions context
21
+ * fields merged in.
22
+ */
23
+ declare function createRequestHandler({ build, mode, getLoadContext, }: {
24
+ build: ServerBuild;
25
+ mode?: string;
26
+ getLoadContext?: GetLoadContextFunction<Context>;
27
+ }): RequestHandler<Context>;
28
+
29
+ export { createRequestHandler, netlifyRouterContext };
package/dist/edge.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import {
2
+ createNetlifyRequestHandler,
3
+ createNetlifyRouterContext
4
+ } from "./chunk-J5PMA2AP.mjs";
5
+
6
+ // src/runtimes/netlify-edge-functions.ts
7
+ var netlifyRouterContext = createNetlifyRouterContext();
8
+ function createRequestHandler({
9
+ build,
10
+ mode,
11
+ getLoadContext
12
+ }) {
13
+ return createNetlifyRequestHandler({
14
+ build,
15
+ mode,
16
+ getLoadContext,
17
+ netlifyRouterContext,
18
+ runtimeName: "edge"
19
+ });
20
+ }
21
+ export {
22
+ createRequestHandler,
23
+ netlifyRouterContext
24
+ };
@@ -0,0 +1,18 @@
1
+ import { RouterContextProvider, AppLoadContext } from 'react-router';
2
+
3
+ /**
4
+ * A function that returns the value to use as `context` in route `loader` and `action` functions.
5
+ *
6
+ * You can think of this as an escape hatch that allows you to pass environment/platform-specific
7
+ * values through to your loader/action.
8
+ *
9
+ * NOTE: v7.9.0 introduced a breaking change when the user opts in to `future.v8_middleware`. This
10
+ * requires returning an instance of `RouterContextProvider` instead of a plain object. We have a
11
+ * peer dependency on >=7.9.0 so we can safely *import* these, but we cannot assume the user has
12
+ * opted in to the flag.
13
+ */
14
+ type GetLoadContextFunction<TNetlifyContext> = ((request: Request, context: TNetlifyContext) => Promise<RouterContextProvider> | RouterContextProvider) | ((request: Request, context: TNetlifyContext) => Promise<AppLoadContext> | AppLoadContext);
15
+
16
+ type RequestHandler<TNetlifyContext> = (request: Request, context: TNetlifyContext) => Promise<Response>;
17
+
18
+ export type { GetLoadContextFunction as G, RequestHandler as R };
package/dist/index.d.mts CHANGED
@@ -1,46 +1,26 @@
1
- import * as react_router from 'react-router';
2
- import { AppLoadContext, RouterContextProvider, ServerBuild } from 'react-router';
3
- import { Context } from '@netlify/functions';
1
+ export { GetLoadContextFunction, RequestHandler, createRequestHandler, netlifyRouterContext } from './serverless.mjs';
4
2
  import { Plugin } from 'vite';
3
+ import 'react-router';
4
+ import '@netlify/functions';
5
+ import './handler-4e_lz5jw.mjs';
5
6
 
6
- declare module 'react-router' {
7
- interface AppLoadContext extends Context {
8
- }
7
+ interface NetlifyPluginOptions {
8
+ /**
9
+ * Deploy to Netlify Edge Functions instead of Netlify Functions.
10
+ * @default false
11
+ */
12
+ edge?: boolean;
13
+ /**
14
+ * Paths to exclude from being handled by the React Router handler.
15
+ *
16
+ * @IMPORTANT If you have opted in to edge rendering with `edge: true` and you have your own Netlify
17
+ * Functions running on custom `path`s, you must exclude those paths here to avoid conflicts.
18
+ *
19
+ * @type {URLPattern[]}
20
+ * @default []
21
+ */
22
+ excludedPaths?: string[];
9
23
  }
10
- /**
11
- * A function that returns the value to use as `context` in route `loader` and `action` functions.
12
- *
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
30
- */
31
- declare const netlifyRouterContext: react_router.RouterContext<Context>;
32
- /**
33
- * Given a build and a callback to get the base loader context, this returns
34
- * a Netlify Function handler (https://docs.netlify.com/functions/overview/) which renders the
35
- * requested path. The loader context in this lifecycle will contain the Netlify Functions context
36
- * fields merged in.
37
- */
38
- declare function createRequestHandler({ build, mode, getLoadContext, }: {
39
- build: ServerBuild;
40
- mode?: string;
41
- getLoadContext?: GetLoadContextFunction;
42
- }): RequestHandler;
24
+ declare function netlifyPlugin(options?: NetlifyPluginOptions): Plugin;
43
25
 
44
- declare function netlifyPlugin(): Plugin;
45
-
46
- export { type GetLoadContextFunction, type RequestHandler, createRequestHandler, netlifyPlugin as default, netlifyRouterContext };
26
+ export { netlifyPlugin as default };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,8 @@
1
1
  import * as react_router from 'react-router';
2
- import { AppLoadContext, RouterContextProvider, ServerBuild } from 'react-router';
2
+ import { RouterContextProvider, AppLoadContext, ServerBuild } from 'react-router';
3
3
  import { Context } from '@netlify/functions';
4
4
  import { Plugin } from 'vite';
5
5
 
6
- declare module 'react-router' {
7
- interface AppLoadContext extends Context {
8
- }
9
- }
10
6
  /**
11
7
  * A function that returns the value to use as `context` in route `loader` and `action` functions.
12
8
  *
@@ -18,17 +14,23 @@ declare module 'react-router' {
18
14
  * peer dependency on >=7.9.0 so we can safely *import* these, but we cannot assume the user has
19
15
  * opted in to the flag.
20
16
  */
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>;
17
+ type GetLoadContextFunction$1<TNetlifyContext> = ((request: Request, context: TNetlifyContext) => Promise<RouterContextProvider> | RouterContextProvider) | ((request: Request, context: TNetlifyContext) => Promise<AppLoadContext> | AppLoadContext);
18
+
19
+ type RequestHandler$1<TNetlifyContext> = (request: Request, context: TNetlifyContext) => Promise<Response>;
20
+
21
+ declare module 'react-router' {
22
+ interface AppLoadContext extends Context {
23
+ }
24
+ }
25
+ type GetLoadContextFunction = GetLoadContextFunction$1<Context>;
26
+ type RequestHandler = RequestHandler$1<Context>;
25
27
  /**
26
- * An instance of `ReactContextProvider` providing access to
28
+ * An instance of `RouterContext` providing access to
27
29
  * [Netlify request context]{@link https://docs.netlify.com/build/functions/api/#netlify-specific-context-object}
28
30
  *
29
31
  * @example context.get(netlifyRouterContext).geo?.country?.name
30
32
  */
31
- declare const netlifyRouterContext: react_router.RouterContext<Context>;
33
+ declare const netlifyRouterContext: react_router.RouterContext<Partial<Context>>;
32
34
  /**
33
35
  * Given a build and a callback to get the base loader context, this returns
34
36
  * a Netlify Function handler (https://docs.netlify.com/functions/overview/) which renders the
@@ -41,6 +43,23 @@ declare function createRequestHandler({ build, mode, getLoadContext, }: {
41
43
  getLoadContext?: GetLoadContextFunction;
42
44
  }): RequestHandler;
43
45
 
44
- declare function netlifyPlugin(): Plugin;
46
+ interface NetlifyPluginOptions {
47
+ /**
48
+ * Deploy to Netlify Edge Functions instead of Netlify Functions.
49
+ * @default false
50
+ */
51
+ edge?: boolean;
52
+ /**
53
+ * Paths to exclude from being handled by the React Router handler.
54
+ *
55
+ * @IMPORTANT If you have opted in to edge rendering with `edge: true` and you have your own Netlify
56
+ * Functions running on custom `path`s, you must exclude those paths here to avoid conflicts.
57
+ *
58
+ * @type {URLPattern[]}
59
+ * @default []
60
+ */
61
+ excludedPaths?: string[];
62
+ }
63
+ declare function netlifyPlugin(options?: NetlifyPluginOptions): Plugin;
45
64
 
46
65
  export { type GetLoadContextFunction, type RequestHandler, createRequestHandler, netlifyPlugin as default, netlifyRouterContext };
package/dist/index.js CHANGED
@@ -26,13 +26,14 @@ __export(index_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
- // src/server.ts
29
+ // src/lib/handler.ts
30
30
  var import_react_router = require("react-router");
31
- var netlifyRouterContext = (0, import_react_router.createContext)();
32
- function createRequestHandler({
31
+ function createNetlifyRequestHandler({
33
32
  build,
34
33
  mode,
35
- getLoadContext
34
+ getLoadContext,
35
+ netlifyRouterContext: netlifyRouterContext2,
36
+ runtimeName
36
37
  }) {
37
38
  const reactRouterHandler = (0, import_react_router.createRequestHandler)(build, mode);
38
39
  return async (request, netlifyContext) => {
@@ -41,13 +42,13 @@ function createRequestHandler({
41
42
  try {
42
43
  const getDefaultReactRouterContext = () => {
43
44
  const ctx = new import_react_router.RouterContextProvider();
44
- ctx.set(netlifyRouterContext, netlifyContext);
45
+ ctx.set(netlifyRouterContext2, netlifyContext);
45
46
  Object.assign(ctx, netlifyContext);
46
47
  return ctx;
47
48
  };
48
49
  const reactRouterContext = await getLoadContext?.(request, netlifyContext) ?? getDefaultReactRouterContext();
49
50
  const response = await reactRouterHandler(request, reactRouterContext);
50
- response.headers.set("x-nf-runtime", "Node");
51
+ response.headers.set("x-nf-runtime", runtimeName);
51
52
  console.log(`[${response.status}] ${request.url} (${Date.now() - start}ms)`);
52
53
  return response;
53
54
  } catch (error) {
@@ -57,6 +58,53 @@ function createRequestHandler({
57
58
  };
58
59
  }
59
60
 
61
+ // src/lib/context.ts
62
+ var import_react_router2 = require("react-router");
63
+ function createNetlifyRouterContext() {
64
+ return (0, import_react_router2.createContext)(
65
+ new Proxy(
66
+ // Can't reference `Netlify.context` here because it isn't set outside of a request lifecycle
67
+ {},
68
+ {
69
+ get(_target, prop, receiver) {
70
+ return Reflect.get(Netlify.context ?? {}, prop, receiver);
71
+ },
72
+ set(_target, prop, value, receiver) {
73
+ return Reflect.set(Netlify.context ?? {}, prop, value, receiver);
74
+ },
75
+ has(_target, prop) {
76
+ return Reflect.has(Netlify.context ?? {}, prop);
77
+ },
78
+ deleteProperty(_target, prop) {
79
+ return Reflect.deleteProperty(Netlify.context ?? {}, prop);
80
+ },
81
+ ownKeys(_target) {
82
+ return Reflect.ownKeys(Netlify.context ?? {});
83
+ },
84
+ getOwnPropertyDescriptor(_target, prop) {
85
+ return Reflect.getOwnPropertyDescriptor(Netlify.context ?? {}, prop);
86
+ }
87
+ }
88
+ )
89
+ );
90
+ }
91
+
92
+ // src/runtimes/netlify-functions.ts
93
+ var netlifyRouterContext = createNetlifyRouterContext();
94
+ function createRequestHandler({
95
+ build,
96
+ mode,
97
+ getLoadContext
98
+ }) {
99
+ return createNetlifyRequestHandler({
100
+ build,
101
+ mode,
102
+ getLoadContext,
103
+ netlifyRouterContext,
104
+ runtimeName: "Node"
105
+ });
106
+ }
107
+
60
108
  // src/plugin.ts
61
109
  var import_promises = require("fs/promises");
62
110
  var import_node_path = require("path");
@@ -64,10 +112,11 @@ var import_posix = require("path/posix");
64
112
 
65
113
  // package.json
66
114
  var name = "@netlify/vite-plugin-react-router";
67
- var version = "2.0.0";
115
+ var version = "2.1.0";
68
116
 
69
117
  // src/plugin.ts
70
118
  var NETLIFY_FUNCTIONS_DIR = ".netlify/v1/functions";
119
+ var NETLIFY_EDGE_FUNCTIONS_DIR = ".netlify/v1/edge-functions";
71
120
  var FUNCTION_FILENAME = "react-router-server.mjs";
72
121
  var FUNCTION_HANDLER_CHUNK = "server";
73
122
  var FUNCTION_HANDLER_MODULE_ID = "virtual:netlify-server";
@@ -76,14 +125,24 @@ var toPosixPath = (path) => path.split(import_node_path.sep).join(import_posix.s
76
125
  var FUNCTION_HANDLER = (
77
126
  /* js */
78
127
  `
79
- import { createRequestHandler } from "@netlify/vite-plugin-react-router";
128
+ import { createRequestHandler } from "@netlify/vite-plugin-react-router/serverless";
80
129
  import * as build from "virtual:react-router/server-build";
81
130
  export default createRequestHandler({
82
131
  build,
83
132
  });
84
133
  `
85
134
  );
86
- function generateNetlifyFunction(handlerPath) {
135
+ var EDGE_FUNCTION_HANDLER = (
136
+ /* js */
137
+ `
138
+ import { createRequestHandler } from "@netlify/vite-plugin-react-router/edge";
139
+ import * as build from "virtual:react-router/server-build";
140
+ export default createRequestHandler({
141
+ build,
142
+ });
143
+ `
144
+ );
145
+ function generateNetlifyFunction(handlerPath, excludedPath) {
87
146
  return (
88
147
  /* js */
89
148
  `
@@ -93,12 +152,31 @@ function generateNetlifyFunction(handlerPath) {
93
152
  name: "React Router server handler",
94
153
  generator: "${name}@${version}",
95
154
  path: "/*",
155
+ excludedPath: ${JSON.stringify(excludedPath)},
96
156
  preferStatic: true,
97
157
  };
98
158
  `
99
159
  );
100
160
  }
101
- function netlifyPlugin() {
161
+ function generateEdgeFunction(handlerPath, excludedPath) {
162
+ return (
163
+ /* js */
164
+ `
165
+ export { default } from "${handlerPath}";
166
+
167
+ export const config = {
168
+ name: "React Router server handler",
169
+ generator: "${name}@${version}",
170
+ cache: "manual",
171
+ path: "/*",
172
+ excludedPath: ${JSON.stringify(excludedPath)},
173
+ };
174
+ `
175
+ );
176
+ }
177
+ function netlifyPlugin(options = {}) {
178
+ const edge = options.edge ?? false;
179
+ const additionalExcludedPaths = options.excludedPaths ?? [];
102
180
  let resolvedConfig;
103
181
  let isProductionSsrBuild = false;
104
182
  return {
@@ -119,6 +197,19 @@ function netlifyPlugin() {
119
197
  config.build.rollupOptions.output = {};
120
198
  }
121
199
  config.build.rollupOptions.output.entryFileNames = "[name].js";
200
+ if (edge) {
201
+ config.ssr = {
202
+ ...config.ssr,
203
+ target: "webworker",
204
+ // Bundle everything except Node.js built-ins (which are supported but must use the `node:` prefix):
205
+ // https://docs.netlify.com/build/edge-functions/api/#runtime-environment
206
+ noExternal: /^(?!node:).*$/,
207
+ resolve: {
208
+ ...config.resolve,
209
+ conditions: ["worker", "deno", "browser"]
210
+ }
211
+ };
212
+ }
122
213
  }
123
214
  },
124
215
  async resolveId(source) {
@@ -129,7 +220,7 @@ function netlifyPlugin() {
129
220
  // See https://vitejs.dev/guide/api-plugin#virtual-modules-convention.
130
221
  load(id) {
131
222
  if (id === RESOLVED_FUNCTION_HANDLER_MODULE_ID) {
132
- return FUNCTION_HANDLER;
223
+ return edge ? EDGE_FUNCTION_HANDLER : FUNCTION_HANDLER;
133
224
  }
134
225
  },
135
226
  async configResolved(config) {
@@ -138,11 +229,32 @@ function netlifyPlugin() {
138
229
  // See https://rollupjs.org/plugin-development/#writebundle.
139
230
  async writeBundle() {
140
231
  if (isProductionSsrBuild) {
141
- const functionsDirectory = (0, import_node_path.join)(resolvedConfig.root, NETLIFY_FUNCTIONS_DIR);
142
- await (0, import_promises.mkdir)(functionsDirectory, { recursive: true });
143
232
  const handlerPath = (0, import_node_path.join)(resolvedConfig.build.outDir, `${FUNCTION_HANDLER_CHUNK}.js`);
144
- const relativeHandlerPath = toPosixPath((0, import_node_path.relative)(functionsDirectory, handlerPath));
145
- await (0, import_promises.writeFile)((0, import_node_path.join)(functionsDirectory, FUNCTION_FILENAME), generateNetlifyFunction(relativeHandlerPath));
233
+ if (edge) {
234
+ const clientDir = (0, import_node_path.join)(resolvedConfig.build.outDir, "..", "client");
235
+ const entries = await (0, import_promises.readdir)(clientDir, { withFileTypes: true });
236
+ const excludedPath = [
237
+ "/.netlify/*",
238
+ ...entries.map((entry) => entry.isDirectory() ? `/${entry.name}/*` : `/${entry.name}`),
239
+ ...additionalExcludedPaths
240
+ ];
241
+ const edgeFunctionsDir = (0, import_node_path.join)(resolvedConfig.root, NETLIFY_EDGE_FUNCTIONS_DIR);
242
+ await (0, import_promises.mkdir)(edgeFunctionsDir, { recursive: true });
243
+ const relativeHandlerPath = toPosixPath((0, import_node_path.relative)(edgeFunctionsDir, handlerPath));
244
+ await (0, import_promises.writeFile)(
245
+ (0, import_node_path.join)(edgeFunctionsDir, FUNCTION_FILENAME),
246
+ generateEdgeFunction(relativeHandlerPath, excludedPath)
247
+ );
248
+ } else {
249
+ const functionsDir = (0, import_node_path.join)(resolvedConfig.root, NETLIFY_FUNCTIONS_DIR);
250
+ await (0, import_promises.mkdir)(functionsDir, { recursive: true });
251
+ const relativeHandlerPath = toPosixPath((0, import_node_path.relative)(functionsDir, handlerPath));
252
+ const excludedPath = ["/.netlify/*", ...additionalExcludedPaths];
253
+ await (0, import_promises.writeFile)(
254
+ (0, import_node_path.join)(functionsDir, FUNCTION_FILENAME),
255
+ generateNetlifyFunction(relativeHandlerPath, excludedPath)
256
+ );
257
+ }
146
258
  }
147
259
  }
148
260
  };
package/dist/index.mjs CHANGED
@@ -1,49 +1,21 @@
1
- // src/server.ts
2
1
  import {
3
- createContext,
4
- RouterContextProvider,
5
- createRequestHandler as createReactRouterRequestHandler
6
- } from "react-router";
7
- var netlifyRouterContext = createContext();
8
- function createRequestHandler({
9
- build,
10
- mode,
11
- getLoadContext
12
- }) {
13
- const reactRouterHandler = createReactRouterRequestHandler(build, mode);
14
- return async (request, netlifyContext) => {
15
- const start = Date.now();
16
- console.log(`[${request.method}] ${request.url}`);
17
- try {
18
- const getDefaultReactRouterContext = () => {
19
- const ctx = new RouterContextProvider();
20
- ctx.set(netlifyRouterContext, netlifyContext);
21
- Object.assign(ctx, netlifyContext);
22
- return ctx;
23
- };
24
- const reactRouterContext = await getLoadContext?.(request, netlifyContext) ?? getDefaultReactRouterContext();
25
- const response = await reactRouterHandler(request, reactRouterContext);
26
- response.headers.set("x-nf-runtime", "Node");
27
- console.log(`[${response.status}] ${request.url} (${Date.now() - start}ms)`);
28
- return response;
29
- } catch (error) {
30
- console.error(error);
31
- return new Response("Internal Error", { status: 500 });
32
- }
33
- };
34
- }
2
+ createRequestHandler,
3
+ netlifyRouterContext
4
+ } from "./chunk-3EFABROO.mjs";
5
+ import "./chunk-J5PMA2AP.mjs";
35
6
 
36
7
  // src/plugin.ts
37
- import { mkdir, writeFile } from "node:fs/promises";
8
+ import { mkdir, writeFile, readdir } from "node:fs/promises";
38
9
  import { join, relative, sep } from "node:path";
39
10
  import { sep as posixSep } from "node:path/posix";
40
11
 
41
12
  // package.json
42
13
  var name = "@netlify/vite-plugin-react-router";
43
- var version = "2.0.0";
14
+ var version = "2.1.0";
44
15
 
45
16
  // src/plugin.ts
46
17
  var NETLIFY_FUNCTIONS_DIR = ".netlify/v1/functions";
18
+ var NETLIFY_EDGE_FUNCTIONS_DIR = ".netlify/v1/edge-functions";
47
19
  var FUNCTION_FILENAME = "react-router-server.mjs";
48
20
  var FUNCTION_HANDLER_CHUNK = "server";
49
21
  var FUNCTION_HANDLER_MODULE_ID = "virtual:netlify-server";
@@ -52,14 +24,24 @@ var toPosixPath = (path) => path.split(sep).join(posixSep);
52
24
  var FUNCTION_HANDLER = (
53
25
  /* js */
54
26
  `
55
- import { createRequestHandler } from "@netlify/vite-plugin-react-router";
27
+ import { createRequestHandler } from "@netlify/vite-plugin-react-router/serverless";
28
+ import * as build from "virtual:react-router/server-build";
29
+ export default createRequestHandler({
30
+ build,
31
+ });
32
+ `
33
+ );
34
+ var EDGE_FUNCTION_HANDLER = (
35
+ /* js */
36
+ `
37
+ import { createRequestHandler } from "@netlify/vite-plugin-react-router/edge";
56
38
  import * as build from "virtual:react-router/server-build";
57
39
  export default createRequestHandler({
58
40
  build,
59
41
  });
60
42
  `
61
43
  );
62
- function generateNetlifyFunction(handlerPath) {
44
+ function generateNetlifyFunction(handlerPath, excludedPath) {
63
45
  return (
64
46
  /* js */
65
47
  `
@@ -69,12 +51,31 @@ function generateNetlifyFunction(handlerPath) {
69
51
  name: "React Router server handler",
70
52
  generator: "${name}@${version}",
71
53
  path: "/*",
54
+ excludedPath: ${JSON.stringify(excludedPath)},
72
55
  preferStatic: true,
73
56
  };
74
57
  `
75
58
  );
76
59
  }
77
- function netlifyPlugin() {
60
+ function generateEdgeFunction(handlerPath, excludedPath) {
61
+ return (
62
+ /* js */
63
+ `
64
+ export { default } from "${handlerPath}";
65
+
66
+ export const config = {
67
+ name: "React Router server handler",
68
+ generator: "${name}@${version}",
69
+ cache: "manual",
70
+ path: "/*",
71
+ excludedPath: ${JSON.stringify(excludedPath)},
72
+ };
73
+ `
74
+ );
75
+ }
76
+ function netlifyPlugin(options = {}) {
77
+ const edge = options.edge ?? false;
78
+ const additionalExcludedPaths = options.excludedPaths ?? [];
78
79
  let resolvedConfig;
79
80
  let isProductionSsrBuild = false;
80
81
  return {
@@ -95,6 +96,19 @@ function netlifyPlugin() {
95
96
  config.build.rollupOptions.output = {};
96
97
  }
97
98
  config.build.rollupOptions.output.entryFileNames = "[name].js";
99
+ if (edge) {
100
+ config.ssr = {
101
+ ...config.ssr,
102
+ target: "webworker",
103
+ // Bundle everything except Node.js built-ins (which are supported but must use the `node:` prefix):
104
+ // https://docs.netlify.com/build/edge-functions/api/#runtime-environment
105
+ noExternal: /^(?!node:).*$/,
106
+ resolve: {
107
+ ...config.resolve,
108
+ conditions: ["worker", "deno", "browser"]
109
+ }
110
+ };
111
+ }
98
112
  }
99
113
  },
100
114
  async resolveId(source) {
@@ -105,7 +119,7 @@ function netlifyPlugin() {
105
119
  // See https://vitejs.dev/guide/api-plugin#virtual-modules-convention.
106
120
  load(id) {
107
121
  if (id === RESOLVED_FUNCTION_HANDLER_MODULE_ID) {
108
- return FUNCTION_HANDLER;
122
+ return edge ? EDGE_FUNCTION_HANDLER : FUNCTION_HANDLER;
109
123
  }
110
124
  },
111
125
  async configResolved(config) {
@@ -114,11 +128,32 @@ function netlifyPlugin() {
114
128
  // See https://rollupjs.org/plugin-development/#writebundle.
115
129
  async writeBundle() {
116
130
  if (isProductionSsrBuild) {
117
- const functionsDirectory = join(resolvedConfig.root, NETLIFY_FUNCTIONS_DIR);
118
- await mkdir(functionsDirectory, { recursive: true });
119
131
  const handlerPath = join(resolvedConfig.build.outDir, `${FUNCTION_HANDLER_CHUNK}.js`);
120
- const relativeHandlerPath = toPosixPath(relative(functionsDirectory, handlerPath));
121
- await writeFile(join(functionsDirectory, FUNCTION_FILENAME), generateNetlifyFunction(relativeHandlerPath));
132
+ if (edge) {
133
+ const clientDir = join(resolvedConfig.build.outDir, "..", "client");
134
+ const entries = await readdir(clientDir, { withFileTypes: true });
135
+ const excludedPath = [
136
+ "/.netlify/*",
137
+ ...entries.map((entry) => entry.isDirectory() ? `/${entry.name}/*` : `/${entry.name}`),
138
+ ...additionalExcludedPaths
139
+ ];
140
+ const edgeFunctionsDir = join(resolvedConfig.root, NETLIFY_EDGE_FUNCTIONS_DIR);
141
+ await mkdir(edgeFunctionsDir, { recursive: true });
142
+ const relativeHandlerPath = toPosixPath(relative(edgeFunctionsDir, handlerPath));
143
+ await writeFile(
144
+ join(edgeFunctionsDir, FUNCTION_FILENAME),
145
+ generateEdgeFunction(relativeHandlerPath, excludedPath)
146
+ );
147
+ } else {
148
+ const functionsDir = join(resolvedConfig.root, NETLIFY_FUNCTIONS_DIR);
149
+ await mkdir(functionsDir, { recursive: true });
150
+ const relativeHandlerPath = toPosixPath(relative(functionsDir, handlerPath));
151
+ const excludedPath = ["/.netlify/*", ...additionalExcludedPaths];
152
+ await writeFile(
153
+ join(functionsDir, FUNCTION_FILENAME),
154
+ generateNetlifyFunction(relativeHandlerPath, excludedPath)
155
+ );
156
+ }
122
157
  }
123
158
  }
124
159
  };
@@ -0,0 +1,31 @@
1
+ import * as react_router from 'react-router';
2
+ import { ServerBuild } from 'react-router';
3
+ import { Context } from '@netlify/functions';
4
+ import { G as GetLoadContextFunction$1, R as RequestHandler$1 } from './handler-4e_lz5jw.mjs';
5
+
6
+ declare module 'react-router' {
7
+ interface AppLoadContext extends Context {
8
+ }
9
+ }
10
+ type GetLoadContextFunction = GetLoadContextFunction$1<Context>;
11
+ type RequestHandler = RequestHandler$1<Context>;
12
+ /**
13
+ * An instance of `RouterContext` providing access to
14
+ * [Netlify request context]{@link https://docs.netlify.com/build/functions/api/#netlify-specific-context-object}
15
+ *
16
+ * @example context.get(netlifyRouterContext).geo?.country?.name
17
+ */
18
+ declare const netlifyRouterContext: react_router.RouterContext<Partial<Context>>;
19
+ /**
20
+ * Given a build and a callback to get the base loader context, this returns
21
+ * a Netlify Function handler (https://docs.netlify.com/functions/overview/) which renders the
22
+ * requested path. The loader context in this lifecycle will contain the Netlify Functions context
23
+ * fields merged in.
24
+ */
25
+ declare function createRequestHandler({ build, mode, getLoadContext, }: {
26
+ build: ServerBuild;
27
+ mode?: string;
28
+ getLoadContext?: GetLoadContextFunction;
29
+ }): RequestHandler;
30
+
31
+ export { type GetLoadContextFunction, type RequestHandler, createRequestHandler, netlifyRouterContext };
@@ -0,0 +1,9 @@
1
+ import {
2
+ createRequestHandler,
3
+ netlifyRouterContext
4
+ } from "./chunk-3EFABROO.mjs";
5
+ import "./chunk-J5PMA2AP.mjs";
6
+ export {
7
+ createRequestHandler,
8
+ netlifyRouterContext
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/vite-plugin-react-router",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "React Router 7+ Vite plugin for Netlify",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",
@@ -16,6 +16,14 @@
16
16
  "types": "./dist/index.d.mts",
17
17
  "default": "./dist/index.mjs"
18
18
  }
19
+ },
20
+ "./serverless": {
21
+ "types": "./dist/serverless.d.mts",
22
+ "default": "./dist/serverless.mjs"
23
+ },
24
+ "./edge": {
25
+ "types": "./dist/edge.d.mts",
26
+ "default": "./dist/edge.mjs"
19
27
  }
20
28
  },
21
29
  "files": [
@@ -43,7 +51,8 @@
43
51
  "isbot": "^5.0.0"
44
52
  },
45
53
  "devDependencies": {
46
- "@netlify/functions": "^3.1.9",
54
+ "@netlify/edge-functions": "^2.11.0",
55
+ "@netlify/functions": "^3.1.10",
47
56
  "@types/react": "^18.0.27",
48
57
  "@types/react-dom": "^18.0.10",
49
58
  "react": "^18.2.0",
@@ -63,7 +72,8 @@
63
72
  "access": "public"
64
73
  },
65
74
  "scripts": {
66
- "build": "tsup-node src/index.ts --format esm,cjs --dts --target node18 --clean",
67
- "build:watch": "pnpm run build --watch"
75
+ "build": "rm -rf dist && tsup-node",
76
+ "build:watch": "pnpm run build --watch",
77
+ "publint": "publint --strict"
68
78
  }
69
79
  }