diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/apps/web/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/apps/web/index.html
new file mode 100644
index 0000000000..ce6d6280c2
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/apps/web/index.html
@@ -0,0 +1,6 @@
+
+
+
+ web
+
+
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/apps/web/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/apps/web/package.json
new file mode 100644
index 0000000000..64630869c1
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/apps/web/package.json
@@ -0,0 +1 @@
+{ "name": "web", "private": true }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/package.json
new file mode 100644
index 0000000000..f23425bd85
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "per-command-config",
+ "type": "module",
+ "private": true,
+ "workspaces": [
+ "apps/*",
+ "packages/*"
+ ]
+}
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/packages/ui/package.json
new file mode 100644
index 0000000000..dd63f88e1f
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/packages/ui/package.json
@@ -0,0 +1 @@
+{ "name": "ui", "private": true }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/packages/ui/src/index.ts
new file mode 100644
index 0000000000..f538469c7d
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/packages/ui/src/index.ts
@@ -0,0 +1 @@
+export const ui = 'ui';
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/vite.config.ts
new file mode 100644
index 0000000000..754c5f8e4d
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command/vite.config.ts
@@ -0,0 +1,3 @@
+export default {
+ defaultPackage: { build: './apps/web', pack: './packages/ui' },
+};
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/index.html
new file mode 100644
index 0000000000..31acf91c54
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/index.html
@@ -0,0 +1,6 @@
+
+
+
+ root app
+
+
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/package.json
new file mode 100644
index 0000000000..eb32387cc7
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/package.json
@@ -0,0 +1 @@
+{ "name": "per-command-fallthrough", "type": "module", "private": true, "workspaces": ["packages/*"] }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/packages/ui/package.json
new file mode 100644
index 0000000000..dd63f88e1f
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/packages/ui/package.json
@@ -0,0 +1 @@
+{ "name": "ui", "private": true }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/packages/ui/src/index.ts
new file mode 100644
index 0000000000..f538469c7d
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/packages/ui/src/index.ts
@@ -0,0 +1 @@
+export const ui = 'ui';
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/vite.config.ts
new file mode 100644
index 0000000000..f46068d2cc
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/per_command_fallthrough/vite.config.ts
@@ -0,0 +1,3 @@
+export default {
+ defaultPackage: { pack: './packages/ui' },
+};
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml
index b8e0be78a5..d52a2c6520 100644
--- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml
@@ -59,3 +59,30 @@ defaultPackage inside `defineConfig({ ... } satisfies UserConfig)` is also
honored: the wrapper on the defineConfig argument is unwrapped too.
"""
steps = [{ argv = ["vp", "build"], cwd = "dc_wrapper" }]
+
+[[case]]
+name = "default_package_per_command"
+vp = ["local", "global"]
+comment = """
+The object form maps commands individually at a workspace root: `vp build`
+targets ./apps/web while `vp pack` targets ./packages/ui, so one monorepo
+can dev an app and pack a library (rfcs/cwd-flag.md).
+"""
+steps = [
+ { argv = ["vp", "build"], cwd = "per_command" },
+ { argv = ["vp", "pack"], cwd = "per_command" },
+]
+
+[[case]]
+name = "default_package_per_command_fallthrough"
+vp = ["local", "global"]
+comment = """
+A command absent from the defaultPackage object falls through to the
+normal resolution: the map only declares `pack`, so bare `vp build` at
+this workspace root runs in place at the (runnable) root with no note,
+while `vp pack` still routes to the declared ./packages/ui.
+"""
+steps = [
+ { argv = ["vp", "build"], cwd = "per_command_fallthrough" },
+ { argv = ["vp", "pack"], cwd = "per_command_fallthrough" },
+]
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command.global.md
new file mode 100644
index 0000000000..a56291d0b5
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command.global.md
@@ -0,0 +1,31 @@
+# default_package_per_command
+
+The object form maps commands individually at a workspace root: `vp build`
+targets ./apps/web while `vp pack` targets ./packages/ui, so one monorepo
+can dev an app and pack a library (rfcs/cwd-flag.md).
+
+## `cd per_command && vp build`
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+note: vp build: using ./apps/web (defaultPackage in vite.config.ts)
+✓ 2 modules transformed.
+computing gzip size...
+dist/index.html kB │ gzip: kB
+
+✓ built in
+```
+
+## `cd per_command && vp pack`
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+note: vp pack: using ./packages/ui (defaultPackage in vite.config.ts)
+ℹ entry: src/index.ts
+ℹ Build start
+ℹ dist/index.mjs kB │ gzip: kB
+ℹ 1 files, total: kB
+✔ Build complete in
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command.local.md
new file mode 100644
index 0000000000..43141d909f
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command.local.md
@@ -0,0 +1,27 @@
+# default_package_per_command
+
+The object form maps commands individually at a workspace root: `vp build`
+targets ./apps/web while `vp pack` targets ./packages/ui, so one monorepo
+can dev an app and pack a library (rfcs/cwd-flag.md).
+
+## `cd per_command && vp build`
+
+```
+note: vp build: using ./apps/web (defaultPackage in vite.config.ts)
+✓ 2 modules transformed.
+computing gzip size...
+dist/index.html kB │ gzip: kB
+
+✓ built in
+```
+
+## `cd per_command && vp pack`
+
+```
+note: vp pack: using ./packages/ui (defaultPackage in vite.config.ts)
+ℹ entry: src/index.ts
+ℹ Build start
+ℹ dist/index.mjs kB │ gzip: kB
+ℹ 1 files, total: kB
+✔ Build complete in
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command_fallthrough.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command_fallthrough.global.md
new file mode 100644
index 0000000000..00ec553fbd
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command_fallthrough.global.md
@@ -0,0 +1,31 @@
+# default_package_per_command_fallthrough
+
+A command absent from the defaultPackage object falls through to the
+normal resolution: the map only declares `pack`, so bare `vp build` at
+this workspace root runs in place at the (runnable) root with no note,
+while `vp pack` still routes to the declared ./packages/ui.
+
+## `cd per_command_fallthrough && vp build`
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+✓ 2 modules transformed.
+computing gzip size...
+dist/index.html kB │ gzip: kB
+
+✓ built in
+```
+
+## `cd per_command_fallthrough && vp pack`
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+note: vp pack: using ./packages/ui (defaultPackage in vite.config.ts)
+ℹ entry: src/index.ts
+ℹ Build start
+ℹ dist/index.mjs kB │ gzip: kB
+ℹ 1 files, total: kB
+✔ Build complete in
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command_fallthrough.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command_fallthrough.local.md
new file mode 100644
index 0000000000..cd4a43cebe
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package_per_command_fallthrough.local.md
@@ -0,0 +1,27 @@
+# default_package_per_command_fallthrough
+
+A command absent from the defaultPackage object falls through to the
+normal resolution: the map only declares `pack`, so bare `vp build` at
+this workspace root runs in place at the (runnable) root with no note,
+while `vp pack` still routes to the declared ./packages/ui.
+
+## `cd per_command_fallthrough && vp build`
+
+```
+✓ 2 modules transformed.
+computing gzip size...
+dist/index.html kB │ gzip: kB
+
+✓ built in
+```
+
+## `cd per_command_fallthrough && vp pack`
+
+```
+note: vp pack: using ./packages/ui (defaultPackage in vite.config.ts)
+ℹ entry: src/index.ts
+ℹ Build start
+ℹ dist/index.mjs kB │ gzip: kB
+ℹ 1 files, total: kB
+✔ Build complete in
+```
diff --git a/docs/config/index.md b/docs/config/index.md
index 97e51c8ef9..bfab9ef6c5 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -45,4 +45,4 @@ export default {
};
```
-vp reads the value without executing the config, so `defaultPackage` also works at repository roots with no vite-plus dependency (for example a Laravel or Rails repo whose Vite app lives in `frontend/`, with vite-plus installed only there). That static read is why the value must stay a plain string literal rather than an expression. An explicit `-C` or positional target always wins over the config.
+vp reads the values without executing the config, so `defaultPackage` also works at repository roots with no vite-plus dependency (for example a Laravel or Rails repo whose Vite app lives in `frontend/`, with vite-plus installed only there). That static read is why the values must stay plain string literals rather than expressions. An explicit `-C` or positional target always wins over the config.
diff --git a/docs/guide/monorepo.md b/docs/guide/monorepo.md
index e040d32d8f..60caef9ae8 100644
--- a/docs/guide/monorepo.md
+++ b/docs/guide/monorepo.md
@@ -232,6 +232,14 @@ note: vp dev: using ./apps/web (defaultPackage in vite.config.ts)
This is the right choice for framework monorepos that are not JavaScript workspaces, such as a Laravel or Rails app with a `frontend/` directory: there is no package list to resolve, so `defaultPackage` points vp straight at the app. Because vp reads it without executing the config, it works even when `vite-plus` is installed only inside that subdirectory.
+An object form maps commands individually, so `vp pack` can target a library while `vp dev` targets an app; a command absent from the object falls through to the resolution above:
+
+```ts [vite.config.ts]
+export default {
+ defaultPackage: { dev: './apps/web', pack: './packages/ui' },
+};
+```
+
### Package scripts and workspace-wide tasks
Keep package-specific scripts in each package when the command differs per app:
diff --git a/packages/cli/binding/src/cli/app_target.rs b/packages/cli/binding/src/cli/app_target.rs
index 283f7582df..ff361f45a4 100644
--- a/packages/cli/binding/src/cli/app_target.rs
+++ b/packages/cli/binding/src/cli/app_target.rs
@@ -332,6 +332,24 @@ enum Elicitation {
WorkspaceRoot(vite_workspace::WorkspaceRoot),
}
+/// Applies a `defaultPackage` declaration to one command. A string covers
+/// all four app commands; an object maps commands individually
+/// (`{ dev: './apps/web', pack: './packages/ui' }`), and a command absent
+/// from the object falls through to the picker/listing resolution. Every
+/// other shape (a non-string, a non-static value) passes through for
+/// [`resolve_default_package`] to report.
+fn default_package_for_command(
+ command: &str,
+ value: vite_static_config::FieldValue,
+) -> Option {
+ match value {
+ vite_static_config::FieldValue::Json(serde_json::Value::Object(map)) => {
+ map.get(command).cloned().map(vite_static_config::FieldValue::Json)
+ }
+ other => Some(other),
+ }
+}
+
/// The RFC's resolution order, written once for both entry points: bare app
/// command, then `defaultPackage` at the invocation root, then the workspace
/// root itself. `defaultPackage` is a root-pointer concept: it applies where
@@ -356,8 +374,10 @@ fn classify(subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath) -> Classif
// Resolved once and reused by `root_looks_runnable` below, so a bare
// command at a root reads and parses the config a single time.
let root_config = at_invocation_root.then(|| vite_static_config::resolve_static_config(cwd));
- if let Some(value) =
- root_config.as_ref().and_then(|config| config.get_declared("defaultPackage"))
+ if let Some(value) = root_config
+ .as_ref()
+ .and_then(|config| config.get_declared("defaultPackage"))
+ .and_then(|value| default_package_for_command(command, value))
{
return Classification::Elicit(command, Elicitation::DefaultPackage(value));
}
diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts
index dada43d187..0426dc277f 100644
--- a/packages/cli/src/define-config.ts
+++ b/packages/cli/src/define-config.ts
@@ -56,12 +56,14 @@ declare module '@voidzero-dev/vite-plus-core' {
/**
* Default target directory for `vp dev` / `build` / `preview` / `pack`
* when invoked bare in the directory containing this config (an implicit
- * `vp -C `). Relative to the config file's directory. vp reads the
- * value without executing the config, so it also works at roots with no
- * vite-plus dependency; the static read is why it must stay a plain
- * string literal.
+ * `vp -C `). A string targets all four commands; an object maps
+ * commands individually, and a command absent from the object falls
+ * through to the normal picker/listing resolution. Paths are relative to
+ * the config file's directory. vp reads them without executing the
+ * config, so this also works at roots with no vite-plus dependency; the
+ * static read is why the values must stay plain string literals.
*/
- defaultPackage?: string;
+ defaultPackage?: string | { dev?: string; build?: string; preview?: string; pack?: string };
run?: RunConfig;
diff --git a/rfcs/cwd-flag.md b/rfcs/cwd-flag.md
index 1fc784137e..6b9faaeabf 100644
--- a/rfcs/cwd-flag.md
+++ b/rfcs/cwd-flag.md
@@ -309,7 +309,7 @@ export default defineConfig({
});
```
-- Type: `string`, a single directory. A per-command map can come later if real demand appears.
+- Type: `string` (one directory for all four commands) or a per-command object (`{ dev: './apps/web', pack: './packages/ui' }`, added on review demand). A command absent from the object falls through to the picker/listing resolution.
- Consulted when a bare app command runs in the directory containing the root config: a workspace root, or a non-workspace repo root. The non-workspace shape has no package list, so `defaultPackage` is the only mechanism that covers it. An explicit `-C` always wins.
- A missing directory errors: `defaultPackage points to a missing directory: ./frontend`.
- Read via static extraction (`vite_static_config` + the loader in `packages/cli/binding/src/cli/handler.rs`), like `run` config. At a non-workspace root there is no install to execute the config, so the file must work unexecuted: a plain default-export object with a static string value.