From 180e5bc746cda0cf1ec0e1dfa4ebf852d42301d8 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sat, 7 Nov 2026 22:05:00 +0800 Subject: [PATCH 1/4] feat(task-graph): add run.scripts options for package.json scripts Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + crates/vt_graph/run-config.ts | 35 ++- crates/vt_graph/src/config/mod.rs | 6 +- crates/vt_graph/src/config/user.rs | 24 +- crates/vt_graph/src/lib.rs | 6 +- crates/vt_graph/src/task_loader.rs | 20 +- .../snapshots/task_graph_load_error.snap | 2 +- .../fixtures/script_options/package.json | 12 + .../fixtures/script_options/snapshots.toml | 33 ++ ...ok_uses_depends_on_when_run_directly.jsonc | 58 ++++ ...ignore_depends_on_applies_to_scripts.jsonc | 55 ++++ ...ipt_cache_false_wins_over_cache_flag.jsonc | 29 ++ ...t_uses_cache_options_with_cache_flag.jsonc | 144 +++++++++ .../snapshots/query_script_uses_cwd.jsonc | 29 ++ ...s_depends_on_and_hooks_without_cache.jsonc | 84 ++++++ .../script_options/snapshots/task_graph.md | 281 ++++++++++++++++++ .../fixtures/script_options/src/.gitkeep | 0 .../fixtures/script_options/vite-task.json | 11 + .../package.json | 6 + .../snapshots/task_graph_load_error.snap | 1 + .../vite-task.json | 9 + 21 files changed, 835 insertions(+), 11 deletions(-) create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/package.json create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots.toml create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_hook_uses_depends_on_when_run_directly.jsonc create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_ignore_depends_on_applies_to_scripts.jsonc create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_cache_false_wins_over_cache_flag.jsonc create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cache_options_with_cache_flag.jsonc create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cwd.jsonc create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_depends_on_and_hooks_without_cache.jsonc create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/task_graph.md create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/src/.gitkeep create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/vite-task.json create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/package.json create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/snapshots/task_graph_load_error.snap create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/vite-task.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5cfa856..05c94e153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Added** `run.scripts` adds `dependsOn`, `cache` and `cwd` options to a `package.json` script without a task of a different name. The script keeps its command and its `preX`/`postX` hooks, and `cache.scripts` or `--cache` still decides whether it's cached ([#???](https://github.com/voidzero-dev/vite-task/pull/???)). - **Fixed** On Windows, tasks with automatic file-access tracking can now run Git Bash's `sh` and other MSYS2 or Cygwin programs, which aborted with `fatal error - MapViewOfFileEx 'shared.5'(…) Win32 error 487` ([#830](https://github.com/voidzero-dev/vite-task/issues/830), [#952](https://github.com/voidzero-dev/vite-task/pull/952)). - **Fixed** On Linux, cached tasks no longer fail to spawn child processes in restricted sandboxes, for example rootless bubblewrap that denies the `seccomp` syscall. When the `seccomp` filter for file-access tracking cannot be installed, the process runs untracked instead and the run is reported as not cached. The not-cached message now reads "automatic file tracking couldn't record everything this task did", which fits both this case and a full tracking channel ([#700](https://github.com/voidzero-dev/vite-task/issues/700), [#701](https://github.com/voidzero-dev/vite-task/pull/701)). - **Fixed** `vp run ` in a package that doesn't define the task now reports `Task "" not found` when another package's `vite.config.*` fails to load, instead of failing with that package's error. Showing the tasks to choose from loads every package's config, so the task selector or task list is skipped then ([#987](https://github.com/voidzero-dev/vite-task/pull/987)). diff --git a/crates/vt_graph/run-config.ts b/crates/vt_graph/run-config.ts index b32282aef..61c917345 100644 --- a/crates/vt_graph/run-config.ts +++ b/crates/vt_graph/run-config.ts @@ -91,12 +91,37 @@ output?: Array, }; export type TaskDefinition = Task | Command; +export type TaskOptions = { +/** + * The working directory for the task, relative to the package root (not workspace root). + */ +cwd?: string, +/** + * Tasks that must run before this task. + * + * - A string runs one named task, such as `"build"` in the same package or + * `"package-name#build"` in another package. + * - An object runs a task in direct workspace dependency packages selected + * from package.json fields. For example, + * `{ "task": "build", "from": "dependencies" }` runs `build` in each + * direct workspace dependency that defines a `build` task. + */ +dependsOn?: Array, +/** + * Whether and how to cache the task. + * + * - Omitted or `true`: caching enabled with default settings (same as `{}`) + * - `false`: caching disabled + * - Object: caching enabled with the given settings + */ +cache?: TaskCache, }; + export type UserGlobalCacheConfig = boolean | { /** * Enable caching for package.json scripts not defined in the `tasks` map. * * When `false`, package.json scripts will not be cached. - * When `true`, package.json scripts will be cached with default settings. + * When `true`, package.json scripts will be cached, using their `scripts` options if any. * * Default: `false` */ @@ -135,6 +160,14 @@ cache?: UserGlobalCacheConfig, * Task definitions: full task objects, command strings, or command string arrays. */ tasks?: { [key in string]: TaskDefinition }, +/** + * Options for `package.json` scripts, by script name. + * + * The command stays in `package.json`, and the script stays a script: the root `cache` + * setting's `scripts` field decides whether it's cached, and its `preX`/`postX` hooks + * still run. When a script runs as another script's hook, its `dependsOn` doesn't apply. + */ +scripts?: { [key in string]: TaskOptions }, /** * Whether to automatically run `preX`/`postX` package.json scripts as * lifecycle hooks when script `X` is executed. diff --git a/crates/vt_graph/src/config/mod.rs b/crates/vt_graph/src/config/mod.rs index 3f2eecff3..55b14480b 100644 --- a/crates/vt_graph/src/config/mod.rs +++ b/crates/vt_graph/src/config/mod.rs @@ -398,9 +398,8 @@ pub enum ResolveTaskConfigError { } impl ResolvedTaskConfig { - /// Resolve from package.json script only (no config entry for this task). + /// Resolves from a package.json script and its options in the `scripts` map. /// - /// Always resolves with caching enabled (default settings). /// The global cache config is applied at plan time, not here. /// /// # Errors @@ -409,13 +408,14 @@ impl ResolvedTaskConfig { pub fn resolve_package_json_script( package_dir: &Arc, package_json_script: &str, + options: UserTaskOptions, workspace_root: &AbsolutePath, glob_resolver: &GlobResolver, ) -> Result { Ok(Self { commands: vec![package_json_script.into()].into(), resolved_options: ResolvedTaskOptions::resolve( - UserTaskOptions::default(), + options, package_dir, workspace_root, glob_resolver, diff --git a/crates/vt_graph/src/config/user.rs b/crates/vt_graph/src/config/user.rs index 92f00a2ef..18641b8a0 100644 --- a/crates/vt_graph/src/config/user.rs +++ b/crates/vt_graph/src/config/user.rs @@ -259,8 +259,8 @@ pub struct EnabledCacheConfig { /// Options for user-defined tasks in `vite.config.*`, excluding the command. #[derive(Debug, Deserialize, PartialEq, Eq)] // TS derive macro generates code using std types that clippy disallows; skip derive during linting -#[cfg_attr(all(test, not(clippy)), derive(TS), ts(optional_fields))] -#[serde(rename_all = "camelCase")] +#[cfg_attr(all(test, not(clippy)), derive(TS), ts(optional_fields, rename = "TaskOptions"))] +#[serde(deny_unknown_fields, rename_all = "camelCase")] pub struct UserTaskOptions { /// The working directory for the task, relative to the package root (not workspace root). #[serde(rename = "cwd")] @@ -378,7 +378,7 @@ pub enum UserGlobalCacheConfig { /// Enable caching for package.json scripts not defined in the `tasks` map. /// /// When `false`, package.json scripts will not be cached. - /// When `true`, package.json scripts will be cached with default settings. + /// When `true`, package.json scripts will be cached, using their `scripts` options if any. /// /// Default: `false` scripts: Option, @@ -460,6 +460,13 @@ pub struct UserRunConfig { /// Task definitions: full task objects, command strings, or command string arrays. pub tasks: Option>, + /// Options for `package.json` scripts, by script name. + /// + /// The command stays in `package.json`, and the script stays a script: the root `cache` + /// setting's `scripts` field decides whether it's cached, and its `preX`/`postX` hooks + /// still run. When a script runs as another script's hook, its `dependsOn` doesn't apply. + pub scripts: Option>, + /// Whether to automatically run `preX`/`postX` package.json scripts as /// lifecycle hooks when script `X` is executed. /// @@ -572,6 +579,17 @@ mod tests { use super::*; + #[test] + fn test_script_options_reject_task_only_fields() { + for script_options in [json!({ "command": "echo test" }), json!({ "env": ["NODE_ENV"] })] { + let user_config_json = json!({ "scripts": { "build": script_options } }); + assert!( + serde_json::from_value::(user_config_json.clone()).is_err(), + "{user_config_json}" + ); + } + } + #[test] fn test_command_required() { let user_config_json = json!({}); diff --git a/crates/vt_graph/src/lib.rs b/crates/vt_graph/src/lib.rs index 78255d7a6..b1e65f2aa 100644 --- a/crates/vt_graph/src/lib.rs +++ b/crates/vt_graph/src/lib.rs @@ -74,10 +74,14 @@ pub enum TaskGraphLoadError { #[error( "Task {task_display} conflicts with a package.json script of the same name. \ - Remove the script from package.json or rename the task" + Remove the script from package.json, rename the task, or configure the script in \ + `run.scripts`" )] ScriptConflict { task_display: TaskDisplay }, + #[error("`run.scripts` configures {task_display}, but package.json has no such script")] + ScriptNotFound { task_display: TaskDisplay }, + #[error("Failed to resolve task config for task {task_display}")] ResolveConfigError { task_display: TaskDisplay, diff --git a/crates/vt_graph/src/task_loader.rs b/crates/vt_graph/src/task_loader.rs index ae832806d..b19d7ca7f 100644 --- a/crates/vt_graph/src/task_loader.rs +++ b/crates/vt_graph/src/task_loader.rs @@ -125,6 +125,10 @@ impl Loader for TaskLoader<'_> { key.package_index } + #[expect( + clippy::too_many_lines, + reason = "sequential loading steps are clearer in one function" + )] async fn load( &self, package_index: PackageNodeIndex, @@ -219,11 +223,16 @@ impl Loader for TaskLoader<'_> { tasks.push((task_node, depends_on)); } - // For remaining package.json scripts not in the tasks map, create tasks with default config + // For remaining package.json scripts not in the tasks map, create tasks with their + // options from the scripts map, or default options + let mut script_options = user_config.scripts.unwrap_or_default(); for (script_name, package_json_script) in package_json_scripts { + let options = script_options.remove(script_name).unwrap_or_default(); + let depends_on = options.depends_on.clone(); let resolved_config = ResolvedTaskConfig::resolve_package_json_script( &package_dir, package_json_script, + options, &self.workspace_path, &self.glob_resolver, ) @@ -237,7 +246,14 @@ impl Loader for TaskLoader<'_> { resolved_config, source: TaskSource::PackageJsonScript, }; - tasks.push((task_node, None)); + tasks.push((task_node, depends_on)); + } + + // `min` keeps the error stable when several entries have no script + if let Some(script_name) = script_options.into_keys().min() { + return Err(TaskGraphLoadError::ScriptNotFound { + task_display: task_display(script_name), + }); } top_level_cache_fields.finish().map_err(TaskGraphLoadError::TopLevelCacheFields)?; diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_conflict/snapshots/task_graph_load_error.snap b/crates/vt_plan/tests/plan_snapshots/fixtures/script_conflict/snapshots/task_graph_load_error.snap index e756b946e..0c9569829 100644 --- a/crates/vt_plan/tests/plan_snapshots/fixtures/script_conflict/snapshots/task_graph_load_error.snap +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_conflict/snapshots/task_graph_load_error.snap @@ -1 +1 @@ -Task @test/script-conflict#build conflicts with a package.json script of the same name. Remove the script from package.json or rename the task \ No newline at end of file +Task @test/script-conflict#build conflicts with a package.json script of the same name. Remove the script from package.json, rename the task, or configure the script in `run.scripts` \ No newline at end of file diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/package.json b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/package.json new file mode 100644 index 000000000..5aabdbfb3 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/package.json @@ -0,0 +1,12 @@ +{ + "name": "@test/script-options", + "scripts": { + "build": "vtt print build", + "prebuild": "vtt print prebuild", + "postbuild": "vtt print postbuild", + "lint": "vtt print lint", + "setup": "vtt print setup", + "deploy": "vtt print deploy", + "where": "vtt print where" + } +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots.toml b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots.toml new file mode 100644 index 000000000..c9e1c9828 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots.toml @@ -0,0 +1,33 @@ +# Tests options for package.json scripts in the `scripts` map. + +[fields] +key = true +neighbors = true +execution_item_display = { command = true, cwd = true } +cache_metadata = { spawn_fingerprint = { env_fingerprints = true }, input_config = true } + +# `cache.scripts` still decides caching, and hooks still run +[[plan]] +name = "script_uses_depends_on_and_hooks_without_cache" +args = ["run", "build"] + +[[plan]] +name = "script_uses_cache_options_with_cache_flag" +args = ["run", "--cache", "build"] +env = { APP_ENV = "production" } + +[[plan]] +name = "script_cache_false_wins_over_cache_flag" +args = ["run", "--cache", "deploy"] + +[[plan]] +name = "ignore_depends_on_applies_to_scripts" +args = ["run", "--ignore-depends-on", "build"] + +[[plan]] +name = "hook_uses_depends_on_when_run_directly" +args = ["run", "prebuild"] + +[[plan]] +name = "script_uses_cwd" +args = ["run", "where"] diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_hook_uses_depends_on_when_run_directly.jsonc b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_hook_uses_depends_on_when_run_directly.jsonc new file mode 100644 index 000000000..3de8885b0 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_hook_uses_depends_on_when_run_directly.jsonc @@ -0,0 +1,58 @@ +// run prebuild +{ + "graph": [ + { + "key": [ + "/", + "prebuild" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print prebuild", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + } + ] + }, + "neighbors": [ + [ + "/", + "setup" + ] + ] + }, + { + "key": [ + "/", + "setup" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print setup", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + } + ] + }, + "neighbors": [] + } + ] +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_ignore_depends_on_applies_to_scripts.jsonc b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_ignore_depends_on_applies_to_scripts.jsonc new file mode 100644 index 000000000..51f29d0f6 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_ignore_depends_on_applies_to_scripts.jsonc @@ -0,0 +1,55 @@ +// run --ignore-depends-on build +{ + "graph": [ + { + "key": [ + "/", + "build" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print prebuild", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + }, + { + "execution_item_display": { + "command": "vtt print build", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + }, + { + "execution_item_display": { + "command": "vtt print postbuild", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + } + ] + }, + "neighbors": [] + } + ] +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_cache_false_wins_over_cache_flag.jsonc b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_cache_false_wins_over_cache_flag.jsonc new file mode 100644 index 000000000..c5e48991c --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_cache_false_wins_over_cache_flag.jsonc @@ -0,0 +1,29 @@ +// run --cache deploy +{ + "graph": [ + { + "key": [ + "/", + "deploy" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print deploy", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + } + ] + }, + "neighbors": [] + } + ] +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cache_options_with_cache_flag.jsonc b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cache_options_with_cache_flag.jsonc new file mode 100644 index 000000000..13c12cf04 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cache_options_with_cache_flag.jsonc @@ -0,0 +1,144 @@ +// run --cache build +{ + "graph": [ + { + "key": [ + "/", + "build" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print prebuild", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + } + } + }, + { + "execution_item_display": { + "command": "vtt print build", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "env_fingerprints": { + "fingerprinted_envs": { + "APP_ENV": "sha256:ab8e18ef4ebebeddc0b3152ce9c9006e14fc05242e3fc9ce32246ea6a9543074" + }, + "untracked_env_config": [ + "" + ] + } + }, + "input_config": { + "includes_auto": false, + "positive_globs": [ + "src/**" + ], + "negative_globs": [] + } + } + } + } + } + }, + { + "execution_item_display": { + "command": "vtt print postbuild", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + } + } + } + ] + }, + "neighbors": [ + [ + "/", + "lint" + ] + ] + }, + { + "key": [ + "/", + "lint" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print lint", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + } + } + } + ] + }, + "neighbors": [] + } + ] +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cwd.jsonc b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cwd.jsonc new file mode 100644 index 000000000..4b2d7e393 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_cwd.jsonc @@ -0,0 +1,29 @@ +// run where +{ + "graph": [ + { + "key": [ + "/", + "where" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print where", + "cwd": "/src" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + } + ] + }, + "neighbors": [] + } + ] +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_depends_on_and_hooks_without_cache.jsonc b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_depends_on_and_hooks_without_cache.jsonc new file mode 100644 index 000000000..67fedab0d --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/query_script_uses_depends_on_and_hooks_without_cache.jsonc @@ -0,0 +1,84 @@ +// run build +{ + "graph": [ + { + "key": [ + "/", + "build" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print prebuild", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + }, + { + "execution_item_display": { + "command": "vtt print build", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + }, + { + "execution_item_display": { + "command": "vtt print postbuild", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + } + ] + }, + "neighbors": [ + [ + "/", + "lint" + ] + ] + }, + { + "key": [ + "/", + "lint" + ], + "node": { + "items": [ + { + "execution_item_display": { + "command": "vtt print lint", + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null + } + } + } + } + ] + }, + "neighbors": [] + } + ] +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/task_graph.md b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/task_graph.md new file mode 100644 index 000000000..eba8deaf1 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/snapshots/task_graph.md @@ -0,0 +1,281 @@ +# task graph + +```mermaid +flowchart TD + task_0["/#build"] + task_0 --> task_2 + task_1["/#deploy"] + task_2["/#lint"] + task_3["/#postbuild"] + task_4["/#prebuild"] + task_4 --> task_5 + task_5["/#setup"] + task_6["/#where"] +``` + +## `/#build` + +```json +{ + "task_display": { + "package_name": "@test/script-options", + "task_name": "build", + "package_path": "/" + }, + "resolved_config": { + "commands": [ + "vtt print build" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "remote_cache_allowed": true, + "env_config": { + "fingerprinted_envs": [ + "APP_ENV" + ], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": false, + "positive_globs": [ + "src/**" + ], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" +} +``` + +## `/#deploy` + +```json +{ + "task_display": { + "package_name": "@test/script-options", + "task_name": "deploy", + "package_path": "/" + }, + "resolved_config": { + "commands": [ + "vtt print deploy" + ], + "resolved_options": { + "cwd": "/", + "cache_config": null + } + }, + "source": "PackageJsonScript" +} +``` + +## `/#lint` + +```json +{ + "task_display": { + "package_name": "@test/script-options", + "task_name": "lint", + "package_path": "/" + }, + "resolved_config": { + "commands": [ + "vtt print lint" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "remote_cache_allowed": true, + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" +} +``` + +## `/#postbuild` + +```json +{ + "task_display": { + "package_name": "@test/script-options", + "task_name": "postbuild", + "package_path": "/" + }, + "resolved_config": { + "commands": [ + "vtt print postbuild" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "remote_cache_allowed": true, + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" +} +``` + +## `/#prebuild` + +```json +{ + "task_display": { + "package_name": "@test/script-options", + "task_name": "prebuild", + "package_path": "/" + }, + "resolved_config": { + "commands": [ + "vtt print prebuild" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "remote_cache_allowed": true, + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" +} +``` + +## `/#setup` + +```json +{ + "task_display": { + "package_name": "@test/script-options", + "task_name": "setup", + "package_path": "/" + }, + "resolved_config": { + "commands": [ + "vtt print setup" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "remote_cache_allowed": true, + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" +} +``` + +## `/#where` + +```json +{ + "task_display": { + "package_name": "@test/script-options", + "task_name": "where", + "package_path": "/" + }, + "resolved_config": { + "commands": [ + "vtt print where" + ], + "resolved_options": { + "cwd": "/src", + "cache_config": { + "remote_cache_allowed": true, + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" +} +``` + diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/src/.gitkeep b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/src/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/vite-task.json b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/vite-task.json new file mode 100644 index 000000000..0e081d6df --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/vite-task.json @@ -0,0 +1,11 @@ +{ + "scripts": { + "build": { + "dependsOn": ["lint"], + "cache": { "env": ["APP_ENV"], "input": ["src/**"] } + }, + "prebuild": { "dependsOn": ["setup"] }, + "deploy": { "cache": false }, + "where": { "cwd": "src" } + } +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/package.json b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/package.json new file mode 100644 index 000000000..c0f57ed91 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/package.json @@ -0,0 +1,6 @@ +{ + "name": "@test/script-options-script-not-found", + "scripts": { + "build": "echo build" + } +} diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/snapshots/task_graph_load_error.snap b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/snapshots/task_graph_load_error.snap new file mode 100644 index 000000000..7f957de46 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/snapshots/task_graph_load_error.snap @@ -0,0 +1 @@ +`run.scripts` configures @test/script-options-script-not-found#lint, but package.json has no such script \ No newline at end of file diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/vite-task.json b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/vite-task.json new file mode 100644 index 000000000..0531dd4cd --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options_script_not_found/vite-task.json @@ -0,0 +1,9 @@ +{ + "tasks": { + "lint": { "command": "echo lint" } + }, + "scripts": { + "test": {}, + "lint": {} + } +} From 759b008fe1f694daddbf0bc020dfcdc85e5551ab Mon Sep 17 00:00:00 2001 From: SerKo Date: Sat, 7 Nov 2026 22:26:14 +0800 Subject: [PATCH 2/4] feat(plan): warn when a hook's dependsOn is ignored Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 2 +- .../script_hook_depends_on/package.json | 10 +++++++++ .../script_hook_depends_on/snapshots.toml | 20 ++++++++++++++++++ ...irectly_uses_depends_on_without_warning.md | 16 ++++++++++++++ .../hooks_warn_that_depends_on_is_ignored.md | 21 +++++++++++++++++++ .../ignore_depends_on_does_not_warn.md | 19 +++++++++++++++++ .../script_hook_depends_on/vite-task.json | 6 ++++++ crates/vt_plan/src/plan.rs | 21 +++++++++++++++++++ 8 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/package.json create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots.toml create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hook_run_directly_uses_depends_on_without_warning.md create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hooks_warn_that_depends_on_is_ignored.md create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/ignore_depends_on_does_not_warn.md create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/vite-task.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c94e153..76c4d7ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -- **Added** `run.scripts` adds `dependsOn`, `cache` and `cwd` options to a `package.json` script without a task of a different name. The script keeps its command and its `preX`/`postX` hooks, and `cache.scripts` or `--cache` still decides whether it's cached ([#???](https://github.com/voidzero-dev/vite-task/pull/???)). +- **Added** `run.scripts` adds `dependsOn`, `cache` and `cwd` options to a `package.json` script without a task of a different name. The script keeps its command and its `preX`/`postX` hooks, and `cache.scripts` or `--cache` still decides whether it's cached. A script's `dependsOn` is ignored when it runs as another script's hook, and `vp run` warns about it ([#???](https://github.com/voidzero-dev/vite-task/pull/???)). - **Fixed** On Windows, tasks with automatic file-access tracking can now run Git Bash's `sh` and other MSYS2 or Cygwin programs, which aborted with `fatal error - MapViewOfFileEx 'shared.5'(…) Win32 error 487` ([#830](https://github.com/voidzero-dev/vite-task/issues/830), [#952](https://github.com/voidzero-dev/vite-task/pull/952)). - **Fixed** On Linux, cached tasks no longer fail to spawn child processes in restricted sandboxes, for example rootless bubblewrap that denies the `seccomp` syscall. When the `seccomp` filter for file-access tracking cannot be installed, the process runs untracked instead and the run is reported as not cached. The not-cached message now reads "automatic file tracking couldn't record everything this task did", which fits both this case and a full tracking channel ([#700](https://github.com/voidzero-dev/vite-task/issues/700), [#701](https://github.com/voidzero-dev/vite-task/pull/701)). - **Fixed** `vp run ` in a package that doesn't define the task now reports `Task "" not found` when another package's `vite.config.*` fails to load, instead of failing with that package's error. Showing the tasks to choose from loads every package's config, so the task selector or task list is skipped then ([#987](https://github.com/voidzero-dev/vite-task/pull/987)). diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/package.json new file mode 100644 index 000000000..9fe5b8c05 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/script-hook-depends-on", + "private": true, + "scripts": { + "setup": "vtt print setup", + "prebuild": "vtt print prebuild", + "build": "vtt print build", + "postbuild": "vtt print postbuild" + } +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots.toml new file mode 100644 index 000000000..69626aa7e --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots.toml @@ -0,0 +1,20 @@ +[[e2e]] +name = "hooks_warn_that_depends_on_is_ignored" +comment = """ +`dependsOn` of a pre or post hook doesn't apply when the hook runs inside its script, so `setup` doesn't run and each hook warns. +""" +steps = [["vt", "run", "build"]] + +[[e2e]] +name = "hook_run_directly_uses_depends_on_without_warning" +comment = """ +A hook that runs as a task of its own uses its `dependsOn`, so there's nothing to warn about. +""" +steps = [["vt", "run", "prebuild"]] + +[[e2e]] +name = "ignore_depends_on_does_not_warn" +comment = """ +`--ignore-depends-on` already asks to skip dependencies, so hooks don't warn. +""" +steps = [["vt", "run", "--ignore-depends-on", "build"]] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hook_run_directly_uses_depends_on_without_warning.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hook_run_directly_uses_depends_on_without_warning.md new file mode 100644 index 000000000..d6e557548 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hook_run_directly_uses_depends_on_without_warning.md @@ -0,0 +1,16 @@ +# hook_run_directly_uses_depends_on_without_warning + +A hook that runs as a task of its own uses its `dependsOn`, so there's nothing to warn about. + +## `vt run prebuild` + +``` +$ vtt print setup ⊘ cache disabled +setup + +$ vtt print prebuild ⊘ cache disabled +prebuild + +--- +vt run: 2/2 successful. (Run `vt run --last-details` for full details) +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hooks_warn_that_depends_on_is_ignored.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hooks_warn_that_depends_on_is_ignored.md new file mode 100644 index 000000000..7032f23b4 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/hooks_warn_that_depends_on_is_ignored.md @@ -0,0 +1,21 @@ +# hooks_warn_that_depends_on_is_ignored + +`dependsOn` of a pre or post hook doesn't apply when the hook runs inside its script, so `setup` doesn't run and each hook warns. + +## `vt run build` + +``` +`dependsOn` of @test/script-hook-depends-on#prebuild is ignored when it runs as a hook of @test/script-hook-depends-on#build. Add it to `build` in `run.scripts`. +`dependsOn` of @test/script-hook-depends-on#postbuild is ignored when it runs as a hook of @test/script-hook-depends-on#build. Add it to `build` in `run.scripts`. +$ vtt print prebuild ⊘ cache disabled +prebuild + +$ vtt print build ⊘ cache disabled +build + +$ vtt print postbuild ⊘ cache disabled +postbuild + +--- +vt run: 3/3 successful. (Run `vt run --last-details` for full details) +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/ignore_depends_on_does_not_warn.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/ignore_depends_on_does_not_warn.md new file mode 100644 index 000000000..33fadf9a9 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/snapshots/ignore_depends_on_does_not_warn.md @@ -0,0 +1,19 @@ +# ignore_depends_on_does_not_warn + +`--ignore-depends-on` already asks to skip dependencies, so hooks don't warn. + +## `vt run --ignore-depends-on build` + +``` +$ vtt print prebuild ⊘ cache disabled +prebuild + +$ vtt print build ⊘ cache disabled +build + +$ vtt print postbuild ⊘ cache disabled +postbuild + +--- +vt run: 3/3 successful. (Run `vt run --last-details` for full details) +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/vite-task.json new file mode 100644 index 000000000..3df41c421 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/script_hook_depends_on/vite-task.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "prebuild": { "dependsOn": ["setup"] }, + "postbuild": { "dependsOn": ["setup"] } + } +} diff --git a/crates/vt_plan/src/plan.rs b/crates/vt_plan/src/plan.rs index 616d830a9..e5f2b42cc 100644 --- a/crates/vt_plan/src/plan.rs +++ b/crates/vt_plan/src/plan.rs @@ -81,6 +81,25 @@ fn effective_cache_config( if enabled { task_cache_config.cloned() } else { None } } +/// Warns when `hook` has `dependsOn`. A hook runs inside the execution node of `script`, so its +/// `dependsOn` only applies when it runs as a task of its own. +#[expect(clippy::print_stderr, reason = "user-facing warning for config that has no effect")] +async fn warn_if_hook_has_dependencies( + hook: &TaskNode, + script: &TaskNode, + context: &PlanContext<'_, '_>, +) -> Result<(), Error> { + if context.parent_query().include_explicit_deps + && context.indexed_task_graph().depends_on(&hook.id).await?.next().is_some() + { + eprintln!( + "`dependsOn` of {} is ignored when it runs as a hook of {}. Add it to `{}` in `run.scripts`.", + hook.task_display, script.task_display, script.task_display.task_name + ); + } + Ok(()) +} + /// - `with_hooks`: whether to look up `preX`/`postX` lifecycle hooks for this task. /// `false` when the task itself is being executed as a hook, so that hooks are /// never expanded more than one level deep (matching npm behavior). @@ -114,6 +133,7 @@ async fn plan_task_as_execution_node( None }; if let Some(pre_hook) = pre_hook { + warn_if_hook_has_dependencies(pre_hook, task_node, &context).await?; let mut pre_context = context.duplicate(); // Extra args (e.g. `vt run test --coverage`) must not be forwarded to hooks. pre_context.set_extra_args(Arc::new([])); @@ -422,6 +442,7 @@ async fn plan_task_as_execution_node( None }; if let Some(post_hook) = post_hook { + warn_if_hook_has_dependencies(post_hook, task_node, &context).await?; let mut post_context = context.duplicate(); // Extra args must not be forwarded to hooks. post_context.set_extra_args(Arc::new([])); From a4c535b995720d89e3991c5efa256ef211bfbbc3 Mon Sep 17 00:00:00 2001 From: SerKo Date: Thu, 12 Nov 2026 14:38:47 +0800 Subject: [PATCH 3/4] test(plan): remove unused script_options placeholder Co-authored-by: Claude Fable 5.2 --- .../tests/plan_snapshots/fixtures/script_options/src/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/script_options/src/.gitkeep diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/src/.gitkeep b/crates/vt_plan/tests/plan_snapshots/fixtures/script_options/src/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From a9bc1cdde7426fdde9105510187fb238f85ee5a1 Mon Sep 17 00:00:00 2001 From: SerKo Date: Thu, 12 Nov 2026 14:38:47 +0800 Subject: [PATCH 4/4] docs(task-graph): explain unknown-field checks for script options Co-authored-by: Claude Fable 5.2 --- crates/vt_graph/src/config/user.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/vt_graph/src/config/user.rs b/crates/vt_graph/src/config/user.rs index 18641b8a0..a0e394e34 100644 --- a/crates/vt_graph/src/config/user.rs +++ b/crates/vt_graph/src/config/user.rs @@ -256,10 +256,13 @@ pub struct EnabledCacheConfig { pub remote: Option, } -/// Options for user-defined tasks in `vite.config.*`, excluding the command. +/// Options for user-defined tasks and `package.json` scripts in `vite.config.*`, excluding the +/// command. #[derive(Debug, Deserialize, PartialEq, Eq)] // TS derive macro generates code using std types that clippy disallows; skip derive during linting #[cfg_attr(all(test, not(clippy)), derive(TS), ts(optional_fields, rename = "TaskOptions"))] +// `deny_unknown_fields` applies to `scripts` entries. Flattened into a task, this struct only +// receives its own fields, so `UserTaskConfig` is the one that rejects unknown fields there. #[serde(deny_unknown_fields, rename_all = "camelCase")] pub struct UserTaskOptions { /// The working directory for the task, relative to the package root (not workspace root).