@@ -134,47 +134,49 @@ fn is_bare(command: &str, args: &[String]) -> bool {
134134/// silent root build this feature exists to prevent.
135135fn looks_runnable ( dir : & AbsolutePathBuf , command : & str , is_root : bool ) -> bool {
136136 match command {
137- // Bare `vp pack` succeeds when the config explicitly declares a
138- // `pack` block or tsdown's default entry exists. A spread that only
137+ // Bare `vp pack` succeeds when tsdown's default entry exists or the
138+ // config explicitly declares a `pack` block (a spread that only
139139 // might contain `pack` does not count: auto-select acts on this
140- // signal, so a false positive runs tsdown in a non-packable package.
140+ // signal, so a false positive runs tsdown in a non-packable
141+ // package). The one-stat entry check runs first: this executes per
142+ // workspace package, and the config check reads and parses a file.
141143 "pack" => {
142- vite_static_config :: resolve_static_config ( dir) . get_declared ( "pack ") . is_some ( )
143- || dir . as_path ( ) . join ( "src/index.ts ") . is_file ( )
144+ dir. as_path ( ) . join ( "src/index.ts ") . is_file ( )
145+ || vite_static_config :: resolve_static_config ( dir ) . get_declared ( "pack ") . is_some ( )
144146 }
145147 _ if is_root => dir. as_path ( ) . join ( "index.html" ) . is_file ( ) ,
146148 _ => vite_static_config:: has_config_file ( dir) || dir. as_path ( ) . join ( "index.html" ) . is_file ( ) ,
147149 }
148150}
149151
150- /// `defaultPackage` from the `vite.config.*` in `cwd`, read via static
151- /// extraction so it works at roots without a vite-plus install (non-workspace
152- /// framework repos). The value must be a static string literal.
153- ///
154- /// `get_declared` keeps this to explicitly written fields: a config that is
155- /// unanalyzable or hides fields behind a spread simply falls through to the
156- /// picker/current-dir resolution instead of failing every bare app command.
157- fn resolve_default_package ( command : & str , cwd : & AbsolutePathBuf ) -> Option < AppTarget > {
152+ /// Resolve the `defaultPackage` value [`classify`] extracted from the
153+ /// invocation root's `vite.config.*` (static extraction, so it works at
154+ /// roots without a vite-plus install). The value must be a static string
155+ /// literal naming an existing directory.
156+ fn resolve_default_package (
157+ command : & str ,
158+ cwd : & AbsolutePathBuf ,
159+ value : vite_static_config:: FieldValue ,
160+ ) -> AppTarget {
158161 let fail = |msg : & str | {
159162 output:: error ( msg) ;
160- Some ( AppTarget :: Exit ( ExitStatus ( 1 ) ) )
163+ AppTarget :: Exit ( ExitStatus ( 1 ) )
161164 } ;
162- match vite_static_config :: resolve_static_config ( cwd ) . get_declared ( "defaultPackage" ) {
163- Some ( vite_static_config:: FieldValue :: Json ( serde_json:: Value :: String ( dir) ) ) => {
165+ match value {
166+ vite_static_config:: FieldValue :: Json ( serde_json:: Value :: String ( dir) ) => {
164167 let target = cwd. join ( & dir) . clean ( ) ;
165168 if !target. as_path ( ) . is_dir ( ) {
166169 return fail ( & format ! ( "defaultPackage points to a missing directory: {dir}" ) ) ;
167170 }
168171 output:: note ( & format ! ( "vp {command}: using {dir} (defaultPackage)" ) ) ;
169- Some ( AppTarget :: Dir ( target) )
172+ AppTarget :: Dir ( target)
170173 }
171- Some ( vite_static_config:: FieldValue :: Json ( other) ) => {
174+ vite_static_config:: FieldValue :: Json ( other) => {
172175 fail ( & format ! ( "defaultPackage must be a string of a directory, got: {other}" ) )
173176 }
174- Some ( vite_static_config:: FieldValue :: NonStatic ) => fail (
177+ vite_static_config:: FieldValue :: NonStatic => fail (
175178 "defaultPackage in vite.config.ts must be a static string literal so vp can read it without executing the config" ,
176179 ) ,
177- None => None ,
178180 }
179181}
180182
@@ -239,67 +241,68 @@ pub(super) fn needs_elicitation(
239241 subcommand : & SynthesizableSubcommand ,
240242 cwd : & AbsolutePathBuf ,
241243) -> bool {
242- let Some ( ( command, args) ) = app_command_parts ( subcommand) else {
243- return false ;
244- } ;
245- if !is_bare ( command, args) {
246- return false ;
247- }
248- let workspace = vite_workspace:: find_workspace_root ( cwd) ;
249- if at_invocation_root ( workspace. as_ref ( ) . ok ( ) . map ( |( _, rel) | rel. as_str ( ) ) )
250- && vite_static_config:: resolve_static_config ( cwd) . get_declared ( "defaultPackage" ) . is_some ( )
251- {
252- return true ;
253- }
254- let Ok ( ( workspace_root, rel_from_root) ) = workspace else {
255- return false ;
256- } ;
257- rel_from_root. as_str ( ) . is_empty ( )
258- && !matches ! ( workspace_root. workspace_file, WorkspaceFile :: NonWorkspacePackage ( _) )
244+ classify ( subcommand, cwd) . is_some ( )
259245}
260246
261- /// `defaultPackage` is a root-pointer concept: it applies where the
262- /// invocation directory is its own root (a workspace root, a standalone
263- /// package, or a framework directory with no package.json ancestry — pass
264- /// the workspace lookup's `rel_from_root`, or `None` when the lookup
265- /// failed). Below a workspace root the current directory already identifies
266- /// the target package, so a member's own config must not redirect.
267- fn at_invocation_root ( rel_from_root : Option < & str > ) -> bool {
268- rel_from_root. is_none_or ( str:: is_empty)
247+ /// Why a bare app command needs target elicitation.
248+ enum Elicitation {
249+ /// The invocation root's config explicitly declares `defaultPackage`
250+ /// (with this value — possibly invalid, which the resolver reports).
251+ DefaultPackage ( vite_static_config:: FieldValue ) ,
252+ /// Bare app command at a real workspace root: picker/listing territory.
253+ WorkspaceRoot ( vite_workspace:: WorkspaceRoot ) ,
269254}
270255
271- pub ( super ) fn resolve_app_target (
256+ /// The RFC's resolution order, written once for both entry points: bare app
257+ /// command, then `defaultPackage` at the invocation root, then the workspace
258+ /// root itself. `defaultPackage` is a root-pointer concept: it applies where
259+ /// the invocation directory is its own root (a workspace root, a standalone
260+ /// package, or a framework directory with no package.json ancestry); below a
261+ /// workspace root the current directory already identifies the target, so a
262+ /// member's own config must not redirect.
263+ fn classify (
272264 subcommand : & SynthesizableSubcommand ,
273265 cwd : & AbsolutePathBuf ,
274- ) -> Result < AppTarget , Error > {
275- let Some ( ( command, args) ) = app_command_parts ( subcommand) else {
276- return Ok ( AppTarget :: CurrentDir ) ;
277- } ;
266+ ) -> Option < ( & ' static str , Elicitation ) > {
267+ let ( command, args) = app_command_parts ( subcommand) ?;
278268 if !is_bare ( command, args) {
279- return Ok ( AppTarget :: CurrentDir ) ;
269+ return None ;
280270 }
281-
282- // `defaultPackage` is consulted before the workspace-shape dispatch (the
283- // non-workspace framework shape has no workspace metadata at all), but
284- // only at the invocation root: a member package's config must not
285- // redirect a command already running in that member.
286271 let workspace = vite_workspace:: find_workspace_root ( cwd) ;
287- if at_invocation_root ( workspace. as_ref ( ) . ok ( ) . map ( |( _, rel) | rel. as_str ( ) ) )
288- && let Some ( target) = resolve_default_package ( command, cwd)
272+ let at_invocation_root =
273+ workspace. as_ref ( ) . map_or ( true , |( _, rel_from_root) | rel_from_root. as_str ( ) . is_empty ( ) ) ;
274+ if at_invocation_root
275+ && let Some ( value) =
276+ vite_static_config:: resolve_static_config ( cwd) . get_declared ( "defaultPackage" )
289277 {
290- return Ok ( target ) ;
278+ return Some ( ( command , Elicitation :: DefaultPackage ( value ) ) ) ;
291279 }
292-
293- // The package listing needs workspace metadata; anything unresolvable
280+ // The picker/listing needs workspace metadata; anything unresolvable
294281 // keeps today's behavior (the caller surfaces its own workspace errors).
295282 let Ok ( ( workspace_root, rel_from_root) ) = workspace else {
296- return Ok ( AppTarget :: CurrentDir ) ;
283+ return None ;
297284 } ;
298285 if !rel_from_root. as_str ( ) . is_empty ( )
299286 || matches ! ( workspace_root. workspace_file, WorkspaceFile :: NonWorkspacePackage ( _) )
300287 {
301- return Ok ( AppTarget :: CurrentDir ) ;
288+ return None ;
302289 }
290+ Some ( ( command, Elicitation :: WorkspaceRoot ( workspace_root) ) )
291+ }
292+
293+ pub ( super ) fn resolve_app_target (
294+ subcommand : & SynthesizableSubcommand ,
295+ cwd : & AbsolutePathBuf ,
296+ ) -> Result < AppTarget , Error > {
297+ let Some ( ( command, elicitation) ) = classify ( subcommand, cwd) else {
298+ return Ok ( AppTarget :: CurrentDir ) ;
299+ } ;
300+ let workspace_root = match elicitation {
301+ Elicitation :: DefaultPackage ( value) => {
302+ return Ok ( resolve_default_package ( command, cwd, value) ) ;
303+ }
304+ Elicitation :: WorkspaceRoot ( workspace_root) => workspace_root,
305+ } ;
303306
304307 let graph =
305308 vite_workspace:: load_package_graph ( & workspace_root) . map_err ( |e| Error :: Anyhow ( e. into ( ) ) ) ?;
0 commit comments