diff --git a/integration/vite-plugin-cloudflare-test.ts b/integration/vite-plugin-cloudflare-test.ts
index e77eae582a..a8ceb5754a 100644
--- a/integration/vite-plugin-cloudflare-test.ts
+++ b/integration/vite-plugin-cloudflare-test.ts
@@ -93,6 +93,53 @@ test.describe("vite-plugin-cloudflare", () => {
);
});
+ test("does not force node export conditions", async ({ dev, page }) => {
+ const baseFiles = defineFiles();
+ const files: Files = async (args) => ({
+ ...(await baseFiles(args)),
+ "app/routes/conditional-export.tsx": tsx`
+ import { runtime } from "conditional-runtime";
+
+ export function loader() {
+ return { runtime };
+ }
+
+ export default function ConditionalExportsRoute({
+ loaderData,
+ }: {
+ loaderData: { runtime: string };
+ }) {
+ return
{loaderData.runtime}
;
+ }
+ `,
+ "node_modules/conditional-runtime/package.json": JSON.stringify({
+ name: "conditional-runtime",
+ type: "module",
+ exports: {
+ ".": {
+ node: "./node.js",
+ default: "./worker.js",
+ },
+ },
+ }),
+ "node_modules/conditional-runtime/node.js": tsx`
+ import "node:http";
+ export const runtime = "node";
+ `,
+ "node_modules/conditional-runtime/worker.js": tsx`
+ export const runtime = "worker";
+ `,
+ });
+ const { port } = await dev(files, "vite-plugin-cloudflare-template");
+
+ await page.goto(`http://localhost:${port}/conditional-export`, {
+ waitUntil: "networkidle",
+ });
+
+ expect(page.errors).toEqual([]);
+ await expect(page.locator("[data-runtime]")).toHaveText("worker");
+ });
+
test.describe("without JavaScript", () => {
test.use({ javaScriptEnabled: false });
diff --git a/packages/react-router-dev/.changes/patch.vite-node-external-conditions.md b/packages/react-router-dev/.changes/patch.vite-node-external-conditions.md
new file mode 100644
index 0000000000..63b2e4c502
--- /dev/null
+++ b/packages/react-router-dev/.changes/patch.vite-node-external-conditions.md
@@ -0,0 +1,3 @@
+Only add the `"node"` Vite server condition for Framework mode apps that declare a Node server adapter dependency
+
+This prevents non-Node SSR runtimes from resolving Node-specific package exports by default.
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index a9853cd198..682dcc4420 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -36,6 +36,7 @@ import pick from "lodash/pick.js";
import jsesc from "jsesc";
import colors from "picocolors";
import kebabCase from "lodash/kebabCase.js";
+import { readPackageJSON, type PackageJson } from "pkg-types";
const nodeRequire = createRequire(import.meta.url);
@@ -3500,6 +3501,9 @@ export async function getEnvironmentOptionsResolvers(
viteCommand: Vite.ResolvedConfig["command"],
): Promise {
let { serverBuildFile, serverModuleFormat } = ctx.reactRouterConfig;
+ let pkgJson: PackageJson = await readPackageJSON(ctx.rootDirectory).catch(
+ () => ({}),
+ );
let packageRoot = path.dirname(
nodeRequire.resolve("@react-router/dev/package.json"),
@@ -3564,10 +3568,24 @@ export async function getEnvironmentOptionsResolvers(
// https://vite.dev/guide/migration.html#default-value-for-resolve-conditions
let maybeDefaultServerConditions = vite.defaultServerConditions || [];
- // There is no helpful export with the default external conditions (see
- // https://github.com/vitejs/vite/pull/20279 for more details). So, for now,
- // we are hardcording the default here.
- let defaultExternalConditions = ["node"];
+ // Vite added this in 7.1, so we need to be defensive since our minimum version is 7.0
+ let defaultExternalConditions = vite.defaultExternalConditions ?? ["node"];
+
+ // If we couldn't find the package.json, we assume node for backwards compatibility
+ let isNode =
+ !pkgJson.dependencies ||
+ pkgJson.dependencies["@react-router/node"] ||
+ pkgJson.dependencies["@react-router/express"] ||
+ pkgJson.dependencies["@react-router/serve"];
+
+ if (!isNode) {
+ maybeDefaultServerConditions = maybeDefaultServerConditions.filter(
+ (c) => c !== "node",
+ );
+ defaultExternalConditions = defaultExternalConditions.filter(
+ (c) => c !== "node",
+ );
+ }
let baseConditions = [
...maybeDevelopmentConditions,
diff --git a/scripts/changes/migrate-changesets.ts b/scripts/changes/migrate-changesets.ts
index d8a38a20e1..78f8c40faf 100644
--- a/scripts/changes/migrate-changesets.ts
+++ b/scripts/changes/migrate-changesets.ts
@@ -21,6 +21,7 @@ import * as cp from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
+import { parseArgs } from "node:util";
import {
GITHUB_REPO_URL,
packageNameToDirectoryName,
@@ -31,8 +32,14 @@ const repoRoot = path.resolve(__dirname, "..", "..");
const changesetsDir = path.join(repoRoot, ".changeset");
const packagesDir = path.join(repoRoot, "packages");
-const args = process.argv.slice(2);
-const dryRun = args.includes("--dry-run");
+const { values } = parseArgs({
+ options: {
+ "dry-run": {
+ type: "boolean",
+ },
+ },
+});
+const dryRun = values["dry-run"] === true;
const validBumps = new Set(["major", "minor", "patch"]);
diff --git a/scripts/changes/pr.ts b/scripts/changes/pr.ts
index 21b6d73225..a7f86519ff 100644
--- a/scripts/changes/pr.ts
+++ b/scripts/changes/pr.ts
@@ -7,6 +7,8 @@
* Environment:
* GITHUB_TOKEN - Required (unless --preview)
*/
+import { parseArgs } from "node:util";
+
import * as semver from "semver";
import { readJson } from "../utils/fs.ts";
@@ -26,8 +28,14 @@ import {
parseAllChangeFiles,
} from "./changes.ts";
-let args = process.argv.slice(2);
-let preview = args.includes("--preview");
+let { values } = parseArgs({
+ options: {
+ preview: {
+ type: "boolean",
+ },
+ },
+});
+let preview = values.preview === true;
let baseBranch = logAndExec("git rev-parse --abbrev-ref HEAD", true).trim();
let releaseBranches = ["main", "hotfix", "v7"];
diff --git a/scripts/docs.ts b/scripts/docs.ts
index 0f2776064b..f5db20ec5b 100644
--- a/scripts/docs.ts
+++ b/scripts/docs.ts
@@ -99,7 +99,6 @@ const isComponentApi = (c: SimplifiedComment) =>
// Read a filename from standard input using the node parseArgs utility
const { values: args } = util.parseArgs({
- args: process.argv.slice(2),
options: {
path: {
type: "string",
@@ -122,7 +121,6 @@ const { values: args } = util.parseArgs({
short: "h",
},
},
- allowPositionals: true,
});
if (args.help) {
diff --git a/scripts/experimental.ts b/scripts/experimental.ts
index ddafdfc3cb..32bbbebac1 100644
--- a/scripts/experimental.ts
+++ b/scripts/experimental.ts
@@ -1,6 +1,7 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
+import { parseArgs } from "node:util";
import { colorize, colors } from "./utils/color.ts";
import { logAndExec } from "./utils/process.ts";
@@ -11,9 +12,16 @@ const packageDirNames = fs
.readdirSync("packages")
.filter((name) => fs.statSync(`packages/${name}`).isDirectory());
-const command = process.argv[2];
-const args = process.argv.slice(3);
-const dryRun = args.includes("--dry-run");
+const { values, positionals } = parseArgs({
+ allowPositionals: true,
+ options: {
+ "dry-run": {
+ type: "boolean",
+ },
+ },
+});
+const command = positionals[0];
+const dryRun = values["dry-run"] === true;
if (command === "version") {
await bumpVersion();
diff --git a/scripts/pr.ts b/scripts/pr.ts
index 7575e5465c..0da0c9dc53 100644
--- a/scripts/pr.ts
+++ b/scripts/pr.ts
@@ -147,7 +147,7 @@ async function changeFileCheck(ctx: CheckContext): Promise {
let regex =
/^packages\/[^/]+\/\.changes\/(major|minor|patch|unstable)\.[^/]+\.md$/;
let summaries: ChangeFileSummary[] = files
- .filter((f) => regex.test(f.filename))
+ .filter((f) => f.status !== "removed" && regex.test(f.filename))
.map((f) => {
let type = f.filename.match(regex)?.[1] ?? "unknown";
let firstLine =