Skip to content

Commit 619e1d6

Browse files
wan9chicodex
andauthored
feat(client): support prefix env queries
## Motivation Allow runner-aware tools to request environment variables by literal prefix, including prefixes that contain glob metacharacters such as `*`, without treating those characters as wildcards. Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 9a33a73 commit 619e1d6

15 files changed

Lines changed: 305 additions & 87 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
- **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)).
44
- **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)).
5-
- **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)).
5+
- **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)).
66
- **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)).
77
- **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)).
88
- **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))

crates/vite_task/src/session/execute/cache_update.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ fn collect_tracked_env_queries(reports: &Reports) -> anyhow::Result<TrackedEnvQu
352352
vite_task_server::EnvQuery::Glob(pattern) => {
353353
TrackedEnvQuery::Glob(Str::from(pattern.as_ref()))
354354
}
355+
vite_task_server::EnvQuery::Prefix(prefix) => {
356+
TrackedEnvQuery::Prefix(Str::from(prefix.as_ref()))
357+
}
355358
};
356359
tracked_env_queries.insert(query, matches);
357360
}

crates/vite_task/src/session/execute/fingerprint.rs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::{
2929
)]
3030
pub enum TrackedEnvQuery {
3131
Glob(Str),
32+
Prefix(Str),
3233
}
3334

3435
/// Path read access info
@@ -230,9 +231,15 @@ fn match_env_query(
230231
query: &TrackedEnvQuery,
231232
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
232233
) -> anyhow::Result<EnvQueryValidation> {
233-
let TrackedEnvQuery::Glob(pattern) = query;
234-
let glob = vite_glob::env::EnvGlob::new(pattern.as_str())?;
235-
Ok(collect_matching_envs(envs, |name| glob.is_match(name)))
234+
Ok(match query {
235+
TrackedEnvQuery::Glob(pattern) => {
236+
let glob = vite_glob::env::EnvGlob::new(pattern.as_str())?;
237+
collect_matching_envs(envs, |name| glob.is_match(name))
238+
}
239+
TrackedEnvQuery::Prefix(prefix) => {
240+
collect_matching_envs(envs, |name| env_name_starts_with(name, prefix.as_str()))
241+
}
242+
})
236243
}
237244

