Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple CLI arg parsing/execution for NODE_ENV initialization #12505

Closed
wants to merge 11 commits into from
6 changes: 6 additions & 0 deletions .changeset/default-node-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-router/serve": patch
"@react-router/dev": patch
---

Properly initialize `NODE_ENV` if not already set for compatibility with React 19
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
- johnpangalos
- jonkoops
- jrakotoharisoa
- jrestall
- juanpprieto
- jungwoo3490
- kachun333
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-dev/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ViteDevOptions } from "../vite/dev";
import type { ViteBuildOptions } from "../vite/build";
import { formatRoutes } from "../config/format";
import type { RoutesFormat } from "../config/format";
import { loadPluginContext } from "../vite/plugin";
import { loadPluginContext } from "../vite/plugin-context";
import { transpile as convertFileToJS } from "./useJavascript";
import * as profiler from "../vite/profiler";
import * as Typegen from "../typegen";
Expand Down
14 changes: 13 additions & 1 deletion packages/react-router-dev/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
#!/usr/bin/env node
import { run } from "./run";
import { parseArgs } from "./parse";

run().then(
let { input, flags, command } = parseArgs();

// If not already set, default `NODE_ENV` so React loads the proper
// version in it's CJS entry script. We have to do this before importing `run.ts`
// since that is what imports `react` (indirectly via `react-router`)
if (command === "dev") {
process.env.NODE_ENV = process.env.NODE_ENV ?? "development";
} else {
process.env.NODE_ENV = process.env.NODE_ENV ?? "production";
}
Comment on lines +5 to +14
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parse args and set NODE_ENV first


run(input, flags, command).then(
() => {
process.exit(0);
},
Expand Down
85 changes: 85 additions & 0 deletions packages/react-router-dev/cli/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import arg from "arg";
import semver from "semver";

/**
* Parse command line arguments for `react-router` dev CLI
*/
export function parseArgs(argv: string[] = process.argv.slice(2)) {
// Check the node version
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes to this logic, just extracted out from run() below

let versions = process.versions;
let MINIMUM_NODE_VERSION = 20;
if (
versions &&
versions.node &&
semver.major(versions.node) < MINIMUM_NODE_VERSION
) {
console.warn(
`️⚠️ Oops, Node v${versions.node} detected. react-router requires ` +
`a Node version greater than ${MINIMUM_NODE_VERSION}.`
);
}

let isBooleanFlag = (arg: string) => {
let index = argv.indexOf(arg);
let nextArg = argv[index + 1];
return !nextArg || nextArg.startsWith("-");
};

let args = arg(
{
"--force": Boolean,
"--help": Boolean,
"-h": "--help",
"--json": Boolean,
"--token": String,
"--typescript": Boolean,
"--no-typescript": Boolean,
"--version": Boolean,
"-v": "--version",
"--port": Number,
"-p": "--port",
"--config": String,
"-c": "--config",
"--assetsInlineLimit": Number,
"--clearScreen": Boolean,
"--cors": Boolean,
"--emptyOutDir": Boolean,
"--host": isBooleanFlag("--host") ? Boolean : String,
"--logLevel": String,
"-l": "--logLevel",
"--minify": String,
"--mode": String,
"-m": "--mode",
"--open": isBooleanFlag("--open") ? Boolean : String,
"--strictPort": Boolean,
"--profile": Boolean,
"--sourcemapClient": isBooleanFlag("--sourcemapClient")
? Boolean
: String,
"--sourcemapServer": isBooleanFlag("--sourcemapServer")
? Boolean
: String,
"--watch": Boolean,
},
{
argv,
}
);

let flags: any = Object.entries(args).reduce((acc, [key, value]) => {
key = key.replace(/^--/, "");
acc[key] = value;
return acc;
}, {} as any);

flags.interactive = flags.interactive ?? require.main === module;
if (args["--no-typescript"]) {
flags.typescript = false;
}

return {
input: args._,
command: args._[0],
flags,
};
}
80 changes: 1 addition & 79 deletions packages/react-router-dev/cli/run.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import arg from "arg";
import semver from "semver";
import colors from "picocolors";

import * as commands from "./commands";
Expand Down Expand Up @@ -81,76 +79,7 @@ ${colors.blueBright("react-router")}
* Programmatic interface for running the react-router CLI with the given command line
* arguments.
*/
export async function run(argv: string[] = process.argv.slice(2)) {
// Check the node version
let versions = process.versions;
let MINIMUM_NODE_VERSION = 20;
if (
versions &&
versions.node &&
semver.major(versions.node) < MINIMUM_NODE_VERSION
) {
console.warn(
`️⚠️ Oops, Node v${versions.node} detected. react-router requires ` +
`a Node version greater than ${MINIMUM_NODE_VERSION}.`
);
}

let isBooleanFlag = (arg: string) => {
let index = argv.indexOf(arg);
let nextArg = argv[index + 1];
return !nextArg || nextArg.startsWith("-");
};

let args = arg(
{
"--force": Boolean,
"--help": Boolean,
"-h": "--help",
"--json": Boolean,
"--token": String,
"--typescript": Boolean,
"--no-typescript": Boolean,
"--version": Boolean,
"-v": "--version",
"--port": Number,
"-p": "--port",
"--config": String,
"-c": "--config",
"--assetsInlineLimit": Number,
"--clearScreen": Boolean,
"--cors": Boolean,
"--emptyOutDir": Boolean,
"--host": isBooleanFlag("--host") ? Boolean : String,
"--logLevel": String,
"-l": "--logLevel",
"--minify": String,
"--mode": String,
"-m": "--mode",
"--open": isBooleanFlag("--open") ? Boolean : String,
"--strictPort": Boolean,
"--profile": Boolean,
"--sourcemapClient": isBooleanFlag("--sourcemapClient")
? Boolean
: String,
"--sourcemapServer": isBooleanFlag("--sourcemapServer")
? Boolean
: String,
"--watch": Boolean,
},
{
argv,
}
);

let input = args._;

let flags: any = Object.entries(args).reduce((acc, [key, value]) => {
key = key.replace(/^--/, "");
acc[key] = value;
return acc;
}, {} as any);

export async function run(input: string[], flags: any, command: string) {
if (flags.help) {
console.log(helpText);
return;
Expand All @@ -161,13 +90,6 @@ export async function run(argv: string[] = process.argv.slice(2)) {
return;
}

flags.interactive = flags.interactive ?? require.main === module;
if (args["--no-typescript"]) {
flags.typescript = false;
}

let command = input[0];

// Note: Keep each case in this switch statement small.
switch (command) {
case "routes":
Expand Down
4 changes: 2 additions & 2 deletions packages/react-router-dev/vite/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
type ServerBundleBuildConfig,
resolveViteConfig,
extractPluginContext,
getServerBuildDirectory,
} from "./plugin";
} from "./plugin-context";
import { getServerBuildDirectory } from "./plugin";
import {
type BuildManifest,
type ServerBundlesBuildManifest,
Expand Down
120 changes: 120 additions & 0 deletions packages/react-router-dev/vite/plugin-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// We can only import types from Vite at the top level since we're in a CJS
// context but want to use Vite's ESM build to avoid deprecation warnings
import * as fse from "fs-extra";
import * as path from "node:path";
import colors from "picocolors";
import type * as Vite from "vite";
import type { ResolvedReactRouterConfig } from "../config/config";
import type { RouteManifest } from "../config/routes";
import type { Manifest as ReactRouterManifest } from "../manifest";

export type ServerBundleBuildConfig = {
routes: RouteManifest;
serverBundleId: string;
};

export type ReactRouterPluginSsrBuildContext =
| {
isSsrBuild: false;
getReactRouterServerManifest?: never;
serverBundleBuildConfig?: never;
}
| {
isSsrBuild: true;
getReactRouterServerManifest: () => Promise<ReactRouterManifest>;
serverBundleBuildConfig: ServerBundleBuildConfig | null;
};

export type ReactRouterPluginContext = ReactRouterPluginSsrBuildContext & {
rootDirectory: string;
entryClientFilePath: string;
entryServerFilePath: string;
publicPath: string;
reactRouterConfig: ResolvedReactRouterConfig;
viteManifestEnabled: boolean;
};

export function findConfig(
dir: string,
basename: string,
extensions: string[]
): string | undefined {
for (let ext of extensions) {
let name = basename + ext;
let file = path.join(dir, name);
if (fse.existsSync(file)) return file;
}

return undefined;
}

export async function resolveViteConfig({
configFile,
mode,
root,
}: {
configFile?: string;
mode?: string;
root: string;
}) {
let vite = await import("vite");

let viteConfig = await vite.resolveConfig(
{ mode, configFile, root },
"build", // command
"production", // default mode
"production" // default NODE_ENV
);

if (typeof viteConfig.build.manifest === "string") {
throw new Error("Custom Vite manifest paths are not supported");
}

return viteConfig;
}

export async function extractPluginContext(viteConfig: Vite.ResolvedConfig) {
return viteConfig["__reactRouterPluginContext" as keyof typeof viteConfig] as
| ReactRouterPluginContext
| undefined;
}

export async function loadPluginContext({
configFile,
root,
}: {
configFile?: string;
root?: string;
}) {
if (!root) {
root = process.env.REACT_ROUTER_ROOT || process.cwd();
}

configFile =
configFile ??
findConfig(root, "vite.config", [
".ts",
".cts",
".mts",
".js",
".cjs",
".mjs",
]);

if (!configFile) {
console.error(colors.red("Vite config file not found"));
process.exit(1);
}

let viteConfig = await resolveViteConfig({ configFile, root });
let ctx = await extractPluginContext(viteConfig);

if (!ctx) {
console.error(
colors.red("React Router Vite plugin not found in Vite config")
);
process.exit(1);
}

return ctx;
}
Loading
Loading