diff --git a/CHANGELOG.md b/CHANGELOG.md index 54be3cfa1..8695fe8c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ - **Added** `dependsOn` can now run the specified task in each direct workspace package listed in `dependencies`, `devDependencies`, or `peerDependencies`; for example, `{ "task": "build", "from": ["dependencies", "devDependencies"] }` runs `build` in each direct dependency and dev dependency ([#467](https://github.com/voidzero-dev/vite-task/pull/467), [#469](https://github.com/voidzero-dev/vite-task/pull/469)). - **Added** First-party support for caching `vite build` with zero cache config, giving Vite projects correct cache hits out of the box ([vitejs/vite#22453](https://github.com/vitejs/vite/pull/22453)). -- **Added** [`@voidzero-dev/vite-task-client`](https://npmx.dev/package/@voidzero-dev/vite-task-client), allowing tools to report cache information to Vite Task at runtime so users do not need to configure it manually ([#441](https://github.com/voidzero-dev/vite-task/pull/441), [#454](https://github.com/voidzero-dev/vite-task/pull/454), [#449](https://github.com/voidzero-dev/vite-task/pull/449), [#450](https://github.com/voidzero-dev/vite-task/pull/450), [#458](https://github.com/voidzero-dev/vite-task/pull/458), [#431](https://github.com/voidzero-dev/vite-task/pull/431), [#459](https://github.com/voidzero-dev/vite-task/pull/459)). +- **Added** [`@voidzero-dev/vite-task-client`](https://npmx.dev/package/@voidzero-dev/vite-task-client), allowing tools to report cache information to Vite Task at runtime so users do not need to configure it manually, including literal-prefix env lookups with `getEnvs({ prefix: "..." })` ([#441](https://github.com/voidzero-dev/vite-task/pull/441), [#454](https://github.com/voidzero-dev/vite-task/pull/454), [#449](https://github.com/voidzero-dev/vite-task/pull/449), [#450](https://github.com/voidzero-dev/vite-task/pull/450), [#458](https://github.com/voidzero-dev/vite-task/pull/458), [#431](https://github.com/voidzero-dev/vite-task/pull/431), [#459](https://github.com/voidzero-dev/vite-task/pull/459), [#472](https://github.com/voidzero-dev/vite-task/pull/472)). - **Changed** Cached tasks now restore automatically tracked output files by default; use `output: []` to disable restoration ([#460](https://github.com/voidzero-dev/vite-task/pull/460), [#461](https://github.com/voidzero-dev/vite-task/pull/461)). - **Changed** Environment values in task cache fingerprints are now stored only as SHA-256 digests, and env-related cache miss details report names without values ([#455](https://github.com/voidzero-dev/vite-task/pull/455)). - **Fixed** Prefix environment assignments like `PATH=... command` now affect executable lookup during task planning, so tools provided only by the prefixed `PATH` can be resolved correctly ([#440](https://github.com/voidzero-dev/vite-task/pull/440)) diff --git a/crates/vite_task/src/session/execute/cache_update.rs b/crates/vite_task/src/session/execute/cache_update.rs index 3369230a9..a1ab1ef5f 100644 --- a/crates/vite_task/src/session/execute/cache_update.rs +++ b/crates/vite_task/src/session/execute/cache_update.rs @@ -352,6 +352,9 @@ fn collect_tracked_env_queries(reports: &Reports) -> anyhow::Result { TrackedEnvQuery::Glob(Str::from(pattern.as_ref())) } + vite_task_server::EnvQuery::Prefix(prefix) => { + TrackedEnvQuery::Prefix(Str::from(prefix.as_ref())) + } }; tracked_env_queries.insert(query, matches); } diff --git a/crates/vite_task/src/session/execute/fingerprint.rs b/crates/vite_task/src/session/execute/fingerprint.rs index 9d4a425e0..11173d40e 100644 --- a/crates/vite_task/src/session/execute/fingerprint.rs +++ b/crates/vite_task/src/session/execute/fingerprint.rs @@ -29,6 +29,7 @@ use crate::{ )] pub enum TrackedEnvQuery { Glob(Str), + Prefix(Str), } /// Path read access info @@ -230,9 +231,15 @@ fn match_env_query( query: &TrackedEnvQuery, envs: &FxHashMap, Arc>, ) -> anyhow::Result { - let TrackedEnvQuery::Glob(pattern) = query; - let glob = vite_glob::env::EnvGlob::new(pattern.as_str())?; - Ok(collect_matching_envs(envs, |name| glob.is_match(name))) + Ok(match query { + TrackedEnvQuery::Glob(pattern) => { + let glob = vite_glob::env::EnvGlob::new(pattern.as_str())?; + collect_matching_envs(envs, |name| glob.is_match(name)) + } + TrackedEnvQuery::Prefix(prefix) => { + collect_matching_envs(envs, |name| env_name_starts_with(name, prefix.as_str())) + } + }) } fn collect_matching_envs( @@ -262,6 +269,25 @@ enum EnvQueryValidation { NonUtf8Value(EnvMismatch), } +#[cfg(not(windows))] +fn env_name_starts_with(name: &str, prefix: &str) -> bool { + name.starts_with(prefix) +} + +#[cfg(windows)] +fn env_name_starts_with(name: &str, prefix: &str) -> bool { + let mut name_chars = name.chars(); + for prefix_char in prefix.chars() { + let Some(name_char) = name_chars.next() else { + return false; + }; + if !name_char.eq_ignore_ascii_case(&prefix_char) { + return false; + } + } + true +} + /// Find the first deterministic difference between stored and current env /// glob match-sets. fn first_env_glob_mismatch( @@ -557,6 +583,32 @@ mod tests { } } + #[test] + fn validate_tracked_env_prefix_treats_star_literally() { + let mut tracked_env_queries = BTreeMap::new(); + let mut stored_matches = BTreeMap::new(); + stored_matches.insert(Str::from("PROBE_*A"), EnvValueHash::new("literal")); + tracked_env_queries.insert(TrackedEnvQuery::Prefix(Str::from("PROBE_*")), stored_matches); + let fingerprint = + PostRunFingerprint { tracked_env_queries, ..PostRunFingerprint::default() }; + + let mut unfiltered_envs = FxHashMap::default(); + unfiltered_envs.insert( + Arc::::from(OsStr::new("PROBE_*A")), + Arc::::from(OsStr::new("literal")), + ); + unfiltered_envs.insert( + Arc::::from(OsStr::new("PROBE_XA")), + Arc::::from(OsStr::new("wildcard if interpreted as glob")), + ); + + let workspace_root = vite_path::current_dir().expect("cwd"); + let mismatch = + fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds"); + + assert!(mismatch.is_none()); + } + #[test] fn validate_ignores_non_utf8_tracked_env_glob_names() { let mut tracked_env_queries = BTreeMap::new(); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs index f59762ce8..66342b899 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs @@ -1,7 +1,10 @@ import { getEnvs } from '@voidzero-dev/vite-task-client'; -const tracked = process.argv[2] === '--untracked' ? false : true; -const matches = getEnvs('PROBE_*', { tracked }); +const args = process.argv.slice(2); +const tracked = !args.includes('--untracked'); +const prefixIndex = args.indexOf('--prefix'); +const query = prefixIndex === -1 ? 'PROBE_*' : { prefix: args[prefixIndex + 1] ?? 'PROBE_' }; +const matches = getEnvs(query, { tracked }); const sorted = Object.entries(matches).sort(([a], [b]) => a.localeCompare(b)); for (const [key, value] of sorted) { diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml index a1a04822e..852b5ff4f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml @@ -425,6 +425,64 @@ steps = [ ], comment = "runner serves only envs matching PROBE_*" }, ] +[[e2e]] +name = "fetch_envs_prefix_reads_match_set" +comment = """ +Exercises `getEnvs({ prefix: "PROBE_" })`: the tool asks the runner for every env whose name starts with `PROBE_` and prints the served match set. +""" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "fetch-envs-prefix", + ], envs = [ + [ + "PROBE_A", + "a", + ], + [ + "PROBE_B", + "b", + ], + [ + "PROBEX", + "not-a-prefix-match", + ], + [ + "UNRELATED", + "noise", + ], + ], comment = "runner serves only envs with the literal PROBE_ prefix" }, +] + +[[e2e]] +name = "fetch_envs_prefix_treats_star_literally" +comment = """ +Exercises `getEnvs({ prefix: "PROBE_*" })`: `*` is part of the prefix string, not a glob wildcard. +""" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "fetch-envs-star-prefix", + ], envs = [ + [ + "PROBE_*A", + "literal", + ], + [ + "PROBE_XA", + "wildcard-if-glob", + ], + [ + "PROBE_A", + "also-wildcard-if-glob", + ], + ], comment = "runner serves only envs whose name starts with literal PROBE_*" }, +] + [[e2e]] name = "fetch_envs_tracks_glob_match_set" comment = """ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_prefix_reads_match_set.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_prefix_reads_match_set.md new file mode 100644 index 000000000..833953897 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_prefix_reads_match_set.md @@ -0,0 +1,13 @@ +# fetch_envs_prefix_reads_match_set + +Exercises `getEnvs({ prefix: "PROBE_" })`: the tool asks the runner for every env whose name starts with `PROBE_` and prints the served match set. + +## `PROBE_A=a PROBE_B=b PROBEX=not-a-prefix-match UNRELATED=noise vt run fetch-envs-prefix` + +runner serves only envs with the literal PROBE_ prefix + +``` +$ node scripts/fetch_envs.mjs --prefix +PROBE_A=a +PROBE_B=b +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_prefix_treats_star_literally.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_prefix_treats_star_literally.md new file mode 100644 index 000000000..01f4b2d18 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/fetch_envs_prefix_treats_star_literally.md @@ -0,0 +1,12 @@ +# fetch_envs_prefix_treats_star_literally + +Exercises `getEnvs({ prefix: "PROBE_*" })`: `*` is part of the prefix string, not a glob wildcard. + +## `PROBE_*A=literal PROBE_XA=wildcard-if-glob PROBE_A=also-wildcard-if-glob vt run fetch-envs-star-prefix` + +runner serves only envs whose name starts with literal PROBE_* + +``` +$ node scripts/fetch_envs.mjs --prefix PROBE_* +PROBE_*A=literal +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json index d4dcaeed8..dbbdfeb72 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json @@ -68,6 +68,14 @@ "command": "node scripts/fetch_envs.mjs", "cache": true }, + "fetch-envs-prefix": { + "command": "node scripts/fetch_envs.mjs --prefix", + "cache": true + }, + "fetch-envs-star-prefix": { + "command": "node scripts/fetch_envs.mjs --prefix PROBE_*", + "cache": true + }, "fetch-env-untracked": { "command": "node scripts/fetch_env.mjs --untracked PROBE_ENV", "cache": true diff --git a/crates/vite_task_client/src/lib.rs b/crates/vite_task_client/src/lib.rs index 5b0ba657d..28740c861 100644 --- a/crates/vite_task_client/src/lib.rs +++ b/crates/vite_task_client/src/lib.rs @@ -26,19 +26,7 @@ pub struct Client { #[derive(Debug, Clone, Copy)] pub enum GetEnvsQuery<'a> { Glob(&'a str), -} - -impl<'a> GetEnvsQuery<'a> { - #[must_use] - pub const fn glob(pattern: &'a str) -> Self { - Self::Glob(pattern) - } -} - -impl<'a> From<&'a str> for GetEnvsQuery<'a> { - fn from(pattern: &'a str) -> Self { - Self::Glob(pattern) - } + Prefix(&'a str), } impl Client { @@ -128,14 +116,14 @@ impl Client { /// /// Returns an error if the request or response fails, or if the server /// rejects a glob query as an invalid glob. - pub fn get_envs<'a>( + pub fn get_envs( &self, - query: impl Into>, + query: GetEnvsQuery<'_>, tracked: bool, ) -> io::Result, Arc>> { - let query = query.into(); let query = match query { GetEnvsQuery::Glob(pattern) => IpcEnvQuery::Glob(pattern), + GetEnvsQuery::Prefix(prefix) => IpcEnvQuery::Prefix(prefix), }; self.send(&Request::GetEnvs { query, tracked })?; let response: GetEnvsResponse = self.recv()?; diff --git a/crates/vite_task_client_napi/src/lib.rs b/crates/vite_task_client_napi/src/lib.rs index a437ee603..81f4a0058 100644 --- a/crates/vite_task_client_napi/src/lib.rs +++ b/crates/vite_task_client_napi/src/lib.rs @@ -30,9 +30,9 @@ )] use std::{collections::HashMap, ffi::OsStr}; -use napi::{Error, Result}; +use napi::{Either, Error, Result}; use napi_derive::napi; -use vite_task_client::Client; +use vite_task_client::{Client, GetEnvsQuery}; /// Options for [`RunnerClient::get_env`] and [`RunnerClient::get_envs`]. /// @@ -51,6 +51,11 @@ pub struct GetEnvOptions { pub tracked: Option, } +#[napi(object)] +pub struct GetEnvsPrefixQuery { + pub prefix: String, +} + /// Handle returned by [`load`]. Holds the IPC connection and exposes the /// runner-side operations as instance methods. #[napi] @@ -96,20 +101,23 @@ impl RunnerClient { #[napi] pub fn get_envs( &self, - pattern: String, + query: Either, options: Option, ) -> Result> { let tracked = options.and_then(|o| o.tracked).unwrap_or(true); - let matches = self - .client - .get_envs(pattern.as_str(), tracked) - .map_err(|err| err_string(vite_str::format!("{err}")))?; + let matches = match &query { + Either::A(pattern) => { + self.client.get_envs(GetEnvsQuery::Glob(pattern.as_str()), tracked) + } + Either::B(prefix) => { + self.client.get_envs(GetEnvsQuery::Prefix(&prefix.prefix), tracked) + } + } + .map_err(|err| err_string(vite_str::format!("{err}")))?; let mut result = HashMap::with_capacity(matches.len()); for (name, value) in matches { let name = name.to_str().ok_or_else(|| { - err_string(vite_str::format!( - "env name matched by pattern {pattern} is not valid UTF-8" - )) + err_static("env name matched by getEnvs query is not valid UTF-8") })?; let value = value.to_str().ok_or_else(|| { err_string(vite_str::format!("env value for {name} is not valid UTF-8")) diff --git a/crates/vite_task_ipc_shared/src/lib.rs b/crates/vite_task_ipc_shared/src/lib.rs index 3b27b6145..8c3f7fb15 100644 --- a/crates/vite_task_ipc_shared/src/lib.rs +++ b/crates/vite_task_ipc_shared/src/lib.rs @@ -38,6 +38,7 @@ pub enum Request<'a> { #[derive(Debug, Clone, Copy, SchemaWrite, SchemaRead)] pub enum EnvQuery<'a> { Glob(&'a str), + Prefix(&'a str), } #[derive(Debug, SchemaWrite, SchemaRead)] diff --git a/crates/vite_task_server/src/lib.rs b/crates/vite_task_server/src/lib.rs index 7069d6fba..b33ac877a 100644 --- a/crates/vite_task_server/src/lib.rs +++ b/crates/vite_task_server/src/lib.rs @@ -82,21 +82,7 @@ pub struct Recorder { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum EnvQuery { Glob(Arc), -} - -impl EnvQuery { - #[must_use] - pub fn glob(pattern: &str) -> Self { - Self::Glob(Arc::from(pattern)) - } -} - -impl From<&IpcEnvQuery<'_>> for EnvQuery { - fn from(query: &IpcEnvQuery<'_>) -> Self { - match query { - IpcEnvQuery::Glob(pattern) => Self::glob(pattern), - } - } + Prefix(Arc), } /// A record of a tracked env query made via `get_envs`. @@ -169,24 +155,41 @@ impl Handler for Recorder { query: &IpcEnvQuery<'_>, tracked: bool, ) -> Result, Arc>, vite_glob::env::EnvGlobError> { - let key = EnvQuery::from(query); + let key = match query { + IpcEnvQuery::Glob(pattern) => EnvQuery::Glob(Arc::from(*pattern)), + IpcEnvQuery::Prefix(prefix) => EnvQuery::Prefix(Arc::from(*prefix)), + }; if let Some(existing) = self.tracked_get_envs.get(&key) { return Ok(existing.matches.clone()); } - let IpcEnvQuery::Glob(pattern) = query; - let glob = vite_glob::env::EnvGlob::new(pattern)?; - let matches: FxHashMap, Arc> = self - .envs - .iter() - .filter_map(|(name, value)| { - let name_str = name.to_str()?; - if glob.is_match(name_str) { - Some((Arc::clone(name), Arc::clone(value))) - } else { - None - } - }) - .collect(); + let matches: FxHashMap, Arc> = match query { + IpcEnvQuery::Glob(pattern) => { + let glob = vite_glob::env::EnvGlob::new(pattern)?; + self.envs + .iter() + .filter_map(|(name, value)| { + let name_str = name.to_str()?; + if glob.is_match(name_str) { + Some((Arc::clone(name), Arc::clone(value))) + } else { + None + } + }) + .collect() + } + IpcEnvQuery::Prefix(prefix) => self + .envs + .iter() + .filter_map(|(name, value)| { + let name_str = name.to_str()?; + if env_name_starts_with(name_str, prefix) { + Some((Arc::clone(name), Arc::clone(value))) + } else { + None + } + }) + .collect(), + }; if tracked { self.tracked_get_envs.insert(key, EnvQueryRecord { matches: matches.clone() }); } @@ -194,6 +197,25 @@ impl Handler for Recorder { } } +#[cfg(not(windows))] +fn env_name_starts_with(name: &str, prefix: &str) -> bool { + name.starts_with(prefix) +} + +#[cfg(windows)] +fn env_name_starts_with(name: &str, prefix: &str) -> bool { + let mut name_chars = name.chars(); + for prefix_char in prefix.chars() { + let Some(name_char) = name_chars.next() else { + return false; + }; + if !name_char.eq_ignore_ascii_case(&prefix_char) { + return false; + } + } + true +} + /// Handle to a running IPC server. /// /// `driver` must be polled to accept clients and handle messages. It resolves @@ -431,7 +453,10 @@ async fn handle_client(mut stream: Stream, handler: &RefCell) -> } Request::GetEnvs { query, tracked } => { let matches = handler.borrow_mut().get_envs(&query, tracked).map_err(|source| { - let IpcEnvQuery::Glob(pattern) = query; + let pattern = match query { + IpcEnvQuery::Glob(pattern) => pattern, + IpcEnvQuery::Prefix(prefix) => prefix, + }; Error::InvalidGlob(Box::new(InvalidGlob { pattern: Box::::from(pattern), source, diff --git a/crates/vite_task_server/tests/integration.rs b/crates/vite_task_server/tests/integration.rs index 4b37f0bc5..680a47f8f 100644 --- a/crates/vite_task_server/tests/integration.rs +++ b/crates/vite_task_server/tests/integration.rs @@ -13,7 +13,7 @@ type RawStream = std::os::unix::net::UnixStream; type RawStream = std::fs::File; use rustc_hash::FxHashMap; use tokio::runtime::Builder; -use vite_task_client::Client; +use vite_task_client::{Client, GetEnvsQuery}; use vite_task_ipc_shared::Request; use vite_task_server::{EnvQuery, Error, Recorder, Reports, ServerHandle, serve}; @@ -221,7 +221,7 @@ fn get_envs_returns_matching_entries() { env_map(&[("PROBE_A", "alpha"), ("PROBE_B", "beta"), ("UNRELATED", "noise")]), |envs| { let client = connect(&envs); - let matches = client.get_envs("PROBE_*", true).unwrap(); + let matches = client.get_envs(GetEnvsQuery::Glob("PROBE_*"), true).unwrap(); assert_eq!(matches.len(), 2); assert_eq!( matches.get(OsStr::new("PROBE_A")).map(AsRef::as_ref), @@ -237,22 +237,50 @@ fn get_envs_returns_matching_entries() { .expect("driver returned error"); assert!(!reports.cache_disabled); - let glob = reports.tracked_get_envs.get(&EnvQuery::glob("PROBE_*")).expect("glob recorded"); + let glob = + reports.tracked_get_envs.get(&EnvQuery::Glob(Arc::from("PROBE_*"))).expect("glob recorded"); assert_eq!(glob.matches.len(), 2); } +#[test] +fn get_envs_prefix_treats_star_literally() { + let reports = run_with_server( + env_map(&[ + ("PROBE_*A", "literal"), + ("PROBE_XA", "wildcard if interpreted as glob"), + ("PROBE_A", "also wildcard if interpreted as glob"), + ]), + |envs| { + let client = connect(&envs); + let matches = client.get_envs(GetEnvsQuery::Prefix("PROBE_*"), true).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!( + matches.get(OsStr::new("PROBE_*A")).map(AsRef::as_ref), + Some(OsStr::new("literal")) + ); + }, + ) + .expect("driver returned error"); + + let prefix = reports + .tracked_get_envs + .get(&EnvQuery::Prefix(Arc::from("PROBE_*"))) + .expect("prefix query recorded"); + assert_eq!(prefix.matches.len(), 1); +} + #[test] fn get_envs_empty_match_set_is_returned() { let reports = run_with_server(env_map(&[("FOO", "x"), ("BAR", "y")]), |envs| { let client = connect(&envs); - let matches = client.get_envs("PROBE_*", false).unwrap(); + let matches = client.get_envs(GetEnvsQuery::Glob("PROBE_*"), false).unwrap(); assert!(matches.is_empty()); }) .expect("driver returned error"); assert!(!reports.cache_disabled); assert!( - !reports.tracked_get_envs.contains_key(&EnvQuery::glob("PROBE_*")), + !reports.tracked_get_envs.contains_key(&EnvQuery::Glob(Arc::from("PROBE_*"))), "untracked getEnvs calls are not recorded" ); } @@ -261,15 +289,16 @@ fn get_envs_empty_match_set_is_returned() { fn get_envs_untracked_then_tracked_records_once() { let reports = run_with_server(env_map(&[("PROBE_A", "alpha")]), |envs| { let client = connect(&envs); - let first = client.get_envs("PROBE_*", false).unwrap(); - let second = client.get_envs("PROBE_*", true).unwrap(); - let third = client.get_envs("PROBE_*", false).unwrap(); + let first = client.get_envs(GetEnvsQuery::Glob("PROBE_*"), false).unwrap(); + let second = client.get_envs(GetEnvsQuery::Glob("PROBE_*"), true).unwrap(); + let third = client.get_envs(GetEnvsQuery::Glob("PROBE_*"), false).unwrap(); assert_eq!(first, second); assert_eq!(second, third); }) .expect("driver returned error"); - let glob = reports.tracked_get_envs.get(&EnvQuery::glob("PROBE_*")).expect("glob recorded"); + let glob = + reports.tracked_get_envs.get(&EnvQuery::Glob(Arc::from("PROBE_*"))).expect("glob recorded"); assert_eq!(glob.matches.len(), 1); } @@ -277,7 +306,9 @@ fn get_envs_untracked_then_tracked_records_once() { fn get_envs_invalid_pattern_surfaces_error() { let err = run_with_server(env_map(&[]), |envs| { let client = connect(&envs); - let send_err = client.get_envs("{unclosed", true).expect_err("server should reject"); + let send_err = client + .get_envs(GetEnvsQuery::Glob("{unclosed"), true) + .expect_err("server should reject"); assert_eq!(send_err.kind(), io::ErrorKind::UnexpectedEof); }) .expect_err("driver should surface the protocol error"); diff --git a/packages/vite-task-client/src/index.d.ts b/packages/vite-task-client/src/index.d.ts index c529f6430..f61f2882a 100644 --- a/packages/vite-task-client/src/index.d.ts +++ b/packages/vite-task-client/src/index.d.ts @@ -42,8 +42,10 @@ export function getEnv(name: string, options?: { tracked?: boolean; }): string | undefined; /** - * Ask the runner for every env whose name matches `pattern` (a glob, e.g. - * `VITE_*`) and return the match-set as a plain object. + * Ask the runner for matching envs and return the match-set as a plain object. + * + * Pass a glob string (e.g. `VITE_*`) to use glob matching, or pass + * `{ prefix: 'VITE_' }` to match env names by literal prefix. * * With `tracked: true` (the default) the runner records the pattern as a * dependency, so adding, removing, or changing a matching env invalidates @@ -52,10 +54,14 @@ export function getEnv(name: string, options?: { * Has no effect on `process.env`; the caller decides what to do with the * returned values. Returns an empty object when not running inside a runner. * - * @param {string} pattern - * @param {{ tracked?: boolean }} [options] + * @param {GetEnvsQuery} query + * @param {GetEnvOptions} [options] * @returns {Record} */ -export function getEnvs(pattern: string, options?: { +export function getEnvs(query: GetEnvsQuery, options?: GetEnvOptions): Record; +export type GetEnvOptions = { tracked?: boolean; -}): Record; +}; +export type GetEnvsQuery = string | { + prefix: string; +}; diff --git a/packages/vite-task-client/src/index.js b/packages/vite-task-client/src/index.js index 216271f6b..dd8771159 100644 --- a/packages/vite-task-client/src/index.js +++ b/packages/vite-task-client/src/index.js @@ -5,6 +5,14 @@ import { createRequire } from 'node:module'; +/** + * @typedef {{ tracked?: boolean }} GetEnvOptions + */ + +/** + * @typedef {string | { prefix: string }} GetEnvsQuery + */ + /** * Methods exposed by the napi addon. Keep this shape in sync with the * `RunnerClient` returned by `load()` in @@ -15,8 +23,8 @@ import { createRequire } from 'node:module'; * ignoreInput: (path: string) => void, * ignoreOutput: (path: string) => void, * disableCache: () => void, - * getEnv: (name: string, options?: { tracked?: boolean }) => string | undefined, - * getEnvs: (pattern: string, options?: { tracked?: boolean }) => Record, + * getEnv: (name: string, options?: GetEnvOptions) => string | undefined, + * getEnvs: (query: GetEnvsQuery, options?: GetEnvOptions) => Record, * } | null | undefined} */ let addon; @@ -99,8 +107,10 @@ export function getEnv(name, options) { } /** - * Ask the runner for every env whose name matches `pattern` (a glob, e.g. - * `VITE_*`) and return the match-set as a plain object. + * Ask the runner for matching envs and return the match-set as a plain object. + * + * Pass a glob string (e.g. `VITE_*`) to use glob matching, or pass + * `{ prefix: 'VITE_' }` to match env names by literal prefix. * * With `tracked: true` (the default) the runner records the pattern as a * dependency, so adding, removing, or changing a matching env invalidates @@ -109,12 +119,12 @@ export function getEnv(name, options) { * Has no effect on `process.env`; the caller decides what to do with the * returned values. Returns an empty object when not running inside a runner. * - * @param {string} pattern - * @param {{ tracked?: boolean }} [options] + * @param {GetEnvsQuery} query + * @param {GetEnvOptions} [options] * @returns {Record} */ -export function getEnvs(pattern, options) { +export function getEnvs(query, options) { const a = load(); if (!a) return {}; - return a.getEnvs(pattern, options); + return a.getEnvs(query, options); }