From 18fa8ff0d25a8bdd6a4124a8617b6dea82b26ea6 Mon Sep 17 00:00:00 2001 From: branchseer Date: Thu, 12 Mar 2026 17:29:25 +0800 Subject: [PATCH 1/3] feat: add pre/post lifecycle hooks for package.json scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a package.json script `X` is run, automatically expand `preX` and `postX` scripts (if they exist in the same package) as inline execution items — matching npm's lifecycle hook behavior. - Hooks are expanded at plan time, not as graph dependency edges - Only applies to `PackageJsonScript` tasks; `TaskConfig` tasks are excluded - Extra CLI args are not forwarded to hook scripts - Configurable via `enablePrePostScripts` in the workspace root `vite-task.json` (defaults to `true`) Co-Authored-By: Claude Sonnet 4.6 --- crates/vite_task_graph/run-config.ts | 11 ++ crates/vite_task_graph/src/config/user.rs | 10 + crates/vite_task_graph/src/lib.rs | 56 ++++++ crates/vite_task_plan/src/plan.rs | 32 ++++ .../script-hooks-disabled/package.json | 8 + .../script-hooks-disabled/snapshots.toml | 5 + ...test runs without hooks when disabled.snap | 53 ++++++ .../snapshots/task graph.snap | 109 +++++++++++ .../script-hooks-disabled/vite-task.json | 3 + .../script-hooks-task-no-hook/package.json | 6 + .../script-hooks-task-no-hook/snapshots.toml | 8 + ...fig test does not expand pretest hook.snap | 53 ++++++ .../snapshots/task graph.snap | 75 ++++++++ .../script-hooks-task-no-hook/vite-task.json | 10 + .../fixtures/script-hooks/package.json | 10 + .../fixtures/script-hooks/snapshots.toml | 17 ++ ...query - build runs with pre hook only.snap | 79 ++++++++ ...uery - extra args not passed to hooks.snap | 107 +++++++++++ ...query - pretest directly has no hooks.snap | 53 ++++++ ...y - test runs with pre and post hooks.snap | 105 +++++++++++ .../script-hooks/snapshots/task graph.snap | 177 ++++++++++++++++++ 21 files changed, 987 insertions(+) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/query - test runs without hooks when disabled.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/task graph.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/query - task config test does not expand pretest hook.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/task graph.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - build runs with pre hook only.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - extra args not passed to hooks.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly has no hooks.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - test runs with pre and post hooks.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 9a928483..3546917b 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -92,4 +92,15 @@ export type RunConfig = { * Task definitions */ tasks?: { [key in string]?: Task }; + /** + * Whether to automatically run `preX`/`postX` package.json scripts as + * lifecycle hooks when script `X` is executed. + * + * When `true` (the default), running script `test` will automatically + * run `pretest` before and `posttest` after, if they exist. + * + * This option can only be set in the workspace root's config file. + * Setting it in a package's config will result in an error. + */ + enablePrePostScripts?: boolean; }; diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index aca0368e..f568920b 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -224,6 +224,16 @@ pub struct UserRunConfig { /// Task definitions pub tasks: Option>, + + /// Whether to automatically run `preX`/`postX` package.json scripts as + /// lifecycle hooks when script `X` is executed. + /// + /// When `true` (the default), running script `test` will automatically + /// run `pretest` before and `posttest` after, if they exist. + /// + /// This option can only be set in the workspace root's config file. + /// Setting it in a package's config will result in an error. + pub enable_pre_post_scripts: Option, } impl UserRunConfig { diff --git a/crates/vite_task_graph/src/lib.rs b/crates/vite_task_graph/src/lib.rs index 9741c64a..d5b86c92 100644 --- a/crates/vite_task_graph/src/lib.rs +++ b/crates/vite_task_graph/src/lib.rs @@ -118,6 +118,11 @@ pub enum TaskGraphLoadError { #[error("`cache` can only be set in the workspace root config, but found in {package_path}")] CacheInNonRootPackage { package_path: Arc }, + + #[error( + "`enablePrePostScripts` can only be set in the workspace root config, but found in {package_path}" + )] + PrePostScriptsInNonRootPackage { package_path: Arc }, } /// Error when looking up a task by its specifier. @@ -185,8 +190,14 @@ pub struct IndexedTaskGraph { /// task indices by task id for quick lookup pub(crate) node_indices_by_task_id: FxHashMap, + /// Reverse map: task node index → task id (for hook lookup) + task_ids_by_node_index: FxHashMap, + /// Global cache configuration resolved from the workspace root config. resolved_global_cache: ResolvedGlobalCacheConfig, + + /// Whether pre/post script hooks are enabled (from `enablePrePostScripts` in workspace root config). + pre_post_scripts_enabled: bool, } pub type TaskGraph = DiGraph; @@ -222,9 +233,12 @@ impl IndexedTaskGraph { // index tasks by ids let mut node_indices_by_task_id: FxHashMap = FxHashMap::with_capacity_and_hasher(task_graph.node_count(), FxBuildHasher); + let mut task_ids_by_node_index: FxHashMap = + FxHashMap::with_capacity_and_hasher(task_graph.node_count(), FxBuildHasher); // First pass: load all configs, extract root cache config, validate let mut root_cache = None; + let mut root_pre_post_scripts_enabled = None; let mut package_configs: Vec<(PackageNodeIndex, Arc, UserRunConfig)> = Vec::with_capacity(package_graph.node_count()); @@ -252,6 +266,16 @@ impl IndexedTaskGraph { } } + if let Some(val) = user_config.enable_pre_post_scripts { + if is_workspace_root { + root_pre_post_scripts_enabled = Some(val); + } else { + return Err(TaskGraphLoadError::PrePostScriptsInNonRootPackage { + package_path: package_dir.clone(), + }); + } + } + package_configs.push((package_index, package_dir, user_config)); } @@ -312,6 +336,7 @@ impl IndexedTaskGraph { let node_index = task_graph.add_node(task_node); task_ids_with_dependency_specifiers.push((task_id.clone(), dependency_specifiers)); + task_ids_by_node_index.insert(node_index, task_id.clone()); node_indices_by_task_id.insert(task_id, node_index); } @@ -340,6 +365,7 @@ impl IndexedTaskGraph { resolved_config, source: TaskSource::PackageJsonScript, }); + task_ids_by_node_index.insert(node_index, task_id.clone()); node_indices_by_task_id.insert(task_id, node_index); } } @@ -349,7 +375,9 @@ impl IndexedTaskGraph { task_graph, indexed_package_graph: IndexedPackageGraph::index(package_graph), node_indices_by_task_id, + task_ids_by_node_index, resolved_global_cache, + pre_post_scripts_enabled: root_pre_post_scripts_enabled.unwrap_or(true), }; // Add explicit dependencies @@ -459,4 +487,32 @@ impl IndexedTaskGraph { pub const fn global_cache_config(&self) -> &ResolvedGlobalCacheConfig { &self.resolved_global_cache } + + /// Whether pre/post script hooks are enabled workspace-wide. + #[must_use] + pub const fn pre_post_scripts_enabled(&self) -> bool { + self.pre_post_scripts_enabled + } + + /// Returns the `TaskNodeIndex` of the pre/post hook for a `PackageJsonScript` task. + /// + /// Given a task named `X` and `prefix = "pre"`, looks up `preX` in the same package. + /// Given a task named `X` and `prefix = "post"`, looks up `postX` in the same package. + /// + /// Returns `None` if: + /// - The task is not a `PackageJsonScript` + /// - No `{prefix}{name}` script exists in the same package + /// - The hook is not itself a `PackageJsonScript` + #[must_use] + pub fn get_script_hook(&self, task_idx: TaskNodeIndex, prefix: &str) -> Option { + let task_node = &self.task_graph[task_idx]; + if task_node.source != TaskSource::PackageJsonScript { + return None; + } + let task_id = self.task_ids_by_node_index.get(&task_idx)?; + let hook_name = vite_str::format!("{prefix}{}", task_node.task_display.task_name); + let hook_id = TaskId { package_index: task_id.package_index, task_name: hook_name }; + let &hook_idx = self.node_indices_by_task_id.get(&hook_id)?; + (self.task_graph[hook_idx].source == TaskSource::PackageJsonScript).then_some(hook_idx) + } } diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index a78addfc..b3888b2c 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -100,6 +100,23 @@ async fn plan_task_as_execution_node( let mut items = Vec::::new(); + // Expand pre/post hooks (`preX`/`postX`) for package.json scripts. + // Resolve the flag once before any mutable borrow of `context` (duplicate() needs &mut). + let pre_post_scripts_enabled = context.indexed_task_graph().pre_post_scripts_enabled(); + let pre_hook_idx = if pre_post_scripts_enabled { + context.indexed_task_graph().get_script_hook(task_node_index, "pre") + } else { + None + }; + if let Some(pre_hook_idx) = pre_hook_idx { + 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([])); + let pre_execution = + Box::pin(plan_task_as_execution_node(pre_hook_idx, pre_context)).await?; + items.extend(pre_execution.items); + } + // Use task's resolved cwd for display (from task config's cwd option) let mut cwd = Arc::clone(&task_node.resolved_config.resolved_options.cwd); @@ -357,6 +374,21 @@ async fn plan_task_as_execution_node( }); } + // Expand post-hook (`postX`) for package.json scripts. + let post_hook_idx = if pre_post_scripts_enabled { + context.indexed_task_graph().get_script_hook(task_node_index, "post") + } else { + None + }; + if let Some(post_hook_idx) = post_hook_idx { + let mut post_context = context.duplicate(); + // Extra args must not be forwarded to hooks. + post_context.set_extra_args(Arc::new([])); + let post_execution = + Box::pin(plan_task_as_execution_node(post_hook_idx, post_context)).await?; + items.extend(post_execution.items); + } + Ok(TaskExecution { task_display: task_node.task_display.clone(), items }) } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/package.json new file mode 100644 index 00000000..45f66a49 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/script-hooks-disabled", + "scripts": { + "pretest": "echo pretest", + "test": "echo test", + "posttest": "echo posttest" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots.toml new file mode 100644 index 00000000..e451e4cb --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots.toml @@ -0,0 +1,5 @@ +# Tests that pre/post hooks are NOT expanded when enablePrePostScripts is false. + +[[plan]] +name = "test runs without hooks when disabled" +args = ["run", "test"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/query - test runs without hooks when disabled.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/query - test runs without hooks when disabled.snap new file mode 100644 index 00000000..ee7d6ecd --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/query - test runs without hooks when disabled.snap @@ -0,0 +1,53 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +info: + args: + - run + - test +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled +--- +[ + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-disabled", + "task_name": "test", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks-disabled", + "task_name": "test", + "package_path": "/" + }, + "command": "echo test", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "test" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/task graph.snap new file mode 100644 index 00000000..1f90c544 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/snapshots/task graph.snap @@ -0,0 +1,109 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled +--- +[ + { + "key": [ + "/", + "posttest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-disabled", + "task_name": "posttest", + "package_path": "/" + }, + "resolved_config": { + "command": "echo posttest", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "pretest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-disabled", + "task_name": "pretest", + "package_path": "/" + }, + "resolved_config": { + "command": "echo pretest", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-disabled", + "task_name": "test", + "package_path": "/" + }, + "resolved_config": { + "command": "echo test", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/vite-task.json new file mode 100644 index 00000000..f96c98c4 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-disabled/vite-task.json @@ -0,0 +1,3 @@ +{ + "enablePrePostScripts": false +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/package.json new file mode 100644 index 00000000..f729664b --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/package.json @@ -0,0 +1,6 @@ +{ + "name": "@test/script-hooks-task-no-hook", + "scripts": { + "pretest": "echo pretest-script" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots.toml new file mode 100644 index 00000000..c5d1fdfc --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots.toml @@ -0,0 +1,8 @@ +# Tests that TaskConfig tasks do NOT get pre/post hooks expanded, +# even if matching pre/post package.json scripts exist. +# `test` is a TaskConfig; `pretest` is a PackageJsonScript. +# Running `test` must NOT expand `pretest` as a hook. + +[[plan]] +name = "task config test does not expand pretest hook" +args = ["run", "test"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/query - task config test does not expand pretest hook.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/query - task config test does not expand pretest hook.snap new file mode 100644 index 00000000..5e738dfb --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/query - task config test does not expand pretest hook.snap @@ -0,0 +1,53 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +info: + args: + - run + - test +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook +--- +[ + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-task-no-hook", + "task_name": "test", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks-task-no-hook", + "task_name": "test", + "package_path": "/" + }, + "command": "echo test-task", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "test-task" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/task graph.snap new file mode 100644 index 00000000..7d9b0080 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/snapshots/task graph.snap @@ -0,0 +1,75 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook +--- +[ + { + "key": [ + "/", + "pretest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-task-no-hook", + "task_name": "pretest", + "package_path": "/" + }, + "resolved_config": { + "command": "echo pretest-script", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-task-no-hook", + "task_name": "test", + "package_path": "/" + }, + "resolved_config": { + "command": "echo test-task", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/vite-task.json new file mode 100644 index 00000000..0ea13d52 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-task-no-hook/vite-task.json @@ -0,0 +1,10 @@ +{ + "tasks": { + // `test` is a TaskConfig — pre/post hooks must NOT apply to it. + // `pretest` is a package.json script, but since `test` is a TaskConfig, + // `pretest` must not be expanded as a hook when running `test`. + "test": { + "command": "echo test-task" + } + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json new file mode 100644 index 00000000..d4203edc --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/script-hooks", + "scripts": { + "pretest": "echo pretest", + "test": "echo test", + "posttest": "echo posttest", + "build": "echo build", + "prebuild": "echo prebuild" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml new file mode 100644 index 00000000..2494d81e --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml @@ -0,0 +1,17 @@ +# Tests pre/post lifecycle hooks for package.json scripts. + +[[plan]] +name = "test runs with pre and post hooks" +args = ["run", "test"] + +[[plan]] +name = "build runs with pre hook only" +args = ["run", "build"] + +[[plan]] +name = "pretest directly has no hooks" +args = ["run", "pretest"] + +[[plan]] +name = "extra args not passed to hooks" +args = ["run", "test", "--coverage"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - build runs with pre hook only.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - build runs with pre hook only.snap new file mode 100644 index 00000000..566371b4 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - build runs with pre hook only.snap @@ -0,0 +1,79 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +info: + args: + - run + - build +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks +--- +[ + { + "key": [ + "/", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "build", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "prebuild", + "package_path": "/" + }, + "command": "echo prebuild", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "prebuild" + ], + "trailing_newline": true + } + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "build", + "package_path": "/" + }, + "command": "echo build", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "build" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - extra args not passed to hooks.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - extra args not passed to hooks.snap new file mode 100644 index 00000000..d37b03f2 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - extra args not passed to hooks.snap @@ -0,0 +1,107 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +info: + args: + - run + - test + - "--coverage" +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks +--- +[ + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "test", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "pretest", + "package_path": "/" + }, + "command": "echo pretest", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "pretest" + ], + "trailing_newline": true + } + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "test", + "package_path": "/" + }, + "command": "echo test --coverage", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "test", + "--coverage" + ], + "trailing_newline": true + } + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "posttest", + "package_path": "/" + }, + "command": "echo posttest", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "posttest" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly has no hooks.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly has no hooks.snap new file mode 100644 index 00000000..38ed4daa --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly has no hooks.snap @@ -0,0 +1,53 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +info: + args: + - run + - pretest +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks +--- +[ + { + "key": [ + "/", + "pretest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "pretest", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "pretest", + "package_path": "/" + }, + "command": "echo pretest", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "pretest" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - test runs with pre and post hooks.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - test runs with pre and post hooks.snap new file mode 100644 index 00000000..fdf34168 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - test runs with pre and post hooks.snap @@ -0,0 +1,105 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +info: + args: + - run + - test +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks +--- +[ + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "test", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "pretest", + "package_path": "/" + }, + "command": "echo pretest", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "pretest" + ], + "trailing_newline": true + } + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "test", + "package_path": "/" + }, + "command": "echo test", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "test" + ], + "trailing_newline": true + } + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "posttest", + "package_path": "/" + }, + "command": "echo posttest", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "posttest" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap new file mode 100644 index 00000000..33f2310f --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap @@ -0,0 +1,177 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks +--- +[ + { + "key": [ + "/", + "build" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "build", + "package_path": "/" + }, + "resolved_config": { + "command": "echo build", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "posttest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "posttest", + "package_path": "/" + }, + "resolved_config": { + "command": "echo posttest", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "prebuild" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "prebuild", + "package_path": "/" + }, + "resolved_config": { + "command": "echo prebuild", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "pretest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "pretest", + "package_path": "/" + }, + "resolved_config": { + "command": "echo pretest", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "test", + "package_path": "/" + }, + "resolved_config": { + "command": "echo test", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + } +] From 04b9b385a859c28cfad68ad791ec1caeb39a1cbe Mon Sep 17 00:00:00 2001 From: branchseer Date: Thu, 12 Mar 2026 17:41:12 +0800 Subject: [PATCH 2/3] fix: hooks are not expanded recursively (one level deep, matching npm) When `vt run test` triggers `pretest` as a hook, `pretest` itself does not look for `prepretest`. This matches npm's behavior where lifecycle hooks are always one level deep. Implemented via an `expand_hooks: bool` flag on `PlanContext` that is set to `false` when planning any hook script. Added `prepretest` to the script-hooks test fixture to prove that `vt run test` correctly produces [pretest, test, posttest] without `prepretest`, while `vt run pretest` directly does expand `prepretest`. Co-Authored-By: Claude Sonnet 4.6 --- crates/vite_task_plan/src/context.rs | 16 +++++++++ crates/vite_task_plan/src/plan.rs | 7 +++- .../fixtures/script-hooks/package.json | 1 + .../fixtures/script-hooks/snapshots.toml | 2 +- ...epretest but not when called as hook.snap} | 26 ++++++++++++++ .../script-hooks/snapshots/task graph.snap | 34 +++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) rename crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/{query - pretest directly has no hooks.snap => query - pretest directly expands prepretest but not when called as hook.snap} (62%) diff --git a/crates/vite_task_plan/src/context.rs b/crates/vite_task_plan/src/context.rs index 119d68af..4d0a637e 100644 --- a/crates/vite_task_plan/src/context.rs +++ b/crates/vite_task_plan/src/context.rs @@ -48,6 +48,12 @@ pub struct PlanContext<'a> { /// The query that caused the current expansion. /// Used by the skip rule to detect and skip duplicate nested expansions. parent_query: Arc, + + /// Whether pre/post hook scripts should be expanded for the current task. + /// + /// Set to `false` when planning a hook script itself, so that hooks are + /// never expanded more than one level deep (matching npm behavior). + expand_hooks: bool, } impl<'a> PlanContext<'a> { @@ -70,6 +76,7 @@ impl<'a> PlanContext<'a> { extra_args: Arc::default(), resolved_global_cache, parent_query, + expand_hooks: true, } } @@ -150,6 +157,14 @@ impl<'a> PlanContext<'a> { self.task_call_stack.last().map(|(idx, _)| *idx) } + pub const fn expand_hooks(&self) -> bool { + self.expand_hooks + } + + pub const fn set_expand_hooks(&mut self, expand_hooks: bool) { + self.expand_hooks = expand_hooks; + } + pub fn duplicate(&mut self) -> PlanContext<'_> { PlanContext { workspace_path: self.workspace_path, @@ -161,6 +176,7 @@ impl<'a> PlanContext<'a> { extra_args: Arc::clone(&self.extra_args), resolved_global_cache: self.resolved_global_cache, parent_query: Arc::clone(&self.parent_query), + expand_hooks: self.expand_hooks, } } } diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index b3888b2c..2ea74a33 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -101,8 +101,11 @@ async fn plan_task_as_execution_node( let mut items = Vec::::new(); // Expand pre/post hooks (`preX`/`postX`) for package.json scripts. + // Hooks are never expanded more than one level deep (matching npm behavior): when planning a + // hook script, `expand_hooks` is false so it won't look for its own pre/post hooks. // Resolve the flag once before any mutable borrow of `context` (duplicate() needs &mut). - let pre_post_scripts_enabled = context.indexed_task_graph().pre_post_scripts_enabled(); + let pre_post_scripts_enabled = + context.expand_hooks() && context.indexed_task_graph().pre_post_scripts_enabled(); let pre_hook_idx = if pre_post_scripts_enabled { context.indexed_task_graph().get_script_hook(task_node_index, "pre") } else { @@ -112,6 +115,7 @@ async fn plan_task_as_execution_node( 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([])); + pre_context.set_expand_hooks(false); let pre_execution = Box::pin(plan_task_as_execution_node(pre_hook_idx, pre_context)).await?; items.extend(pre_execution.items); @@ -384,6 +388,7 @@ async fn plan_task_as_execution_node( let mut post_context = context.duplicate(); // Extra args must not be forwarded to hooks. post_context.set_extra_args(Arc::new([])); + post_context.set_expand_hooks(false); let post_execution = Box::pin(plan_task_as_execution_node(post_hook_idx, post_context)).await?; items.extend(post_execution.items); diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json index d4203edc..11b251a0 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json @@ -1,6 +1,7 @@ { "name": "@test/script-hooks", "scripts": { + "prepretest": "echo prepretest", "pretest": "echo pretest", "test": "echo test", "posttest": "echo posttest", diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml index 2494d81e..8c4407b4 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots.toml @@ -9,7 +9,7 @@ name = "build runs with pre hook only" args = ["run", "build"] [[plan]] -name = "pretest directly has no hooks" +name = "pretest directly expands prepretest but not when called as hook" args = ["run", "pretest"] [[plan]] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly has no hooks.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly expands prepretest but not when called as hook.snap similarity index 62% rename from crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly has no hooks.snap rename to crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly expands prepretest but not when called as hook.snap index 38ed4daa..56765b7c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly has no hooks.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly expands prepretest but not when called as hook.snap @@ -20,6 +20,32 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks "package_path": "/" }, "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "prepretest", + "package_path": "/" + }, + "command": "echo prepretest", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "prepretest" + ], + "trailing_newline": true + } + } + } + } + } + }, { "execution_item_display": { "task_display": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap index 33f2310f..837bd23b 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap @@ -106,6 +106,40 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks }, "neighbors": [] }, + { + "key": [ + "/", + "prepretest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks", + "task_name": "prepretest", + "package_path": "/" + }, + "resolved_config": { + "command": "echo prepretest", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, { "key": [ "/", From 9656e8bd4632d986c08461420e03b576ebafe171 Mon Sep 17 00:00:00 2001 From: branchseer Date: Thu, 12 Mar 2026 17:48:08 +0800 Subject: [PATCH 3/3] fix: hooks from nested `vt run` calls within hook scripts are expanded When a hook script's command is `vt run someScript`, `someScript`'s own pre/post hooks should still be expanded. Previously, the `expand_hooks` flag (which suppresses recursive hook expansion) incorrectly propagated into nested `plan_query_request` calls, preventing `prescriptInHook` from being found when `scriptInHook` was called via `vt run` from a hook. The flag is now a `with_hooks: bool` parameter of `plan_task_as_execution_node` rather than a field on `PlanContext`. `plan_query_request` always passes `true`, correctly scoping the one-level-deep restriction to the single hook call rather than its entire subtree. Co-Authored-By: Claude Sonnet 4.6 --- crates/vite_task_plan/src/context.rs | 16 -- crates/vite_task_plan/src/plan.rs | 19 ++- .../script-hooks-nested-run/package.json | 9 ++ .../script-hooks-nested-run/snapshots.toml | 7 + ...en scriptInHook is called from a hook.snap | 137 +++++++++++++++++ .../snapshots/task graph.snap | 143 ++++++++++++++++++ 6 files changed, 307 insertions(+), 24 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/query - prescriptInHook runs when scriptInHook is called from a hook.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/task graph.snap diff --git a/crates/vite_task_plan/src/context.rs b/crates/vite_task_plan/src/context.rs index 4d0a637e..119d68af 100644 --- a/crates/vite_task_plan/src/context.rs +++ b/crates/vite_task_plan/src/context.rs @@ -48,12 +48,6 @@ pub struct PlanContext<'a> { /// The query that caused the current expansion. /// Used by the skip rule to detect and skip duplicate nested expansions. parent_query: Arc, - - /// Whether pre/post hook scripts should be expanded for the current task. - /// - /// Set to `false` when planning a hook script itself, so that hooks are - /// never expanded more than one level deep (matching npm behavior). - expand_hooks: bool, } impl<'a> PlanContext<'a> { @@ -76,7 +70,6 @@ impl<'a> PlanContext<'a> { extra_args: Arc::default(), resolved_global_cache, parent_query, - expand_hooks: true, } } @@ -157,14 +150,6 @@ impl<'a> PlanContext<'a> { self.task_call_stack.last().map(|(idx, _)| *idx) } - pub const fn expand_hooks(&self) -> bool { - self.expand_hooks - } - - pub const fn set_expand_hooks(&mut self, expand_hooks: bool) { - self.expand_hooks = expand_hooks; - } - pub fn duplicate(&mut self) -> PlanContext<'_> { PlanContext { workspace_path: self.workspace_path, @@ -176,7 +161,6 @@ impl<'a> PlanContext<'a> { extra_args: Arc::clone(&self.extra_args), resolved_global_cache: self.resolved_global_cache, parent_query: Arc::clone(&self.parent_query), - expand_hooks: self.expand_hooks, } } } diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 2ea74a33..832c4185 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -79,11 +79,15 @@ fn effective_cache_config( if enabled { task_cache_config.cloned() } else { None } } +/// - `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). #[expect(clippy::too_many_lines, reason = "sequential planning steps are clearer in one function")] #[expect(clippy::future_not_send, reason = "PlanContext contains !Send dyn PlanRequestParser")] async fn plan_task_as_execution_node( task_node_index: TaskNodeIndex, mut context: PlanContext<'_>, + with_hooks: bool, ) -> Result { // Check for recursions in the task call stack. context.check_recursion(task_node_index)?; @@ -102,10 +106,10 @@ async fn plan_task_as_execution_node( // Expand pre/post hooks (`preX`/`postX`) for package.json scripts. // Hooks are never expanded more than one level deep (matching npm behavior): when planning a - // hook script, `expand_hooks` is false so it won't look for its own pre/post hooks. + // hook script, `with_hooks` is false so it won't look for its own pre/post hooks. // Resolve the flag once before any mutable borrow of `context` (duplicate() needs &mut). let pre_post_scripts_enabled = - context.expand_hooks() && context.indexed_task_graph().pre_post_scripts_enabled(); + with_hooks && context.indexed_task_graph().pre_post_scripts_enabled(); let pre_hook_idx = if pre_post_scripts_enabled { context.indexed_task_graph().get_script_hook(task_node_index, "pre") } else { @@ -115,9 +119,8 @@ async fn plan_task_as_execution_node( 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([])); - pre_context.set_expand_hooks(false); let pre_execution = - Box::pin(plan_task_as_execution_node(pre_hook_idx, pre_context)).await?; + Box::pin(plan_task_as_execution_node(pre_hook_idx, pre_context, false)).await?; items.extend(pre_execution.items); } @@ -388,9 +391,8 @@ async fn plan_task_as_execution_node( let mut post_context = context.duplicate(); // Extra args must not be forwarded to hooks. post_context.set_extra_args(Arc::new([])); - post_context.set_expand_hooks(false); let post_execution = - Box::pin(plan_task_as_execution_node(post_hook_idx, post_context)).await?; + Box::pin(plan_task_as_execution_node(post_hook_idx, post_context, false)).await?; items.extend(post_execution.items); } @@ -685,8 +687,9 @@ pub async fn plan_query_request( if Some(task_index) == pruned_task { continue; } - let task_execution = - plan_task_as_execution_node(task_index, context.duplicate()).boxed_local().await?; + let task_execution = plan_task_as_execution_node(task_index, context.duplicate(), true) + .boxed_local() + .await?; execution_node_indices_by_task_index .insert(task_index, inner_graph.add_node(task_execution)); } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/package.json new file mode 100644 index 00000000..ef69df12 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/script-hooks-nested-run", + "scripts": { + "prescriptInHook": "echo prescriptInHook", + "scriptInHook": "echo scriptInHook", + "pretest": "vt run scriptInHook", + "test": "echo test" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots.toml new file mode 100644 index 00000000..186c7228 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots.toml @@ -0,0 +1,7 @@ +# Tests that hooks of scripts called via `vt run` within a hook are properly expanded. +# When `pretest` (a hook of `test`) runs `vt run scriptInHook`, the `prescriptInHook` +# hook of `scriptInHook` should still be found and executed. + +[[plan]] +name = "prescriptInHook runs when scriptInHook is called from a hook" +args = ["run", "test"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/query - prescriptInHook runs when scriptInHook is called from a hook.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/query - prescriptInHook runs when scriptInHook is called from a hook.snap new file mode 100644 index 00000000..148cbdfd --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/query - prescriptInHook runs when scriptInHook is called from a hook.snap @@ -0,0 +1,137 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +info: + args: + - run + - test +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run +--- +[ + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "test", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "pretest", + "package_path": "/" + }, + "command": "vt run scriptInHook", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Expanded": [ + { + "key": [ + "/", + "scriptInHook" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "scriptInHook", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "prescriptInHook", + "package_path": "/" + }, + "command": "echo prescriptInHook", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "prescriptInHook" + ], + "trailing_newline": true + } + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "scriptInHook", + "package_path": "/" + }, + "command": "echo scriptInHook", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "scriptInHook" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } + ] + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "test", + "package_path": "/" + }, + "command": "echo test", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "test" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/task graph.snap new file mode 100644 index 00000000..b4bfbae6 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run/snapshots/task graph.snap @@ -0,0 +1,143 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks-nested-run +--- +[ + { + "key": [ + "/", + "prescriptInHook" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "prescriptInHook", + "package_path": "/" + }, + "resolved_config": { + "command": "echo prescriptInHook", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "pretest" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "pretest", + "package_path": "/" + }, + "resolved_config": { + "command": "vt run scriptInHook", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "scriptInHook" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "scriptInHook", + "package_path": "/" + }, + "resolved_config": { + "command": "echo scriptInHook", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "test" + ], + "node": { + "task_display": { + "package_name": "@test/script-hooks-nested-run", + "task_name": "test", + "package_path": "/" + }, + "resolved_config": { + "command": "echo test", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "PackageJsonScript" + }, + "neighbors": [] + } +]