Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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. 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 <task>` in a package that doesn't define the task now reports `Task "<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)).
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"]]
Original file line number Diff line number Diff line change
@@ -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)
```
Original file line number Diff line number Diff line change
@@ -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)
```
Original file line number Diff line number Diff line change
@@ -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)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"scripts": {
"prebuild": { "dependsOn": ["setup"] },
"postbuild": { "dependsOn": ["setup"] }
}
}
35 changes: 34 additions & 1 deletion crates/vt_graph/run-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,37 @@ output?: Array<string | GlobWithBase | AutoTracking>, };

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<DependsOnEntry>,
/**
* 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`
*/
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions crates/vt_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -409,13 +408,14 @@ impl ResolvedTaskConfig {
pub fn resolve_package_json_script(
package_dir: &Arc<AbsolutePath>,
package_json_script: &str,
options: UserTaskOptions,
workspace_root: &AbsolutePath,
glob_resolver: &GlobResolver,
) -> Result<Self, ResolveTaskConfigError> {
Ok(Self {
commands: vec![package_json_script.into()].into(),
resolved_options: ResolvedTaskOptions::resolve(
UserTaskOptions::default(),
options,
package_dir,
workspace_root,
glob_resolver,
Expand Down
29 changes: 25 additions & 4 deletions crates/vt_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,14 @@ pub struct EnabledCacheConfig {
pub remote: Option<bool>,
}

/// 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))]
#[serde(rename_all = "camelCase")]
#[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).
#[serde(rename = "cwd")]
Expand Down Expand Up @@ -378,7 +381,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<bool>,
Expand Down Expand Up @@ -460,6 +463,13 @@ pub struct UserRunConfig {
/// Task definitions: full task objects, command strings, or command string arrays.
pub tasks: Option<FxHashMap<Str, UserTaskDefinition>>,

/// 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<FxHashMap<Str, UserTaskOptions>>,

/// Whether to automatically run `preX`/`postX` package.json scripts as
/// lifecycle hooks when script `X` is executed.
///
Expand Down Expand Up @@ -572,6 +582,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::<UserRunConfig>(user_config_json.clone()).is_err(),
"{user_config_json}"
);
}
}

#[test]
fn test_command_required() {
let user_config_json = json!({});
Expand Down
6 changes: 5 additions & 1 deletion crates/vt_graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 18 additions & 2 deletions crates/vt_graph/src/task_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Expand All @@ -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)?;
Expand Down
21 changes: 21 additions & 0 deletions crates/vt_plan/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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([]));
Expand Down Expand Up @@ -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([]));
Expand Down
Original file line number Diff line number Diff line change
@@ -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
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`
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"]
Loading