diff --git a/.changeset/tidy-pens-help.md b/.changeset/tidy-pens-help.md
new file mode 100644
index 0000000000..5db3b598d6
--- /dev/null
+++ b/.changeset/tidy-pens-help.md
@@ -0,0 +1,10 @@
+---
+"@react-router/express": major
+"@react-router/node": major
+"@react-router/dev": major
+"react-router": major
+---
+
+Drop support for Node 18, update minimum Node vestion to 20
+
+- Remove `installGlobals()` as this should no longer be necessary
diff --git a/.github/workflows/integration-full.yml b/.github/workflows/integration-full.yml
index bdebc1d319..3f79331816 100644
--- a/.github/workflows/integration-full.yml
+++ b/.github/workflows/integration-full.yml
@@ -34,7 +34,7 @@ jobs:
uses: ./.github/workflows/shared-integration.yml
with:
os: "ubuntu-latest"
- node_version: "[18, 20]"
+ node_version: "[20, 22]"
browser: '["chromium", "firefox"]'
integration-windows:
@@ -43,7 +43,7 @@ jobs:
uses: ./.github/workflows/shared-integration.yml
with:
os: "windows-latest"
- node_version: "[18, 20]"
+ node_version: "[20, 22]"
browser: '["msedge"]'
integration-macos:
@@ -52,5 +52,5 @@ jobs:
uses: ./.github/workflows/shared-integration.yml
with:
os: "macos-latest"
- node_version: "[18, 20]"
+ node_version: "[20, 22]"
browser: '["webkit"]'
diff --git a/.github/workflows/integration-pr-ubuntu.yml b/.github/workflows/integration-pr-ubuntu.yml
index 6223906475..ac25713e08 100644
--- a/.github/workflows/integration-pr-ubuntu.yml
+++ b/.github/workflows/integration-pr-ubuntu.yml
@@ -31,5 +31,5 @@ jobs:
uses: ./.github/workflows/shared-integration.yml
with:
os: "ubuntu-latest"
- node_version: "[20]"
+ node_version: "[22]"
browser: '["chromium"]'
diff --git a/.github/workflows/integration-pr-windows-macos.yml b/.github/workflows/integration-pr-windows-macos.yml
index c5e08598e6..780a81f289 100644
--- a/.github/workflows/integration-pr-windows-macos.yml
+++ b/.github/workflows/integration-pr-windows-macos.yml
@@ -21,7 +21,7 @@ jobs:
uses: ./.github/workflows/shared-integration.yml
with:
os: "ubuntu-latest"
- node_version: "[20]"
+ node_version: "[22]"
browser: '["firefox"]'
integration-msedge:
@@ -30,7 +30,7 @@ jobs:
uses: ./.github/workflows/shared-integration.yml
with:
os: "windows-latest"
- node_version: "[20]"
+ node_version: "[22]"
browser: '["msedge"]'
integration-webkit:
@@ -39,5 +39,5 @@ jobs:
uses: ./.github/workflows/shared-integration.yml
with:
os: "macos-latest"
- node_version: "[20]"
+ node_version: "[22]"
browser: '["webkit"]'
diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml
index 33536ca41f..cc2c3e0ccb 100644
--- a/.github/workflows/shared-integration.yml
+++ b/.github/workflows/shared-integration.yml
@@ -9,7 +9,7 @@ on:
node_version:
required: true
# this is limited to string | boolean | number (https://github.community/t/can-action-inputs-be-arrays/16457)
- # but we want to pass an array (node_version: "[18, 20]"),
+ # but we want to pass an array (node_version: "[20, 22]"),
# so we'll need to manually stringify it for now
type: string
browser:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b85bab5c89..e19ec5116a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -26,8 +26,8 @@ jobs:
fail-fast: false
matrix:
node:
- - 18
- 20
+ - 22
runs-on: ubuntu-latest
diff --git a/docs/deploying/custom-node.md b/docs/deploying/custom-node.md
index d3021a698b..af9ef966ed 100644
--- a/docs/deploying/custom-node.md
+++ b/docs/deploying/custom-node.md
@@ -7,3 +7,41 @@ title: Custom Node.js
This document is a work in progress. There's not much to see here (yet).
+
+## Polyfilling `fetch`
+
+React Router officially supports Active and Maintenance[^1] [Node LTS veleases][node-releases] at any given point in time. Dropping support for End of Life Node versions may be done in a React Router Minor release.
+
+[^1]: Based on timing, React Router may drop support for a Node Maintenance LTS version shortly before it goes end-of-life if it better aligns with a React Router Major SemVer release.
+
+At the time React Router v7 was released, all versions had a usable `fetch` implementation so there is generally no need to polyfill any `fetch` APIs so long as you're on Node 22 or one of the later Node 20 releases.
+
+- Node 22 (Active LTS) has a stable [`fetch`][node-22-fetch] implementation
+- Node 20 (Maintenance LTS) has an experimental (but suitable from our testing) [`fetch`][node-20-fetch] implementation
+
+If you do find that you need to polyfill anything, you can do so directly from the [undici] package which node uses internally.
+
+```ts
+import {
+ fetch as nodeFetch,
+ File as NodeFile,
+ FormData as NodeFormData,
+ Headers as NodeHeaders,
+ Request as NodeRequest,
+ Response as NodeResponse,
+} from "undici";
+
+export function polyfillFetch() {
+ global.File = NodeFile;
+ global.Headers = NodeHeaders;
+ global.Request = NodeRequest;
+ global.Response = NodeResponse;
+ global.fetch = nodeFetch;
+ global.FormData = NodeFormData;
+}
+```
+
+[node-releases]: https://nodejs.org/en/about/previous-releases
+[node-20-fetch]: https://nodejs.org/docs/latest-v20.x/api/globals.html#fetch
+[node-22-fetch]: https://nodejs.org/docs/latest-v22.x/api/globals.html#fetch
+[undici]: https://github.com/nodejs/undici
diff --git a/integration/helpers/create-fixture.ts b/integration/helpers/create-fixture.ts
index f8358c51f6..ee4d9fe648 100644
--- a/integration/helpers/create-fixture.ts
+++ b/integration/helpers/create-fixture.ts
@@ -15,7 +15,6 @@ import {
UNSAFE_decodeViaTurboStream as decodeViaTurboStream,
} from "react-router";
import { createRequestHandler as createExpressHandler } from "@react-router/express";
-import { installGlobals } from "@react-router/node";
import { viteConfig } from "./vite.js";
@@ -43,8 +42,6 @@ export function json(value: JsonObject) {
}
export async function createFixture(init: FixtureInit, mode?: ServerMode) {
- installGlobals();
-
let projectDir = await createFixtureProject(init, mode);
let buildPath = url.pathToFileURL(
path.join(projectDir, "build/server/index.js")
diff --git a/integration/helpers/node-template/package.json b/integration/helpers/node-template/package.json
index cfe718a8b2..e43fc371a6 100644
--- a/integration/helpers/node-template/package.json
+++ b/integration/helpers/node-template/package.json
@@ -34,6 +34,6 @@
"typescript": "^5.1.0"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/integration/helpers/vite-cloudflare-template/package.json b/integration/helpers/vite-cloudflare-template/package.json
index cb05e97ba9..62caf49967 100644
--- a/integration/helpers/vite-cloudflare-template/package.json
+++ b/integration/helpers/vite-cloudflare-template/package.json
@@ -30,6 +30,6 @@
"wrangler": "^3.28.2"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/integration/helpers/vite-template/package.json b/integration/helpers/vite-template/package.json
index 2555f0659a..286a7c9ca7 100644
--- a/integration/helpers/vite-template/package.json
+++ b/integration/helpers/vite-template/package.json
@@ -36,6 +36,6 @@
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts
index cb09f54f9a..3e33597f6b 100644
--- a/integration/helpers/vite.ts
+++ b/integration/helpers/vite.ts
@@ -69,11 +69,8 @@ export const EXPRESS_SERVER = (args: {
}) =>
String.raw`
import { createRequestHandler } from "@react-router/express";
- import { installGlobals } from "@react-router/node";
import express from "express";
- installGlobals();
-
let viteDevServer =
process.env.NODE_ENV === "production"
? undefined
diff --git a/integration/vite-basename-test.ts b/integration/vite-basename-test.ts
index 42f5c25e6c..f723b625b5 100644
--- a/integration/vite-basename-test.ts
+++ b/integration/vite-basename-test.ts
@@ -95,9 +95,7 @@ const customServerFile = ({
return js`
import { createRequestHandler } from "@react-router/express";
- import { installGlobals } from "@react-router/node";
import express from "express";
- installGlobals();
const viteDevServer =
process.env.NODE_ENV === "production"
@@ -488,9 +486,7 @@ test.describe("Vite base / React Router basename / express build", async () => {
// Slim server that only serves basename (route) requests from the React Router handler
"server.mjs": String.raw`
import { createRequestHandler } from "@react-router/express";
- import { installGlobals } from "@react-router/node";
import express from "express";
- installGlobals();
const app = express();
app.all(
diff --git a/jest/jest.config.shared.js b/jest/jest.config.shared.js
index 186df726b9..05322e3a9b 100644
--- a/jest/jest.config.shared.js
+++ b/jest/jest.config.shared.js
@@ -19,7 +19,6 @@ module.exports = {
),
},
modulePathIgnorePatterns: ignorePatterns,
- setupFiles: ["/__tests__/setup.ts"],
testMatch: ["/**/*-test.[jt]s?(x)"],
transform: {
"\\.[jt]sx?$": require.resolve("./transform"),
diff --git a/package.json b/package.json
index 73cf006e6f..e3fb9b90d6 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,6 @@
"@types/wait-on": "^5.3.2",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
- "abort-controller": "^3.0.0",
"babel-jest": "^29.7.0",
"babel-plugin-dev-expression": "^0.2.3",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -122,7 +121,7 @@
"vite-tsconfig-paths": "^4.2.2"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"pnpm": {
"patchedDependencies": {
diff --git a/packages/react-router-architect/__tests__/setup.ts b/packages/react-router-architect/__tests__/setup.ts
deleted file mode 100644
index 996e99893d..0000000000
--- a/packages/react-router-architect/__tests__/setup.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { installGlobals } from "@react-router/node";
-installGlobals();
diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json
index a9664a798f..0029467b24 100644
--- a/packages/react-router-architect/package.json
+++ b/packages/react-router-architect/package.json
@@ -48,7 +48,7 @@
}
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router-cloudflare/package.json b/packages/react-router-cloudflare/package.json
index 472d627722..f261eb7812 100644
--- a/packages/react-router-cloudflare/package.json
+++ b/packages/react-router-cloudflare/package.json
@@ -40,7 +40,7 @@
}
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router-dev/__tests__/fixtures/node/package.json b/packages/react-router-dev/__tests__/fixtures/node/package.json
index ffff19de35..922e211c64 100644
--- a/packages/react-router-dev/__tests__/fixtures/node/package.json
+++ b/packages/react-router-dev/__tests__/fixtures/node/package.json
@@ -24,6 +24,6 @@
"typescript": "^5.1.6"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/packages/react-router-dev/cli/run.ts b/packages/react-router-dev/cli/run.ts
index 5751d49a68..518a92f1e0 100644
--- a/packages/react-router-dev/cli/run.ts
+++ b/packages/react-router-dev/cli/run.ts
@@ -81,9 +81,15 @@ ${colors.logoBlue("react-router")}
export async function run(argv: string[] = process.argv.slice(2)) {
// Check the node version
let versions = process.versions;
- if (versions && versions.node && semver.major(versions.node) < 18) {
+ let MINIMUM_NODE_VERSION = 20;
+ if (
+ versions &&
+ versions.node &&
+ semver.major(versions.node) < MINIMUM_NODE_VERSION
+ ) {
throw new Error(
- `️🚨 Oops, Node v${versions.node} detected. react-router requires a Node version greater than 18.`
+ `️🚨 Oops, Node v${versions.node} detected. react-router requires ` +
+ `a Node version greater than ${MINIMUM_NODE_VERSION}.`
);
}
diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json
index 7acf8e2124..c9abd90fd9 100644
--- a/packages/react-router-dev/package.json
+++ b/packages/react-router-dev/package.json
@@ -112,7 +112,7 @@
}
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json
index dcfa6fcefd..66576cf452 100644
--- a/packages/react-router-dom/package.json
+++ b/packages/react-router-dom/package.json
@@ -47,6 +47,6 @@
"README.md"
],
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/packages/react-router-express/__tests__/setup.ts b/packages/react-router-express/__tests__/setup.ts
deleted file mode 100644
index 996e99893d..0000000000
--- a/packages/react-router-express/__tests__/setup.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { installGlobals } from "@react-router/node";
-installGlobals();
diff --git a/packages/react-router-express/package.json b/packages/react-router-express/package.json
index d714b261bb..127acb5c50 100644
--- a/packages/react-router-express/package.json
+++ b/packages/react-router-express/package.json
@@ -46,7 +46,7 @@
}
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json
index 6f1792964c..1a8e28a7b2 100644
--- a/packages/react-router-fs-routes/package.json
+++ b/packages/react-router-fs-routes/package.json
@@ -40,7 +40,7 @@
}
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router-node/__tests__/setup.ts b/packages/react-router-node/__tests__/setup.ts
deleted file mode 100644
index 53d59d28c0..0000000000
--- a/packages/react-router-node/__tests__/setup.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { installGlobals } from "../globals";
-
-installGlobals();
diff --git a/packages/react-router-node/globals.ts b/packages/react-router-node/globals.ts
deleted file mode 100644
index cbd188c312..0000000000
--- a/packages/react-router-node/globals.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- File as NodeFile,
- fetch as nodeFetch,
- FormData as NodeFormData,
- Headers as NodeHeaders,
- Request as NodeRequest,
- Response as NodeResponse,
-} from "undici";
-import { webcrypto as nodeWebCrypto } from "node:crypto";
-
-declare global {
- namespace NodeJS {
- interface ProcessEnv {
- NODE_ENV: "development" | "production" | "test";
- }
-
- interface Global {
- File: typeof File;
-
- Headers: typeof Headers;
- Request: typeof Request;
- Response: typeof Response;
- fetch: typeof fetch;
- FormData: typeof FormData;
-
- ReadableStream: typeof ReadableStream;
- WritableStream: typeof WritableStream;
-
- crypto: typeof nodeWebCrypto;
- }
- }
-
- interface RequestInit {
- duplex?: "half";
- }
-}
-
-export function installGlobals() {
- global.File = NodeFile as unknown as typeof File;
- // @ts-ignore - this shows as an error in VSCode but is not an error via TSC so we can't use `ts-expect-error`
- global.Headers = NodeHeaders;
- // @ts-expect-error - overriding globals
- global.Request = NodeRequest;
- // @ts-expect-error - overriding globals
- global.Response = NodeResponse;
- // @ts-expect-error - overriding globals
- global.fetch = nodeFetch;
- // @ts-expect-error - overriding globals
- global.FormData = NodeFormData;
-
- if (!global.crypto) {
- // @ts-expect-error - overriding globals
- global.crypto = nodeWebCrypto;
- }
-}
diff --git a/packages/react-router-node/index.ts b/packages/react-router-node/index.ts
index 616231434b..7d7c108d44 100644
--- a/packages/react-router-node/index.ts
+++ b/packages/react-router-node/index.ts
@@ -1,5 +1,3 @@
-export { installGlobals } from "./globals";
-
export { createFileSessionStorage } from "./sessions/fileStorage";
export {
diff --git a/packages/react-router-node/install.ts b/packages/react-router-node/install.ts
deleted file mode 100644
index e5bc566ae4..0000000000
--- a/packages/react-router-node/install.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { installGlobals } from "./globals";
-
-installGlobals();
diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json
index e4640d532a..a4ffb33922 100644
--- a/packages/react-router-node/package.json
+++ b/packages/react-router-node/package.json
@@ -18,10 +18,6 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
- "./install": {
- "types": "./dist/install.d.ts",
- "default": "./dist/install.js"
- },
"./package.json": "./package.json"
},
"sideEffects": [
@@ -52,7 +48,7 @@
}
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router-node/rollup.config.js b/packages/react-router-node/rollup.config.js
index 49266fca02..b100ca6c4c 100644
--- a/packages/react-router-node/rollup.config.js
+++ b/packages/react-router-node/rollup.config.js
@@ -21,11 +21,9 @@ module.exports = function rollup() {
"react-router-node"
);
- const input = [`${SOURCE_DIR}/index.ts`, `${SOURCE_DIR}/install.ts`];
-
return [
{
- input,
+ input: `${SOURCE_DIR}/index.ts`,
external: (id) => isBareModuleId(id),
output: {
banner: createBanner(name, version),
diff --git a/packages/react-router-remix-config-routes-adapter/package.json b/packages/react-router-remix-config-routes-adapter/package.json
index ec6a103d95..ed5795e7e3 100644
--- a/packages/react-router-remix-config-routes-adapter/package.json
+++ b/packages/react-router-remix-config-routes-adapter/package.json
@@ -37,7 +37,7 @@
}
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router-serve/cli.ts b/packages/react-router-serve/cli.ts
index 8929f8624e..acd33b61b9 100644
--- a/packages/react-router-serve/cli.ts
+++ b/packages/react-router-serve/cli.ts
@@ -1,10 +1,8 @@
-import "@react-router/node/install";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import url from "node:url";
import type { ServerBuild } from "react-router";
-import { installGlobals } from "@react-router/node";
import { createRequestHandler } from "@react-router/express";
import compression from "compression";
import express from "express";
@@ -30,7 +28,6 @@ sourceMapSupport.install({
return null;
},
});
-installGlobals();
run();
diff --git a/packages/react-router-serve/package.json b/packages/react-router-serve/package.json
index f2198dbf7f..977299e83d 100644
--- a/packages/react-router-serve/package.json
+++ b/packages/react-router-serve/package.json
@@ -39,7 +39,7 @@
"@types/source-map-support": "^0.5.6"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"files": [
"dist/",
diff --git a/packages/react-router/__tests__/router/navigation-test.ts b/packages/react-router/__tests__/router/navigation-test.ts
index 9210490c91..356f6ec38d 100644
--- a/packages/react-router/__tests__/router/navigation-test.ts
+++ b/packages/react-router/__tests__/router/navigation-test.ts
@@ -171,11 +171,10 @@ describe("navigations", () => {
);
expect(t.router.state.loaderData).toEqual({});
- // Node 16/18 versus 20 output different errors here :/
- let expected = process.version.startsWith("v18")
- ? "Unexpected token } in JSON at position 15"
- : "Unexpected non-whitespace character after JSON at position 15";
- expect(t.router.state.errors?.foo).toEqual(new SyntaxError(expected));
+ expect(t.router.state.errors?.foo).toBeInstanceOf(SyntaxError);
+ expect(t.router.state.errors?.foo.message).toContain(
+ "Unexpected non-whitespace character after JSON at position 15"
+ );
});
it("bubbles errors when unwrapping Responses", async () => {
@@ -207,11 +206,10 @@ describe("navigations", () => {
);
expect(t.router.state.loaderData).toEqual({});
- // Node 16/18 versus 20 output different errors here :/
- let expected = process.version.startsWith("v18")
- ? "Unexpected token } in JSON at position 15"
- : "Unexpected non-whitespace character after JSON at position 15";
- expect(t.router.state.errors?.root).toEqual(new SyntaxError(expected));
+ expect(t.router.state.errors?.root).toBeInstanceOf(SyntaxError);
+ expect(t.router.state.errors?.root.message).toContain(
+ "Unexpected non-whitespace character after JSON at position 15"
+ );
});
it("does not fetch unchanging layout data", async () => {
diff --git a/packages/react-router/__tests__/router/router-memory-test.ts b/packages/react-router/__tests__/router/router-memory-test.ts
index 49ae9617c7..5a06d3510f 100644
--- a/packages/react-router/__tests__/router/router-memory-test.ts
+++ b/packages/react-router/__tests__/router/router-memory-test.ts
@@ -211,36 +211,4 @@ describe("a memory router", () => {
router.dispose();
});
-
- it("throws on submitting FormData when it's not available", async () => {
- if (global.FormData) {
- // This is globally available in Node 18, this test is primarily for Node 16
- // eslint-disable-next-line jest/no-conditional-expect
- expect(true).toBe(true);
- return;
- }
-
- let actionSpy = jest.fn();
-
- let router = createRouter({
- routes: [
- {
- path: "/",
- action: actionSpy,
- },
- ],
- history: createMemoryHistory(),
- });
-
- await expect(() =>
- router.navigate("/", {
- formMethod: "post",
- body: { key: "value" },
- })
- ).rejects.toThrowErrorMatchingInlineSnapshot(
- `"FormData is not available in this environment"`
- );
-
- router.dispose();
- });
});
diff --git a/packages/react-router/__tests__/router/ssr-test.ts b/packages/react-router/__tests__/router/ssr-test.ts
index a38afa1f6e..4ef923a368 100644
--- a/packages/react-router/__tests__/router/ssr-test.ts
+++ b/packages/react-router/__tests__/router/ssr-test.ts
@@ -753,9 +753,6 @@ describe("ssr", () => {
let e;
try {
let contextPromise = query(request);
- // Note this works in Node 18+ - but it does not work if using the
- // `abort-controller` polyfill which doesn't yet support a custom `reason`
- // See: https://github.com/mysticatea/abort-controller/issues/33
controller.abort(new Error("Oh no!"));
// This should resolve even though we never resolved the loader
await contextPromise;
@@ -2082,9 +2079,6 @@ describe("ssr", () => {
let e;
try {
let statePromise = queryRoute(request, { routeId: "root" });
- // Note this works in Node 18+ - but it does not work if using the
- // `abort-controller` polyfill which doesn't yet support a custom `reason`
- // See: https://github.com/mysticatea/abort-controller/issues/33
controller.abort(new Error("Oh no!"));
// This should resolve even though we never resolved the loader
await statePromise;
diff --git a/packages/react-router/__tests__/setup.ts b/packages/react-router/__tests__/setup.ts
index 357b5924bc..8c208adb43 100644
--- a/packages/react-router/__tests__/setup.ts
+++ b/packages/react-router/__tests__/setup.ts
@@ -1,15 +1,19 @@
// https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
-if (!globalThis.fetch) {
+if (!globalThis.TextEncoder || !globalThis.TextDecoder) {
const { TextDecoder, TextEncoder } = require("node:util");
- globalThis.TextDecoder = TextDecoder;
globalThis.TextEncoder = TextEncoder;
+ globalThis.TextDecoder = TextDecoder;
+}
+if (!globalThis.ReadableStream || !globalThis.WritableStream) {
const { ReadableStream, WritableStream } = require("node:stream/web");
globalThis.ReadableStream = ReadableStream;
globalThis.WritableStream = WritableStream;
+}
+if (!globalThis.fetch) {
const { fetch, FormData, Request, Response, Headers } = require("undici");
globalThis.fetch = fetch;
@@ -20,17 +24,6 @@ if (!globalThis.fetch) {
globalThis.FormData = globalThis.FormData || FormData;
}
-if (!globalThis.AbortController) {
- const { AbortController } = require("abort-controller");
- globalThis.AbortController = AbortController;
-}
-
-if (!globalThis.TextEncoder || !globalThis.TextDecoder) {
- const { TextDecoder, TextEncoder } = require("node:util");
- globalThis.TextEncoder = TextEncoder;
- globalThis.TextDecoder = TextDecoder;
-}
-
if (!globalThis.TextEncoderStream) {
const { TextEncoderStream } = require("node:stream/web");
globalThis.TextEncoderStream = TextEncoderStream;
@@ -40,13 +33,3 @@ if (!globalThis.TransformStream) {
const { TransformStream } = require("node:stream/web");
globalThis.TransformStream = TransformStream;
}
-
-if (!globalThis.File) {
- const { File } = require("undici");
- globalThis.File = File;
-}
-
-if (!globalThis.crypto) {
- const { webcrypto } = require("node:crypto");
- globalThis.crypto = webcrypto;
-}
diff --git a/packages/react-router/jest.config.js b/packages/react-router/jest.config.js
index 8408526b8d..dc45566ce5 100644
--- a/packages/react-router/jest.config.js
+++ b/packages/react-router/jest.config.js
@@ -1,6 +1,7 @@
/** @type {import('jest').Config} */
module.exports = {
...require("../../jest/jest.config.shared"),
+ setupFiles: ["/__tests__/setup.ts"],
setupFilesAfterEnv: ["@testing-library/jest-dom"],
testEnvironment: "jsdom",
};
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 215eb32c85..bc9f207be2 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -68,6 +68,6 @@
"README.md"
],
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/playground/compiler-express/package.json b/playground/compiler-express/package.json
index 33e0243b91..3902c0aafc 100644
--- a/playground/compiler-express/package.json
+++ b/playground/compiler-express/package.json
@@ -34,6 +34,6 @@
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/playground/compiler-express/server.js b/playground/compiler-express/server.js
index 9d28534e9f..fa5048f32c 100644
--- a/playground/compiler-express/server.js
+++ b/playground/compiler-express/server.js
@@ -1,11 +1,8 @@
import { createRequestHandler } from "@react-router/express";
-import { installGlobals } from "@react-router/node";
import compression from "compression";
import express from "express";
import morgan from "morgan";
-installGlobals();
-
const viteDevServer =
process.env.NODE_ENV === "production"
? undefined
diff --git a/playground/compiler-spa/package.json b/playground/compiler-spa/package.json
index 503fac9554..b42a057d06 100644
--- a/playground/compiler-spa/package.json
+++ b/playground/compiler-spa/package.json
@@ -25,6 +25,6 @@
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/playground/compiler/package.json b/playground/compiler/package.json
index 1c0efa9e8a..d6d8a3d145 100644
--- a/playground/compiler/package.json
+++ b/playground/compiler/package.json
@@ -27,6 +27,6 @@
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/playground/compiler/vite.config.ts b/playground/compiler/vite.config.ts
index 6735e6449d..f910ad4c18 100644
--- a/playground/compiler/vite.config.ts
+++ b/playground/compiler/vite.config.ts
@@ -1,10 +1,7 @@
import { reactRouter } from "@react-router/dev/vite";
-import { installGlobals } from "@react-router/node";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
-installGlobals();
-
export default defineConfig({
plugins: [reactRouter(), tsconfigPaths()],
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 318d5e6b4c..574dc54fb3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -133,9 +133,6 @@ importers:
'@typescript-eslint/parser':
specifier: ^7.5.0
version: 7.5.0(eslint@8.57.0)(typescript@5.4.5)
- abort-controller:
- specifier: ^3.0.0
- version: 3.0.0
babel-jest:
specifier: ^29.7.0
version: 29.7.0(@babel/core@7.22.9)
@@ -3108,10 +3105,6 @@ packages:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
deprecated: Use your platform's native atob() and btoa() methods instead
- abort-controller@3.0.0:
- resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
- engines: {node: '>=6.5'}
-
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -4255,10 +4248,6 @@ packages:
resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==}
engines: {node: '>= 0.8'}
- event-target-shim@5.0.1:
- resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
- engines: {node: '>=6'}
-
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@@ -10532,10 +10521,6 @@ snapshots:
abab@2.0.6: {}
- abort-controller@3.0.0:
- dependencies:
- event-target-shim: 5.0.1
-
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
@@ -12083,8 +12068,6 @@ snapshots:
'@types/node': 18.19.26
require-like: 0.1.2
- event-target-shim@5.0.1: {}
-
execa@5.1.1:
dependencies:
cross-spawn: 7.0.3
diff --git a/rollup.utils.js b/rollup.utils.js
index e131cc72df..4de511b754 100644
--- a/rollup.utils.js
+++ b/rollup.utils.js
@@ -143,7 +143,7 @@ function isBareModuleId(id) {
const remixBabelConfig = {
presets: [
- ["@babel/preset-env", { targets: { node: "18" } }],
+ ["@babel/preset-env", { targets: { node: "20" } }],
"@babel/preset-react",
"@babel/preset-typescript",
],