Skip to content

Commit 7f7bb67

Browse files
authored
feat(cli): add preview command for production builds (#372)
Add a new Vite CLI subcommand that forwards arguments to the underlying Vite CLI: - `vite preview`: Preview production build (with --port, --host, etc.) This command follows the same pattern as existing `vite dev` and `vite build` commands. Additionally: - Running `vite` with no command now defaults to `vite dev` - Running `vite` with options (like `vite --port 3000`) is treated as `vite dev` - Added `preview` to the list of built-in commands - Added tests for the new command
1 parent b03c74b commit 7f7bb67

10 files changed

Lines changed: 188 additions & 19 deletions

File tree

packages/cli/binding/src/cli.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ pub enum Commands {
108108
/// Arguments to pass to vite dev
109109
args: Vec<String>,
110110
},
111+
/// Preview production build
112+
Preview {
113+
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
114+
/// Arguments to pass to vite preview
115+
args: Vec<String>,
116+
},
111117
/// Build documentation
112118
Doc {
113119
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
@@ -431,6 +437,13 @@ pub async fn main<
431437
workspace.unload().await?;
432438
summary
433439
}
440+
Commands::Preview { args } => {
441+
let workspace = Workspace::partial_load(cwd)?;
442+
let vite_fn = options.map(|o| o.vite).expect("preview command requires CliOptions");
443+
let summary = vite_cmd("preview", vite_fn, &workspace, args).await?;
444+
workspace.unload().await?;
445+
summary
446+
}
434447
Commands::Doc { args } => {
435448
let workspace = Workspace::partial_load(cwd)?;
436449
let doc_fn = options.map(|o| o.doc).expect("doc command requires CliOptions");
@@ -941,6 +954,28 @@ mod tests {
941954
}
942955
}
943956

957+
#[test]
958+
fn test_args_preview_command() {
959+
let args = Args::try_parse_from(["vite-plus", "preview"]).unwrap();
960+
assert_eq!(args.task, None);
961+
assert!(args.task_args.is_empty());
962+
assert!(matches!(args.commands, Commands::Preview { .. }));
963+
assert!(!args.debug);
964+
}
965+
966+
#[test]
967+
fn test_args_preview_command_with_args() {
968+
let args =
969+
Args::try_parse_from(["vite-plus", "preview", "--port", "3000", "--host"]).unwrap();
970+
assert_eq!(args.task, None);
971+
assert!(args.task_args.is_empty());
972+
if let Commands::Preview { args } = &args.commands {
973+
assert_eq!(args, &vec!["--port".to_string(), "3000".to_string(), "--host".to_string()]);
974+
} else {
975+
panic!("Expected Preview command");
976+
}
977+
}
978+
944979
#[test]
945980
fn test_args_complex_task_args() {
946981
let args = Args::try_parse_from([

packages/cli/binding/src/lib.rs

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ impl From<JsCommandResolvedResult> for ResolveCommandResult {
8080
}
8181
}
8282

83-
static BUILTIN_COMMANDS: &[&str] = &["lint", "fmt", "build", "test", "doc", "lib"];
83+
static BUILTIN_COMMANDS: &[&str] =
84+
&["dev", "lint", "fmt", "build", "test", "doc", "lib", "preview"];
8485

8586
/// Main entry point for the CLI, called from JavaScript.
8687
///
@@ -267,29 +268,70 @@ fn js_error_to_resolve_universal_vite_config_error(err: napi::Error) -> Error {
267268
fn parse_args() -> Args {
268269
// ArgsOs [node, vite-plus, ...]
269270
let mut raw_args = std::env::args_os().skip(2);
270-
if let Some(first) = raw_args.next()
271-
&& let Some(first) = first.to_str()
272-
&& BUILTIN_COMMANDS.contains(&first)
273-
{
274-
let forwarded_args = raw_args
271+
272+
// No arguments provided, default to dev command
273+
let Some(first) = raw_args.next() else {
274+
return Args {
275+
task: None,
276+
task_args: vec![],
277+
commands: Commands::Dev { args: vec![] },
278+
debug: false,
279+
no_debug: true,
280+
};
281+
};
282+
283+
// If first arg is not valid UTF-8, fall through to clap parsing
284+
let Some(first_str) = first.to_str() else {
285+
return Args::parse_from(std::env::args_os().skip(1));
286+
};
287+
288+
// Collect remaining args for potential forwarding
289+
let remaining_args: Vec<_> = raw_args.collect();
290+
291+
// Handle builtin commands with fast-path parsing (bypasses clap for better arg forwarding)
292+
if let Some(cmd) = parse_builtin_command(first_str, &remaining_args) {
293+
return cmd;
294+
}
295+
296+
// If first arg starts with '-' but is NOT a help/version flag, treat as options for dev command
297+
// e.g. `vite --port 3000` should be treated as `vite dev --port 3000`
298+
if first_str.starts_with('-') && !matches!(first_str, "-h" | "--help" | "-V" | "--version") {
299+
let forwarded_args: Vec<String> = std::iter::once(first)
300+
.chain(remaining_args)
275301
.map(|a| a.into_string().unwrap_or_else(|os_str| os_str.to_string_lossy().into_owned()))
276302
.collect();
277303
return Args {
278304
task: None,
279305
task_args: vec![],
280-
commands: match first {
281-
"lint" => Commands::Lint { args: forwarded_args },
282-
"fmt" => Commands::Fmt { args: forwarded_args },
283-
"build" => Commands::Build { args: forwarded_args },
284-
"test" => Commands::Test { args: forwarded_args },
285-
"doc" => Commands::Doc { args: forwarded_args },
286-
"lib" => Commands::Lib { args: forwarded_args },
287-
_ => unreachable!(),
288-
},
306+
commands: Commands::Dev { args: forwarded_args },
289307
debug: false,
290308
no_debug: true,
291309
};
292310
}
293-
// Parse CLI arguments (skip first arg which is the node binary)
311+
312+
// Fall through to clap parsing for other commands (run, cache, install, help, etc.)
294313
Args::parse_from(std::env::args_os().skip(1))
295314
}
315+
316+
fn parse_builtin_command(cmd: &str, raw_args: &[std::ffi::OsString]) -> Option<Args> {
317+
if !BUILTIN_COMMANDS.contains(&cmd) {
318+
return None;
319+
}
320+
321+
let forwarded_args: Vec<String> =
322+
raw_args.iter().map(|a| a.to_string_lossy().into_owned()).collect();
323+
324+
let commands = match cmd {
325+
"dev" => Commands::Dev { args: forwarded_args },
326+
"lint" => Commands::Lint { args: forwarded_args },
327+
"fmt" => Commands::Fmt { args: forwarded_args },
328+
"build" => Commands::Build { args: forwarded_args },
329+
"test" => Commands::Test { args: forwarded_args },
330+
"doc" => Commands::Doc { args: forwarded_args },
331+
"lib" => Commands::Lib { args: forwarded_args },
332+
"preview" => Commands::Preview { args: forwarded_args },
333+
_ => return None,
334+
};
335+
336+
Some(Args { task: None, task_args: vec![], commands, debug: false, no_debug: true })
337+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
> vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError
22
RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (12312312312).
3+
4+
> vite --port 12312312313 2>&1 | grep RangeError # vite without args should be alias to dev command
5+
RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (12312312313).

packages/cli/snap-tests/command-dev-with-port/steps.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"VITE_DISABLE_AUTO_INSTALL": "1"
55
},
66
"commands": [
7-
"vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError"
7+
"vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError",
8+
"vite --port 12312312313 2>&1 | grep RangeError # vite without args should be alias to dev command"
89
]
910
}

packages/cli/snap-tests/command-helper/snap.txt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Commands:
99
test Run test
1010
lib Build library
1111
dev Run development server
12+
preview Preview production build
1213
doc Build documentation
1314
cache Manage the task cache
1415
install Install command. It will be passed to the package manager's install command currently
@@ -333,3 +334,64 @@ Options:
333334
--experimental <features> Experimental features.. Use '--help --experimental' for more info.
334335
-h, --help Display this message
335336

337+
338+
> vite preview -h # preview help message
339+
vite/<semver>
340+
341+
Usage:
342+
$ vite preview [root]
343+
344+
Options:
345+
--host [host] [string] specify hostname
346+
--port <port> [number] specify port
347+
--strictPort [boolean] exit if specified port is already in use
348+
--open [path] [boolean | string] open browser on startup
349+
--outDir <dir> [string] output directory (default: dist)
350+
-c, --config <file> [string] use specified config file
351+
--base <path> [string] public base path (default: /)
352+
-l, --logLevel <level> [string] info | warn | error | silent
353+
--clearScreen [boolean] allow/disable clear screen when logging
354+
--configLoader <loader> [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)
355+
-d, --debug [feat] [string | boolean] show debug logs
356+
-f, --filter <filter> [string] filter debug logs
357+
-m, --mode <mode> [string] set env mode
358+
-h, --help Display this message
359+
360+
361+
> vite dev -h # dev help message
362+
vite/<semver>
363+
364+
Usage:
365+
$ vite [root]
366+
367+
Commands:
368+
[root] start dev server
369+
build [root] build for production
370+
optimize [root] pre-bundle dependencies (deprecated, the pre-bundle process runs automatically and does not need to be called)
371+
preview [root] locally preview production build
372+
373+
For more info, run any command with the `--help` flag:
374+
$ vite --help
375+
$ vite build --help
376+
$ vite optimize --help
377+
$ vite preview --help
378+
379+
Options:
380+
--host [host] [string] specify hostname
381+
--port <port> [number] specify port
382+
--open [path] [boolean | string] open browser on startup
383+
--cors [boolean] enable CORS
384+
--strictPort [boolean] exit if specified port is already in use
385+
--force [boolean] force the optimizer to ignore the cache and re-bundle
386+
--experimentalBundle [boolean] use experimental full bundle mode (this is highly experimental)
387+
-c, --config <file> [string] use specified config file
388+
--base <path> [string] public base path (default: /)
389+
-l, --logLevel <level> [string] info | warn | error | silent
390+
--clearScreen [boolean] allow/disable clear screen when logging
391+
--configLoader <loader> [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)
392+
-d, --debug [feat] [string | boolean] show debug logs
393+
-f, --filter <filter> [string] filter debug logs
394+
-m, --mode <mode> [string] set env mode
395+
-h, --help Display this message
396+
-v, --version Display version number
397+

packages/cli/snap-tests/command-helper/steps.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"vite fmt -h # fmt help message",
1010
"vite lint -h # lint help message",
1111
"vite build -h # build help message",
12-
"vite test -h # test help message"
12+
"vite test -h # test help message",
13+
"vite preview -h # preview help message",
14+
"vite dev -h # dev help message"
1315
]
1416
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
> vite preview --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError
2+
RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (12312312312).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"ignoredPlatforms": ["win32"],
3+
"env": {
4+
"VITE_DISABLE_AUTO_INSTALL": "1"
5+
},
6+
"commands": [
7+
"vite preview --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError"
8+
]
9+
}

packages/global/src/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
// Parse command line arguments to intercept 'new', 'gen', and 'migration' commands
22
const args = process.argv.slice(2);
33

4-
const LOCAL_CLI_COMMANDS = ['dev', 'build', 'test', 'lint', 'fmt', 'format', 'lib', 'doc', 'run'];
4+
const LOCAL_CLI_COMMANDS = [
5+
'dev',
6+
'build',
7+
'test',
8+
'lint',
9+
'fmt',
10+
'format',
11+
'lib',
12+
'doc',
13+
'run',
14+
'preview',
15+
];
516

617
const command = args[0];
718

0 commit comments

Comments
 (0)