238245
fn collect_matching_envs(
@@ -262,6 +269,25 @@ enum EnvQueryValidation {
262269
NonUtf8Value(EnvMismatch),
263270
}
264271

272+
#[cfg(not(windows))]
273+
fn env_name_starts_with(name: &str, prefix: &str) -> bool {
274+
name.starts_with(prefix)
275+
}
276+
277+
#[cfg(windows)]
278+
fn env_name_starts_with(name: &str, prefix: &str) -> bool {
279+
let mut name_chars = name.chars();
280+
for prefix_char in prefix.chars() {
281+
let Some(name_char) = name_chars.next() else {
282+
return false;
283+
};
284+
if !name_char.eq_ignore_ascii_case(&prefix_char) {
285+
return false;
286+
}
287+
}
288+
true
289+
}
290+
265291
/// Find the first deterministic difference between stored and current env
266292
/// glob match-sets.
267293
fn first_env_glob_mismatch(
@@ -557,6 +583,32 @@ mod tests {
557583
}
558584
}
559585

586+
#[test]
587+
fn validate_tracked_env_prefix_treats_star_literally() {
588+
let mut tracked_env_queries = BTreeMap::new();
589+
let mut stored_matches = BTreeMap::new();
590+
stored_matches.insert(Str::from("PROBE_*A"), EnvValueHash::new("literal"));
591+
tracked_env_queries.insert(TrackedEnvQuery::Prefix(Str::from("PROBE_*")), stored_matches);
592+
let fingerprint =
593+
PostRunFingerprint { tracked_env_queries, ..PostRunFingerprint::default() };
594+
595+
let mut unfiltered_envs = FxHashMap::default();
596+
unfiltered_envs.insert(
597+
Arc::<OsStr>::from(OsStr::new("PROBE_*A")),
598+
Arc::<OsStr>::from(OsStr::new("literal")),
599+
);
600+
unfiltered_envs.insert(
601+
Arc::<OsStr>::from(OsStr::new("PROBE_XA")),
602+
Arc::<OsStr>::from(OsStr::new("wildcard if interpreted as glob")),
603+
);
604+
605+
let workspace_root = vite_path::current_dir().expect("cwd");
606+
let mismatch =
607+
fingerprint.validate(&workspace_root, &unfiltered_envs).expect("validation succeeds");
608+
609+
assert!(mismatch.is_none());
610+
}
611+
560612
#[test]
561613
fn validate_ignores_non_utf8_tracked_env_glob_names() {
562614
let mut tracked_env_queries = BTreeMap::new();

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_envs.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { getEnvs } from '@voidzero-dev/vite-task-client';
22

3-
const tracked = process.argv[2] === '--untracked' ? false : true;
4-
const matches = getEnvs('PROBE_*', { tracked });
3+
const args = process.argv.slice(2);
4+
const tracked = !args.includes('--untracked');
5+
const prefixIndex = args.indexOf('--prefix');
6+
const query = prefixIndex === -1 ? 'PROBE_*' : { prefix: args[prefixIndex + 1] ?? 'PROBE_' };
7+
const matches = getEnvs(query, { tracked });
58
const sorted = Object.entries(matches).sort(([a], [b]) => a.localeCompare(b));
69

710
for (const [key, value] of sorted) {

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,64 @@ steps = [
425425
], comment = "runner serves only envs matching PROBE_*" },
426426
]
427427

428+
[[e2e]]
429+
name = "fetch_envs_prefix_reads_match_set"
430+
comment = """
431+
Exercises `getEnvs({ prefix: "PROBE_" })`: the tool asks the runner for every env whose name starts with `PROBE_` and prints the served match set.
432+
"""
433+
ignore = true
434+
steps = [
435+
{ argv = [
436+
"vt",
437+
"run",
438+
"fetch-envs-prefix",
439+
], envs = [
440+
[
441+
"PROBE_A",
442+
"a",
443+
],
444+
[
445+
"PROBE_B",
446+
"b",
447+
],
448+
[
449+
"PROBEX",
450+
"not-a-prefix-match",
451+
],
452+
[
453+
"UNRELATED",
454+
"noise",
455+
],
456+
], comment = "runner serves only envs with the literal PROBE_ prefix" },
457+
]
458+
459+
[[e2e]]
460+
name = "fetch_envs_prefix_treats_star_literally"
461+
comment = """
462+
Exercises `getEnvs({ prefix: "PROBE_*" })`: `*` is part of the prefix string, not a glob wildcard.
463+
"""
464+
ignore = true
465+
steps = [
466+
{ argv = [
467+
"vt",
468+
"run",
469+
"fetch-envs-star-prefix",
470+
], envs = [
471+
[
472+
"PROBE_*A",
473+
"literal",
474+
],
475+
[
476+
"PROBE_XA",
477+
"wildcard-if-glob",
478+
],
479+
[
480+
"PROBE_A",
481+
"also-wildcard-if-glob",
482+
],
483+
], comment = "runner serves only envs whose name starts with literal PROBE_*" },
484+
]
485+
428486
[[e2e]]
429487
name = "fetch_envs_tracks_glob_match_set"
430488
comment = """
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# fetch_envs_prefix_reads_match_set
2+
3+
Exercises `getEnvs({ prefix: "PROBE_" })`: the tool asks the runner for every env whose name starts with `PROBE_` and prints the served match set.
4+
5+
## `PROBE_A=a PROBE_B=b PROBEX=not-a-prefix-match UNRELATED=noise vt run fetch-envs-prefix`
6+
7+
runner serves only envs with the literal PROBE_ prefix
8+
9+
```
10+
$ node scripts/fetch_envs.mjs --prefix
11+
PROBE_A=a
12+
PROBE_B=b
13+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# fetch_envs_prefix_treats_star_literally
2+
3+
Exercises `getEnvs({ prefix: "PROBE_*" })`: `*` is part of the prefix string, not a glob wildcard.
4+
5+
## `PROBE_*A=literal PROBE_XA=wildcard-if-glob PROBE_A=also-wildcard-if-glob vt run fetch-envs-star-prefix`
6+
7+
runner serves only envs whose name starts with literal PROBE_*
8+
9+
```
10+
$ node scripts/fetch_envs.mjs --prefix PROBE_*
11+
PROBE_*A=literal
12+
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@
6868
"command": "node scripts/fetch_envs.mjs",
6969
"cache": true
7070
},
71+
"fetch-envs-prefix": {
72+
"command": "node scripts/fetch_envs.mjs --prefix",
73+
"cache": true
74+
},
75+
"fetch-envs-star-prefix": {
76+
"command": "node scripts/fetch_envs.mjs --prefix PROBE_*",
77+
"cache": true
78+
},
7179
"fetch-env-untracked": {
7280
"command": "node scripts/fetch_env.mjs --untracked PROBE_ENV",
7381
"cache": true

crates/vite_task_client/src/lib.rs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,7 @@ pub struct Client {
2626
#[derive(Debug, Clone, Copy)]
2727
pub enum GetEnvsQuery<'a> {
2828
Glob(&'a str),
29-
}
30-
31-
impl<'a> GetEnvsQuery<'a> {
32-
#[must_use]
33-
pub const fn glob(pattern: &'a str) -> Self {
34-
Self::Glob(pattern)
35-
}
36-
}
37-
38-
impl<'a> From<&'a str> for GetEnvsQuery<'a> {
39-
fn from(pattern: &'a str) -> Self {
40-
Self::Glob(pattern)
41-
}
29+
Prefix(&'a str),
4230
}
4331

4432
impl Client {
@@ -128,14 +116,14 @@ impl Client {
128116
///
129117
/// Returns an error if the request or response fails, or if the server
130118
/// rejects a glob query as an invalid glob.
131-
pub fn get_envs<'a>(
119+
pub fn get_envs(
132120
&self,
133-
query: impl Into<GetEnvsQuery<'a>>,
121+
query: GetEnvsQuery<'_>,
134122
tracked: bool,
135123
) -> io::Result<FxHashMap<Arc<OsStr>, Arc<OsStr>>> {
136-
let query = query.into();
137124
let query = match query {
138125
GetEnvsQuery::Glob(pattern) => IpcEnvQuery::Glob(pattern),
126+
GetEnvsQuery::Prefix(prefix) => IpcEnvQuery::Prefix(prefix),
139127
};
140128
self.send(&Request::GetEnvs { query, tracked })?;
141129
let response: GetEnvsResponse = self.recv()?;

crates/vite_task_client_napi/src/lib.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
)]
3131
use std::{collections::HashMap, ffi::OsStr};
3232

33-
use napi::{Error, Result};
33+
use napi::{Either, Error, Result};
3434
use napi_derive::napi;
35-
use vite_task_client::Client;
35+
use vite_task_client::{Client, GetEnvsQuery};
3636

3737
/// Options for [`RunnerClient::get_env`] and [`RunnerClient::get_envs`].
3838
///
@@ -51,6 +51,11 @@ pub struct GetEnvOptions {
5151
pub tracked: Option<bool>,
5252
}
5353

54+
#[napi(object)]
55+
pub struct GetEnvsPrefixQuery {
56+
pub prefix: String,
57+
}
58+
5459
/// Handle returned by [`load`]. Holds the IPC connection and exposes the
5560
/// runner-side operations as instance methods.
5661
#[napi]
@@ -96,20 +101,23 @@ impl RunnerClient {
96101
#[napi]
97102
pub fn get_envs(
98103
&self,
99-
pattern: String,
104+
query: Either<String, GetEnvsPrefixQuery>,
100105
options: Option<GetEnvOptions>,
101106
) -> Result<HashMap<String, String>> {
102107
let tracked = options.and_then(|o| o.tracked).unwrap_or(true);
103-
let matches = self
104-
.client
105-
.get_envs(pattern.as_str(), tracked)
106-
.map_err(|err| err_string(vite_str::format!("{err}")))?;
108+
let matches = match &query {
109+
Either::A(pattern) => {
110+
self.client.get_envs(GetEnvsQuery::Glob(pattern.as_str()), tracked)
111+
}
112+
Either::B(prefix) => {
113+
self.client.get_envs(GetEnvsQuery::Prefix(&prefix.prefix), tracked)
114+
}
115+
}
116+
.map_err(|err| err_string(vite_str::format!("{err}")))?;
107117
let mut result = HashMap::with_capacity(matches.len());
108118
for (name, value) in matches {
109119
let name = name.to_str().ok_or_else(|| {
110-
err_string(vite_str::format!(
111-
"env name matched by pattern {pattern} is not valid UTF-8"
112-
))
120+
err_static("env name matched by getEnvs query is not valid UTF-8")
113121
})?;
114122
let value = value.to_str().ok_or_else(|| {
115123
err_string(vite_str::format!("env value for {name} is not valid UTF-8"))

0 commit comments

Comments
 (0)