Skip to content
Merged
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
29 changes: 24 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ vite_glob = { path = "crates/vite_glob" }
vite_path = { path = "crates/vite_path" }
vite_shell = { path = "crates/vite_shell" }
vite_str = { path = "crates/vite_str" }
vite_task_graph = { path = "crates/vite_task_graph" }
vite_workspace = { path = "crates/vite_workspace" }
wax = "0.6.0"
which = "8.0.0"
Expand Down
7 changes: 7 additions & 0 deletions crates/vite_str/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{
ops::Deref,
path::Path,
str::from_utf8,
sync::Arc,
};

use bincode::{
Expand Down Expand Up @@ -158,6 +159,12 @@ impl From<CompactString> for Str {
}
}

impl From<Str> for Arc<str> {
fn from(value: Str) -> Self {
Arc::from(value.as_str())
}
}

impl PartialEq<&str> for Str {
fn eq(&self, other: &&str) -> bool {
self.0 == other
Expand Down
159 changes: 125 additions & 34 deletions crates/vite_task_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
mod user;
pub mod user;

use std::collections::HashSet;
use std::{collections::HashSet, sync::Arc};

use monostate::MustBe;
pub use user::{UserCacheConfig, UserConfigFile, UserTaskConfig};
use vite_path::{AbsolutePath, AbsolutePathBuf};
use vite_path::AbsolutePath;
use vite_str::Str;

use crate::config::user::UserTaskOptions;

/// Task configuration resolved from `package.json` scripts and/or `vite.config.ts` tasks,
/// without considering external factors like additional args from cli or environment variables.
///
Expand All @@ -16,29 +18,64 @@ use vite_str::Str;
/// For example, `cwd` is resolved to absolute ones (no external factor can change it),
/// but `command` is not parsed into program and args yet because environment variables in it may need to be expanded.
///
/// `depends_on` is not included here because it's represented in the task graph.
/// `depends_on` is not included here because it's represented by the edges of the task graph.
#[derive(Debug)]
pub struct ResolvedUserTaskConfig {
/// The command to run for this task
pub struct ResolvedTaskConfig {
/// The command to run for this task, as a raw string.
///
/// The command may contain environment variables that need to be expanded later.
pub command: Str,

/// The working directory for the task
pub cwd: AbsolutePathBuf,
pub resolved_options: ResolvedTaskOptions,
}

#[derive(Debug)]
pub struct ResolvedTaskOptions {
/// The working directory for the task
pub cwd: Arc<AbsolutePath>,
/// Cache-related config. None means caching is disabled.
pub cache_config: Option<CacheConfig>,
}

impl ResolvedTaskOptions {
/// Resolves from user-defined options and the directory path where the options are defined.
pub fn resolve(user_options: UserTaskOptions, dir: &Arc<AbsolutePath>) -> Self {
let cwd: Arc<AbsolutePath> = if user_options.cwd_relative_to_package.as_str().is_empty() {
Arc::clone(dir)
} else {
dir.join(user_options.cwd_relative_to_package).into()
};
let cache_config = match user_options.cache_config {
UserCacheConfig::Disabled { cache: MustBe!(false) } => None,
UserCacheConfig::Enabled { cache: MustBe!(true), envs, mut pass_through_envs } => {
pass_through_envs.extend(DEFAULT_PASSTHROUGH_ENVS.iter().copied().map(Str::from));
Some(CacheConfig {
env_config: EnvConfig {
fingerprinted_envs: envs.into_iter().collect(),
pass_through_envs: pass_through_envs.into(),
},
})
}
};
Self { cwd, cache_config }
}
}

#[derive(Debug)]
pub struct CacheConfig {
pub env_config: EnvConfig,
}

#[derive(Debug)]
pub struct EnvConfig {
/// environment variable names to be fingerprinted and passed to the task, with defaults populated
pub envs: HashSet<Str>,
pub fingerprinted_envs: HashSet<Str>,
/// environment variable names to be passed to the task without fingerprinting, with defaults populated
pub pass_through_envs: HashSet<Str>,
pub pass_through_envs: Arc<[Str]>,
}

#[derive(Debug, thiserror::Error)]
pub enum ResolveTaskError {
pub enum ResolveTaskConfigError {
/// Both package.json script and vite.config.* task define commands for the task
#[error("Both package.json script and vite.config.* task define commands for the task")]
CommandConflict,
Expand All @@ -48,41 +85,95 @@ pub enum ResolveTaskError {
NoCommand,
}

impl ResolvedUserTaskConfig {
impl ResolvedTaskConfig {
/// Resolve from package.json script only
pub fn resolve_package_json_script(
package_dir: &AbsolutePath,
package_dir: &Arc<AbsolutePath>,
package_json_script: &str,
) -> Self {
Self::resolve(
UserTaskConfig::package_json_script_default(),
package_dir,
Some(package_json_script),
)
.expect("Command conflict/missing for package.json script should never happen")
Self {
command: package_json_script.into(),
resolved_options: ResolvedTaskOptions::resolve(UserTaskOptions::default(), package_dir),
}
}

/// Resolves from user config, package dir, and package.json script (if any).
pub fn resolve(
user_config: UserTaskConfig,
package_dir: &AbsolutePath,
package_dir: &Arc<AbsolutePath>,
package_json_script: Option<&str>,
) -> Result<Self, ResolveTaskError> {
) -> Result<Self, ResolveTaskConfigError> {
let command = match (&user_config.command, package_json_script) {
(Some(_), Some(_)) => return Err(ResolveTaskError::CommandConflict),
(None, None) => return Err(ResolveTaskError::NoCommand),
(Some(_), Some(_)) => return Err(ResolveTaskConfigError::CommandConflict),
(None, None) => return Err(ResolveTaskConfigError::NoCommand),
(Some(cmd), None) => cmd.as_ref(),
(None, Some(script)) => script,
};
let cwd = package_dir.join(user_config.cwd_relative_to_package);
let cache_config = match user_config.cache_config {
UserCacheConfig::Disabled { cache: MustBe!(false) } => None,
UserCacheConfig::Enabled { cache: MustBe!(true), envs, pass_through_envs } => {
Some(CacheConfig {
envs: envs.into_iter().collect(),
pass_through_envs: pass_through_envs.into_iter().collect(),
})
}
};
Ok(Self { command: command.into(), cwd, cache_config })
Ok(Self {
command: command.into(),
resolved_options: ResolvedTaskOptions::resolve(user_config.options, package_dir),
})
}
}

// Exact matches for common environment variables
// Referenced from Turborepo's implementation:
// https://github.com/vercel/turborepo/blob/26d309f073ca3ac054109ba0c29c7e230e7caac3/crates/turborepo-lib/src/task_hash.rs#L439
const DEFAULT_PASSTHROUGH_ENVS: &[&str] = &[
// System and shell
"HOME",
"USER",
"TZ",
"LANG",
"SHELL",
"PWD",
"PATH",
// CI/CD environments
"CI",
// Node.js specific
"NODE_OPTIONS",
"COREPACK_HOME",
"NPM_CONFIG_STORE_DIR",
"PNPM_HOME",
// Library paths
"LD_LIBRARY_PATH",
"DYLD_FALLBACK_LIBRARY_PATH",
"LIBPATH",
// Terminal/display
"COLORTERM",
"TERM",
"TERM_PROGRAM",
"DISPLAY",
"FORCE_COLOR",
"NO_COLOR",
// Temporary directories
"TMP",
"TEMP",
// Vercel specific
"VERCEL",
"VERCEL_*",
"NEXT_*",
"USE_OUTPUT_FOR_EDGE_FUNCTIONS",
"NOW_BUILDER",
// Windows specific
"APPDATA",
"PROGRAMDATA",
"SYSTEMROOT",
"SYSTEMDRIVE",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
// IDE specific (exact matches)
"ELECTRON_RUN_AS_NODE",
"JB_INTERPRETER",
"_JETBRAINS_TEST_RUNNER_RUN_SCOPE_TYPE",
"JB_IDE_*",
// VSCode specific
"VSCODE_*",
// Docker specific
"DOCKER_*",
"BUILDKIT_*",
"COMPOSE_*",
// Token patterns
"*_TOKEN",
];
49 changes: 29 additions & 20 deletions crates/vite_task_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub enum UserCacheConfig {

/// Environment variable names to be passed to the task without fingerprinting.
#[serde(default)] // default to empty if omitted
pass_through_envs: Box<[Str]>,
pass_through_envs: Vec<Str>,
},
/// Cache is disabled
Disabled {
Expand All @@ -33,13 +33,10 @@ pub enum UserCacheConfig {
},
}

/// Task configuration defined by user in `vite.config.*`
/// Options for user-defined tasks in `vite.config.*`, excluding the command.
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UserTaskConfig {
/// If None, the script from `package.json` with the same name will be used
pub command: Option<Box<str>>,

pub struct UserTaskOptions {
/// The working directory for the task, relative to the package root (not workspace root).
#[serde(default)] // default to empty if omitted
#[serde(rename = "cwd")]
Expand All @@ -54,22 +51,36 @@ pub struct UserTaskConfig {
pub cache_config: UserCacheConfig,
}

impl UserTaskConfig {
/// The default user task config for package.json scripts.
pub fn package_json_script_default() -> Self {
impl Default for UserTaskOptions {
/// The default user task options for package.json scripts.
fn default() -> Self {
Self {
command: None,
// Runs in the package root
cwd_relative_to_package: RelativePathBuf::default(),
// No dependencies
depends_on: Arc::new([]),
// Caching enabled with no fingerprinted envs
cache_config: UserCacheConfig::Enabled {
cache: MustBe!(true),
envs: Box::new([]),
pass_through_envs: Box::new([]),
pass_through_envs: Vec::new(),
},
}
}
}

/// Full user-defined task configuration in `vite.config.*`, including the command and options.
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UserTaskConfig {
/// If None, the script from `package.json` with the same name will be used
pub command: Option<Box<str>>,

/// Fields other than the command
#[serde(flatten)]
pub options: UserTaskOptions,
}

/// User configuration file structure for `vite.config.*`
#[derive(Debug, Deserialize)]
pub struct UserConfigFile {
Expand All @@ -90,13 +101,8 @@ mod tests {
user_config,
UserTaskConfig {
command: None,
cwd_relative_to_package: "".try_into().unwrap(),
depends_on: Default::default(),
cache_config: UserCacheConfig::Enabled {
cache: MustBe!(true),
envs: Default::default(),
pass_through_envs: Default::default(),
},
// A empty task config (`{}`) should be equivalent to not specifying any config at all (just package.json script)
options: UserTaskOptions::default(),
}
);
}
Expand All @@ -107,7 +113,7 @@ mod tests {
"cwd": "src"
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(user_config.cwd_relative_to_package.as_str(), "src");
assert_eq!(user_config.options.cwd_relative_to_package.as_str(), "src");
}

#[test]
Expand All @@ -116,7 +122,10 @@ mod tests {
"cache": false
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(user_config.cache_config, UserCacheConfig::Disabled { cache: MustBe!(false) });
assert_eq!(
user_config.options.cache_config,
UserCacheConfig::Disabled { cache: MustBe!(false) }
);
}

#[test]
Expand Down
Loading