33//! A bare `vp dev`/`build`/`preview`/`pack` at a workspace root has no target
44//! and would silently run against the root. Resolution order (rfcs/cwd-flag.md):
55//! explicit `-C` and positional targets are handled before this code and skip
6- //! elicitation entirely; then `defaultPackage` from the root config, then the
7- //! workspace package listing (interactive picker planned once `vite_select`
8- //! supports a custom prompt), then exit 1.
9-
10- use std:: io:: IsTerminal ;
6+ //! elicitation entirely; then `defaultPackage` from the config in the
7+ //! invocation directory, then the workspace package listing (interactive
8+ //! picker planned once `vite_select` supports a custom prompt), then exit 1.
119
1210use vite_error:: Error ;
1311use vite_path:: AbsolutePathBuf ;
@@ -46,61 +44,49 @@ fn app_command_parts(subcommand: &SynthesizableSubcommand) -> Option<(&'static s
4644 }
4745}
4846
49- /// Bare = no positional target. A non-flag token may be a flag value
50- /// (`--port 3000`), so any non-flag argument conservatively disables
51- /// elicitation and forwards the invocation unchanged.
47+ /// Bare = no positional target and no help-like flag. A non-flag token may be
48+ /// a flag value (`--port 3000`), so any non-flag argument conservatively
49+ /// disables elicitation; help/version requests are answered by the underlying
50+ /// tool and must never be redirected.
5251fn is_bare ( args : & [ String ] ) -> bool {
53- args. iter ( ) . all ( |arg| arg. starts_with ( '-' ) )
54- }
55-
56- /// Mirror of the global command picker's interactivity gate.
57- fn is_interactive ( ) -> bool {
58- const CI_ENV_VARS : & [ & str ] =
59- & [ "CI" , "CONTINUOUS_INTEGRATION" , "GITHUB_ACTIONS" , "GITLAB_CI" , "BUILDKITE" , "JENKINS_URL" ] ;
60- std:: io:: stdin ( ) . is_terminal ( )
61- && std:: io:: stdout ( ) . is_terminal ( )
62- && std:: env:: var ( "TERM" ) . map_or ( true , |term| term != "dumb" )
63- && !CI_ENV_VARS . iter ( ) . any ( |key| std:: env:: var_os ( key) . is_some ( ) )
52+ args. iter ( ) . all ( |arg| {
53+ arg. starts_with ( '-' ) && !matches ! ( arg. as_str( ) , "-h" | "--help" | "-V" | "--version" )
54+ } )
6455}
6556
6657/// Heuristic ranking signal: does `dir` look runnable for `command`?
6758/// Used for ordering and single-candidate auto-selection, never for hiding.
6859fn looks_runnable ( dir : & AbsolutePathBuf , command : & str ) -> bool {
69- const VITE_CONFIGS : & [ & str ] =
70- & [ "vite.config.ts" , "vite.config.js" , "vite.config.mts" , "vite.config.mjs" ] ;
71- let has_vite_config = VITE_CONFIGS . iter ( ) . any ( |name| dir. as_path ( ) . join ( name) . is_file ( ) ) ;
60+ let has_vite_config = vite_static_config:: has_config_file ( dir) ;
7261 match command {
7362 "pack" => has_vite_config || dir. as_path ( ) . join ( "src/index.ts" ) . is_file ( ) ,
7463 _ => has_vite_config || dir. as_path ( ) . join ( "index.html" ) . is_file ( ) ,
7564 }
7665}
7766
78- /// `defaultPackage` from the root `vite.config.*`, read via static extraction
79- /// so it works at roots without a vite-plus install (non-workspace framework
80- /// repos). The value must be a static string literal.
67+ /// `defaultPackage` from the `vite.config.*` in `cwd` , read via static
68+ /// extraction so it works at roots without a vite-plus install (non-workspace
69+ /// framework repos). The value must be a static string literal.
8170fn resolve_default_package ( command : & str , cwd : & AbsolutePathBuf ) -> Option < AppTarget > {
82- let fields = vite_static_config:: resolve_static_config ( cwd) ;
83- match fields. get ( "defaultPackage" ) {
71+ let fail = |msg : & str | {
72+ output:: error ( msg) ;
73+ Some ( AppTarget :: Exit ( ExitStatus ( 1 ) ) )
74+ } ;
75+ match vite_static_config:: resolve_static_config ( cwd) . get ( "defaultPackage" ) {
8476 Some ( vite_static_config:: FieldValue :: Json ( serde_json:: Value :: String ( dir) ) ) => {
85- let mut target = cwd. clone ( ) ;
86- target. push ( dir. trim_start_matches ( "./" ) ) ;
77+ let target = cwd. join ( & dir) . clean ( ) ;
8778 if !target. as_path ( ) . is_dir ( ) {
88- output:: error ( & format ! ( "defaultPackage points to a missing directory: {dir}" ) ) ;
89- return Some ( AppTarget :: Exit ( ExitStatus ( 1 ) ) ) ;
79+ return fail ( & format ! ( "defaultPackage points to a missing directory: {dir}" ) ) ;
9080 }
9181 output:: note ( & format ! ( "vp {command}: using {dir} (defaultPackage)" ) ) ;
9282 Some ( AppTarget :: Dir ( target) )
9383 }
9484 Some ( vite_static_config:: FieldValue :: Json ( other) ) => {
95- output:: error ( & format ! ( "defaultPackage must be a string of a directory, got: {other}" ) ) ;
96- Some ( AppTarget :: Exit ( ExitStatus ( 1 ) ) )
97- }
98- Some ( vite_static_config:: FieldValue :: NonStatic ) => {
99- output:: error (
100- "defaultPackage in vite.config.ts must be a static string literal so vp can read it without executing the config" ,
101- ) ;
102- Some ( AppTarget :: Exit ( ExitStatus ( 1 ) ) )
85+ fail ( & format ! ( "defaultPackage must be a string of a directory, got: {other}" ) )
10386 }
87+ Some ( vite_static_config:: FieldValue :: NonStatic ) => fail (
88+ "defaultPackage in vite.config.ts must be a static string literal so vp can read it without executing the config" ,
89+ ) ,
10490 None => None ,
10591 }
10692}
@@ -116,18 +102,21 @@ pub(super) fn resolve_app_target(
116102 return Ok ( AppTarget :: CurrentDir ) ;
117103 }
118104
119- let ( workspace_root, rel_from_root) = vite_workspace:: find_workspace_root ( cwd) ?;
120- if !rel_from_root. as_str ( ) . is_empty ( ) {
121- return Ok ( AppTarget :: CurrentDir ) ;
122- }
123-
105+ // `defaultPackage` comes before any workspace lookup: the non-workspace
106+ // framework shape (a Laravel-style root with a vite.config.ts pointer and
107+ // no package.json up-tree) has no workspace metadata at all.
124108 if let Some ( target) = resolve_default_package ( command, cwd) {
125109 return Ok ( target) ;
126110 }
127111
128- // Only real workspaces have a package list to offer; a standalone package
129- // root keeps today's behavior.
130- if matches ! ( workspace_root. workspace_file, WorkspaceFile :: NonWorkspacePackage ( _) ) {
112+ // The package listing needs workspace metadata; anything unresolvable
113+ // keeps today's behavior (the caller surfaces its own workspace errors).
114+ let Ok ( ( workspace_root, rel_from_root) ) = vite_workspace:: find_workspace_root ( cwd) else {
115+ return Ok ( AppTarget :: CurrentDir ) ;
116+ } ;
117+ if !rel_from_root. as_str ( ) . is_empty ( )
118+ || matches ! ( workspace_root. workspace_file, WorkspaceFile :: NonWorkspacePackage ( _) )
119+ {
131120 return Ok ( AppTarget :: CurrentDir ) ;
132121 }
133122
@@ -136,25 +125,26 @@ pub(super) fn resolve_app_target(
136125 let mut rows: Vec < PackageRow > = graph
137126 . node_weights ( )
138127 . filter ( |info| !info. path . as_str ( ) . is_empty ( ) )
139- . map ( |info| PackageRow {
140- name : info. package_json . name . to_string ( ) ,
141- path : info. path . as_str ( ) . to_string ( ) ,
142- absolute : info. absolute_path . to_absolute_path_buf ( ) ,
143- runnable : false ,
128+ . map ( |info| {
129+ let absolute = info. absolute_path . to_absolute_path_buf ( ) ;
130+ PackageRow {
131+ name : info. package_json . name . to_string ( ) ,
132+ path : info. path . as_str ( ) . to_string ( ) ,
133+ runnable : looks_runnable ( & absolute, command) ,
134+ absolute,
135+ }
144136 } )
145137 . collect ( ) ;
146138 if rows. is_empty ( ) {
147139 return Ok ( AppTarget :: CurrentDir ) ;
148140 }
149- for row in & mut rows {
150- row. runnable = looks_runnable ( & row. absolute , command) ;
151- }
152141 rows. sort_by ( |a, b| ( !a. runnable , a. path . as_str ( ) ) . cmp ( & ( !b. runnable , b. path . as_str ( ) ) ) ) ;
153142
154- // With exactly one likely-runnable package, an interactive terminal
155- // auto-selects it (the degenerate picker).
156- if is_interactive ( ) && rows. iter ( ) . filter ( |row| row. runnable ) . count ( ) == 1 {
157- let row = rows. iter ( ) . find ( |row| row. runnable ) . expect ( "one runnable row exists" ) ;
143+ // With exactly one likely-runnable package (rows are sorted runnable
144+ // first), an interactive terminal auto-selects it (the degenerate picker).
145+ let single_runnable = rows[ 0 ] . runnable && rows. get ( 1 ) . is_none_or ( |row| !row. runnable ) ;
146+ if single_runnable && vite_shared:: is_interactive_terminal ( ) {
147+ let row = & rows[ 0 ] ;
158148 println ! ( "Selected package: {} ({})" , row. name, row. path) ;
159149 println ! ( "Tip: run this directly with `vp -C {} {command}`" , row. path) ;
160150 return Ok ( AppTarget :: Dir ( row. absolute . clone ( ) ) ) ;
@@ -168,7 +158,7 @@ pub(super) fn resolve_app_target(
168158 output:: raw_stderr ( & format ! ( " {:<name_width$} {}" , row. name, row. path) ) ;
169159 }
170160 output:: raw_stderr ( "" ) ;
171- let example = & rows. first ( ) . expect ( "rows is non-empty" ) . path ;
161+ let example = & rows[ 0 ] . path ;
172162 output:: raw_stderr ( & format ! ( " Pass a directory: vp -C {example} {command}" ) ) ;
173163 output:: raw_stderr ( & format ! ( " Or run every package's {command} script: vp run -r {command}" ) ) ;
174164 Ok ( AppTarget :: Exit ( ExitStatus ( 1 ) ) )
@@ -179,7 +169,7 @@ mod tests {
179169 use super :: * ;
180170
181171 #[ test]
182- fn bare_means_no_positional_target ( ) {
172+ fn bare_means_no_positional_target_and_no_help ( ) {
183173 let to_args = |args : & [ & str ] | args. iter ( ) . map ( |s| ( * s) . to_string ( ) ) . collect :: < Vec < _ > > ( ) ;
184174 assert ! ( is_bare( & to_args( & [ ] ) ) ) ;
185175 assert ! ( is_bare( & to_args( & [ "--watch" ] ) ) ) ;
@@ -189,6 +179,10 @@ mod tests {
189179 // A flag value is indistinguishable from a positional without knowing
190180 // the tool's flag arity, so it conservatively counts as non-bare.
191181 assert ! ( !is_bare( & to_args( & [ "--port" , "3000" ] ) ) ) ;
182+ // Help/version requests go to the underlying tool, never elicitation.
183+ assert ! ( !is_bare( & to_args( & [ "--help" ] ) ) ) ;
184+ assert ! ( !is_bare( & to_args( & [ "-h" ] ) ) ) ;
185+ assert ! ( !is_bare( & to_args( & [ "--watch" , "--version" ] ) ) ) ;
192186 }
193187
194188 #[ test]
0 commit comments