diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index d9a9a1c459..d3a7832f95 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -108,6 +108,12 @@ pub enum Commands { /// Arguments to pass to vite dev args: Vec, }, + /// Preview production build + Preview { + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + /// Arguments to pass to vite preview + args: Vec, + }, /// Build documentation Doc { #[arg(allow_hyphen_values = true, trailing_var_arg = true)] @@ -431,6 +437,13 @@ pub async fn main< workspace.unload().await?; summary } + Commands::Preview { args } => { + let workspace = Workspace::partial_load(cwd)?; + let vite_fn = options.map(|o| o.vite).expect("preview command requires CliOptions"); + let summary = vite_cmd("preview", vite_fn, &workspace, args).await?; + workspace.unload().await?; + summary + } Commands::Doc { args } => { let workspace = Workspace::partial_load(cwd)?; let doc_fn = options.map(|o| o.doc).expect("doc command requires CliOptions"); @@ -941,6 +954,28 @@ mod tests { } } + #[test] + fn test_args_preview_command() { + let args = Args::try_parse_from(["vite-plus", "preview"]).unwrap(); + assert_eq!(args.task, None); + assert!(args.task_args.is_empty()); + assert!(matches!(args.commands, Commands::Preview { .. })); + assert!(!args.debug); + } + + #[test] + fn test_args_preview_command_with_args() { + let args = + Args::try_parse_from(["vite-plus", "preview", "--port", "3000", "--host"]).unwrap(); + assert_eq!(args.task, None); + assert!(args.task_args.is_empty()); + if let Commands::Preview { args } = &args.commands { + assert_eq!(args, &vec!["--port".to_string(), "3000".to_string(), "--host".to_string()]); + } else { + panic!("Expected Preview command"); + } + } + #[test] fn test_args_complex_task_args() { let args = Args::try_parse_from([ diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index b7a8b92d92..e4534ac1fd 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -80,7 +80,8 @@ impl From for ResolveCommandResult { } } -static BUILTIN_COMMANDS: &[&str] = &["lint", "fmt", "build", "test", "doc", "lib"]; +static BUILTIN_COMMANDS: &[&str] = + &["dev", "lint", "fmt", "build", "test", "doc", "lib", "preview"]; /// Main entry point for the CLI, called from JavaScript. /// @@ -267,29 +268,70 @@ fn js_error_to_resolve_universal_vite_config_error(err: napi::Error) -> Error { fn parse_args() -> Args { // ArgsOs [node, vite-plus, ...] let mut raw_args = std::env::args_os().skip(2); - if let Some(first) = raw_args.next() - && let Some(first) = first.to_str() - && BUILTIN_COMMANDS.contains(&first) - { - let forwarded_args = raw_args + + // No arguments provided, default to dev command + let Some(first) = raw_args.next() else { + return Args { + task: None, + task_args: vec![], + commands: Commands::Dev { args: vec![] }, + debug: false, + no_debug: true, + }; + }; + + // If first arg is not valid UTF-8, fall through to clap parsing + let Some(first_str) = first.to_str() else { + return Args::parse_from(std::env::args_os().skip(1)); + }; + + // Collect remaining args for potential forwarding + let remaining_args: Vec<_> = raw_args.collect(); + + // Handle builtin commands with fast-path parsing (bypasses clap for better arg forwarding) + if let Some(cmd) = parse_builtin_command(first_str, &remaining_args) { + return cmd; + } + + // If first arg starts with '-' but is NOT a help/version flag, treat as options for dev command + // e.g. `vite --port 3000` should be treated as `vite dev --port 3000` + if first_str.starts_with('-') && !matches!(first_str, "-h" | "--help" | "-V" | "--version") { + let forwarded_args: Vec = std::iter::once(first) + .chain(remaining_args) .map(|a| a.into_string().unwrap_or_else(|os_str| os_str.to_string_lossy().into_owned())) .collect(); return Args { task: None, task_args: vec![], - commands: match first { - "lint" => Commands::Lint { args: forwarded_args }, - "fmt" => Commands::Fmt { args: forwarded_args }, - "build" => Commands::Build { args: forwarded_args }, - "test" => Commands::Test { args: forwarded_args }, - "doc" => Commands::Doc { args: forwarded_args }, - "lib" => Commands::Lib { args: forwarded_args }, - _ => unreachable!(), - }, + commands: Commands::Dev { args: forwarded_args }, debug: false, no_debug: true, }; } - // Parse CLI arguments (skip first arg which is the node binary) + + // Fall through to clap parsing for other commands (run, cache, install, help, etc.) Args::parse_from(std::env::args_os().skip(1)) } + +fn parse_builtin_command(cmd: &str, raw_args: &[std::ffi::OsString]) -> Option { + if !BUILTIN_COMMANDS.contains(&cmd) { + return None; + } + + let forwarded_args: Vec = + raw_args.iter().map(|a| a.to_string_lossy().into_owned()).collect(); + + let commands = match cmd { + "dev" => Commands::Dev { args: forwarded_args }, + "lint" => Commands::Lint { args: forwarded_args }, + "fmt" => Commands::Fmt { args: forwarded_args }, + "build" => Commands::Build { args: forwarded_args }, + "test" => Commands::Test { args: forwarded_args }, + "doc" => Commands::Doc { args: forwarded_args }, + "lib" => Commands::Lib { args: forwarded_args }, + "preview" => Commands::Preview { args: forwarded_args }, + _ => return None, + }; + + Some(Args { task: None, task_args: vec![], commands, debug: false, no_debug: true }) +} diff --git a/packages/cli/snap-tests/command-dev-with-port/snap.txt b/packages/cli/snap-tests/command-dev-with-port/snap.txt index 44d205d25d..f58ae58df8 100644 --- a/packages/cli/snap-tests/command-dev-with-port/snap.txt +++ b/packages/cli/snap-tests/command-dev-with-port/snap.txt @@ -1,2 +1,5 @@ > vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (12312312312). + +> vite --port 12312312313 2>&1 | grep RangeError # vite without args should be alias to dev command +RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (12312312313). diff --git a/packages/cli/snap-tests/command-dev-with-port/steps.json b/packages/cli/snap-tests/command-dev-with-port/steps.json index 37c6780a68..8eea5d917c 100644 --- a/packages/cli/snap-tests/command-dev-with-port/steps.json +++ b/packages/cli/snap-tests/command-dev-with-port/steps.json @@ -4,6 +4,7 @@ "VITE_DISABLE_AUTO_INSTALL": "1" }, "commands": [ - "vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError" + "vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError", + "vite --port 12312312313 2>&1 | grep RangeError # vite without args should be alias to dev command" ] } diff --git a/packages/cli/snap-tests/command-helper/snap.txt b/packages/cli/snap-tests/command-helper/snap.txt index 6113b10259..a55503e37b 100644 --- a/packages/cli/snap-tests/command-helper/snap.txt +++ b/packages/cli/snap-tests/command-helper/snap.txt @@ -9,6 +9,7 @@ Commands: test Run test lib Build library dev Run development server + preview Preview production build doc Build documentation cache Manage the task cache install Install command. It will be passed to the package manager's install command currently @@ -333,3 +334,64 @@ Options: --experimental Experimental features.. Use '--help --experimental' for more info. -h, --help Display this message + +> vite preview -h # preview help message +vite/ + +Usage: + $ vite preview [root] + +Options: + --host [host] [string] specify hostname + --port [number] specify port + --strictPort [boolean] exit if specified port is already in use + --open [path] [boolean | string] open browser on startup + --outDir [string] output directory (default: dist) + -c, --config [string] use specified config file + --base [string] public base path (default: /) + -l, --logLevel [string] info | warn | error | silent + --clearScreen [boolean] allow/disable clear screen when logging + --configLoader [string] use 'bundle' to bundle the config with Rolldown, or 'runner' (experimental) to process it on the fly, or 'native' (experimental) to load using the native runtime (default: bundle) + -d, --debug [feat] [string | boolean] show debug logs + -f, --filter [string] filter debug logs + -m, --mode [string] set env mode + -h, --help Display this message + + +> vite dev -h # dev help message +vite/ + +Usage: + $ vite [root] + +Commands: + [root] start dev server + build [root] build for production + optimize [root] pre-bundle dependencies (deprecated, the pre-bundle process runs automatically and does not need to be called) + preview [root] locally preview production build + +For more info, run any command with the `--help` flag: + $ vite --help + $ vite build --help + $ vite optimize --help + $ vite preview --help + +Options: + --host [host] [string] specify hostname + --port [number] specify port + --open [path] [boolean | string] open browser on startup + --cors [boolean] enable CORS + --strictPort [boolean] exit if specified port is already in use + --force [boolean] force the optimizer to ignore the cache and re-bundle + --experimentalBundle [boolean] use experimental full bundle mode (this is highly experimental) + -c, --config [string] use specified config file + --base [string] public base path (default: /) + -l, --logLevel [string] info | warn | error | silent + --clearScreen [boolean] allow/disable clear screen when logging + --configLoader [string] use 'bundle' to bundle the config with Rolldown, or 'runner' (experimental) to process it on the fly, or 'native' (experimental) to load using the native runtime (default: bundle) + -d, --debug [feat] [string | boolean] show debug logs + -f, --filter [string] filter debug logs + -m, --mode [string] set env mode + -h, --help Display this message + -v, --version Display version number + diff --git a/packages/cli/snap-tests/command-helper/steps.json b/packages/cli/snap-tests/command-helper/steps.json index 706f525726..c9b7cffc1c 100644 --- a/packages/cli/snap-tests/command-helper/steps.json +++ b/packages/cli/snap-tests/command-helper/steps.json @@ -9,6 +9,8 @@ "vite fmt -h # fmt help message", "vite lint -h # lint help message", "vite build -h # build help message", - "vite test -h # test help message" + "vite test -h # test help message", + "vite preview -h # preview help message", + "vite dev -h # dev help message" ] } diff --git a/packages/cli/snap-tests/command-preview/package.json b/packages/cli/snap-tests/command-preview/package.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/packages/cli/snap-tests/command-preview/package.json @@ -0,0 +1,2 @@ +{ +} diff --git a/packages/cli/snap-tests/command-preview/snap.txt b/packages/cli/snap-tests/command-preview/snap.txt new file mode 100644 index 0000000000..a555b89040 --- /dev/null +++ b/packages/cli/snap-tests/command-preview/snap.txt @@ -0,0 +1,2 @@ +> vite preview --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError +RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (12312312312). diff --git a/packages/cli/snap-tests/command-preview/steps.json b/packages/cli/snap-tests/command-preview/steps.json new file mode 100644 index 0000000000..b098bb0a85 --- /dev/null +++ b/packages/cli/snap-tests/command-preview/steps.json @@ -0,0 +1,9 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vite preview --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError" + ] +} diff --git a/packages/global/src/index.ts b/packages/global/src/index.ts index 7650aeada5..d7ec385772 100644 --- a/packages/global/src/index.ts +++ b/packages/global/src/index.ts @@ -1,7 +1,18 @@ // Parse command line arguments to intercept 'new', 'gen', and 'migration' commands const args = process.argv.slice(2); -const LOCAL_CLI_COMMANDS = ['dev', 'build', 'test', 'lint', 'fmt', 'format', 'lib', 'doc', 'run']; +const LOCAL_CLI_COMMANDS = [ + 'dev', + 'build', + 'test', + 'lint', + 'fmt', + 'format', + 'lib', + 'doc', + 'run', + 'preview', +]; const command = args[0];