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..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)?; @@ -100,6 +104,26 @@ 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, `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 = + 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 { + 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, false)).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 +381,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, false)).await?; + items.extend(post_execution.items); + } + Ok(TaskExecution { task_display: task_node.task_display.clone(), items }) } @@ -648,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-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-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": [] + } +] 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..11b251a0 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/script-hooks", + "scripts": { + "prepretest": "echo prepretest", + "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..8c4407b4 --- /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 expands prepretest but not when called as hook" +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 expands prepretest but not when called as hook.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 new file mode 100644 index 00000000..56765b7c --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/query - pretest directly expands prepretest but not when called as hook.snap @@ -0,0 +1,79 @@ +--- +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": "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": { + "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..837bd23b --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/script-hooks/snapshots/task graph.snap @@ -0,0 +1,211 @@ +--- +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": [ + "/", + "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": [ + "/", + "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": [] + } +]