-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathuser.rs
More file actions
457 lines (402 loc) · 16.2 KB
/
user.rs
File metadata and controls
457 lines (402 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Configuration structures for user-defined tasks in `vite.config.*`
use std::sync::Arc;
use monostate::MustBe;
use rustc_hash::FxHashMap;
use serde::Deserialize;
#[cfg(all(test, not(clippy)))]
use ts_rs::TS;
use vite_path::RelativePathBuf;
use vite_str::Str;
/// Cache-related fields of a task defined by user in `vite.config.*`
#[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(untagged, deny_unknown_fields, rename_all = "camelCase")]
pub enum UserCacheConfig {
/// Cache is enabled
Enabled {
/// Whether to cache the task
#[cfg_attr(all(test, not(clippy)), ts(type = "true", optional))]
cache: Option<MustBe!(true)>,
#[serde(flatten)]
enabled_cache_config: EnabledCacheConfig,
},
/// Cache is disabled
Disabled {
/// Whether to cache the task
#[cfg_attr(all(test, not(clippy)), ts(type = "false"))]
cache: MustBe!(false),
},
}
impl UserCacheConfig {
/// Create an enabled cache config with the given `EnabledCacheConfig`.
#[must_use]
pub const fn with_config(config: EnabledCacheConfig) -> Self {
Self::Enabled { cache: Some(MustBe!(true)), enabled_cache_config: config }
}
/// Create a disabled cache config.
#[must_use]
pub const fn disabled() -> Self {
Self::Disabled { cache: MustBe!(false) }
}
}
/// Cache configuration fields when caching is enabled
#[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")]
pub struct EnabledCacheConfig {
/// Environment variable names to be fingerprinted and passed to the task.
pub envs: Option<Box<[Str]>>,
/// Environment variable names to be passed to the task without fingerprinting.
pub pass_through_envs: Option<Vec<Str>>,
}
/// Options for user-defined tasks 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")]
pub struct UserTaskOptions {
/// The working directory for the task, relative to the package root (not workspace root).
#[serde(rename = "cwd")]
pub cwd_relative_to_package: Option<RelativePathBuf>,
/// Dependencies of this task. Use `package-name#task-name` to refer to tasks in other packages.
pub depends_on: Option<Arc<[Str]>>,
/// Cache-related fields
#[serde(flatten)]
pub cache_config: UserCacheConfig,
}
impl Default for UserTaskOptions {
/// The default user task options for package.json scripts.
fn default() -> Self {
Self {
// Runs in the package root
cwd_relative_to_package: None,
// No dependencies
depends_on: None,
// Caching enabled with no fingerprinted envs
cache_config: UserCacheConfig::Enabled {
cache: None,
enabled_cache_config: EnabledCacheConfig { envs: None, pass_through_envs: None },
},
}
}
}
/// Full user-defined task configuration in `vite.config.*`, including the command and options.
#[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, rename = "Task"))]
#[serde(rename_all = "camelCase")]
pub struct UserTaskConfig {
/// The command to run for the task.
///
/// If omitted, 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,
}
/// Root-level cache configuration.
///
/// Controls caching behavior for the entire workspace.
///
/// - `true` is equivalent to `{ scripts: true, tasks: true }` — enables caching for both
/// package.json scripts and task entries.
/// - `false` is equivalent to `{ scripts: false, tasks: false }` — disables all caching.
/// - When omitted, defaults to `{ scripts: false, tasks: true }`.
///
/// 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.
#[derive(Debug, Clone, Copy, 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(untagged, deny_unknown_fields)]
pub enum UserGlobalCacheConfig {
Bool(bool),
/// Detailed cache configuration with separate control for scripts and tasks.
Detailed {
/// 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.
///
/// Default: `false`
scripts: Option<bool>,
/// Global cache kill switch for task entries.
///
/// When `false`, overrides all tasks to disable caching, even tasks with `cache: true`.
/// When `true`, respects each task's individual `cache` setting
/// (each task's `cache` defaults to `true` if omitted).
///
/// Default: `true`
tasks: Option<bool>,
},
}
/// Resolved global cache configuration with concrete boolean values.
#[derive(Debug, Clone, Copy)]
pub struct ResolvedGlobalCacheConfig {
pub scripts: bool,
pub tasks: bool,
}
impl ResolvedGlobalCacheConfig {
/// Resolve from an optional user config, using defaults when `None`.
///
/// Default: `{ scripts: false, tasks: true }`
#[must_use]
pub fn resolve_from(config: Option<&UserGlobalCacheConfig>) -> Self {
match config {
None => Self { scripts: false, tasks: true },
Some(UserGlobalCacheConfig::Bool(true)) => Self { scripts: true, tasks: true },
Some(UserGlobalCacheConfig::Bool(false)) => Self { scripts: false, tasks: false },
Some(UserGlobalCacheConfig::Detailed { scripts, tasks }) => {
Self { scripts: scripts.unwrap_or(false), tasks: tasks.unwrap_or(true) }
}
}
}
}
/// User configuration structure for `run` field in `vite.config.*`
#[derive(Debug, Default, Deserialize)]
// 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, rename = "RunConfig"))]
#[serde(rename_all = "camelCase")]
pub struct UserRunConfig {
/// Root-level cache configuration.
///
/// 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 cache: Option<UserGlobalCacheConfig>,
/// Task definitions
pub tasks: Option<FxHashMap<Str, UserTaskConfig>>,
}
impl UserRunConfig {
/// TypeScript type definitions for user run configuration.
pub const TS_TYPE: &str = include_str!("../../run-config.ts");
/// Generates TypeScript type definitions for user task configuration.
///
/// # Panics
///
/// Panics if `oxfmt` is not found in `packages/tools`, if the formatter process
/// fails to spawn or write, or if the output is not valid UTF-8.
#[cfg(all(test, not(clippy)))]
#[must_use]
// test code: uses std types for convenience
#[expect(clippy::disallowed_types, reason = "test code uses std types for convenience")]
pub fn generate_ts_definition() -> String {
use std::{
any::TypeId,
collections::HashSet,
io::Write,
process::{Command, Stdio},
};
use ts_rs::TypeVisitor;
struct DeclCollector {
decls: Vec<String>,
visited: HashSet<TypeId>,
}
impl TypeVisitor for DeclCollector {
fn visit<T: TS + 'static + ?Sized>(&mut self) {
if !self.visited.insert(TypeId::of::<T>()) {
return;
}
// Only collect declarations from types that are exportable
// (i.e., have an output path - built-in types like HashMap don't)
if T::output_path().is_some() {
self.decls.push(T::decl());
}
// Recursively visit dependencies of T
T::visit_dependencies(self);
}
}
let mut collector = DeclCollector { decls: Vec::new(), visited: HashSet::new() };
Self::visit_dependencies(&mut collector);
// Sort declarations for deterministic output order
collector.decls.sort();
// Header
let mut types: String =
"// This file is auto-generated by `cargo test`. Do not edit manually.\n\n".into();
// Export all types
let dep_types: String = collector
.decls
.iter()
.map(|decl| vite_str::format!("export {decl}"))
.collect::<Vec<_>>()
.join("\n\n");
types.push_str(&dep_types);
// Export the main type
types.push_str("\n\nexport ");
types.push_str(&Self::decl());
// Format using oxfmt from packages/tools
let workspace_root =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap();
let tools_path = workspace_root.join("packages/tools/node_modules/.bin");
let oxfmt_path = which::which_in("oxfmt", Some(&tools_path), &tools_path)
.expect("oxfmt not found in packages/tools");
let mut child = Command::new(oxfmt_path)
.arg("--stdin-filepath=run-config.ts")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("failed to spawn oxfmt");
child
.stdin
.take()
.unwrap()
.write_all(types.as_bytes())
.expect("failed to write to oxfmt stdin");
let output = child.wait_with_output().expect("failed to read oxfmt output");
assert!(output.status.success(), "oxfmt failed");
String::from_utf8(output.stdout).expect("oxfmt output is not valid UTF-8")
}
}
#[cfg(all(test, not(clippy)))]
mod ts_tests {
// test code: uses std types for convenience
#[expect(clippy::disallowed_types, reason = "test code uses std types for convenience")]
use std::{env, path::PathBuf};
use super::UserRunConfig;
#[test]
// test code: uses std types for convenience
#[expect(
clippy::disallowed_methods,
clippy::disallowed_types,
reason = "test code uses std types for convenience"
)]
fn typescript_generation() {
let file_path =
PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("run-config.ts");
let ts = UserRunConfig::generate_ts_definition().replace('\r', "");
if env::var("VT_UPDATE_TS_TYPES").unwrap_or_default() == "1" {
std::fs::write(&file_path, ts).unwrap();
} else {
let existing_ts =
std::fs::read_to_string(&file_path).unwrap_or_default().replace('\r', "");
pretty_assertions::assert_eq!(
ts,
existing_ts,
"Generated TypeScript types do not match the existing ones. If you made changes to the types, please set VT_UPDATE_TS_TYPES=1 and run the tests again to update the TypeScript definitions."
);
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_defaults() {
let user_config_json = json!({});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(
user_config,
UserTaskConfig {
command: None,
// A empty task config (`{}`) should be equivalent to not specifying any config at all (just package.json script)
options: UserTaskOptions::default(),
}
);
}
#[test]
fn test_cwd_rename() {
let user_config_json = json!({
"cwd": "src"
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(user_config.options.cwd_relative_to_package.as_ref().unwrap().as_str(), "src");
}
#[test]
fn test_cache_disabled() {
let user_config_json = json!({
"cache": false
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(
user_config.options.cache_config,
UserCacheConfig::Disabled { cache: MustBe!(false) }
);
}
#[test]
fn test_cache_explicitly_enabled() {
let user_config_json = json!({
"cache": true,
"envs": ["NODE_ENV"],
"passThroughEnvs": ["FOO"],
});
assert_eq!(
serde_json::from_value::<UserCacheConfig>(user_config_json).unwrap(),
UserCacheConfig::Enabled {
cache: Some(MustBe!(true)),
enabled_cache_config: EnabledCacheConfig {
envs: Some(std::iter::once("NODE_ENV".into()).collect()),
pass_through_envs: Some(std::iter::once("FOO".into()).collect()),
}
},
);
}
#[test]
fn test_cache_disabled_but_with_fields() {
let user_config_json = json!({
"cache": false,
"envs": ["NODE_ENV"],
});
assert!(serde_json::from_value::<UserCacheConfig>(user_config_json).is_err());
}
#[test]
fn test_deny_unknown_field() {
let user_config_json = json!({
"foo": 42,
});
assert!(serde_json::from_value::<UserCacheConfig>(user_config_json).is_err());
}
#[test]
fn test_global_cache_bool_true() {
let config: UserGlobalCacheConfig = serde_json::from_value(json!(true)).unwrap();
assert_eq!(config, UserGlobalCacheConfig::Bool(true));
let resolved = ResolvedGlobalCacheConfig::resolve_from(Some(&config));
assert!(resolved.scripts);
assert!(resolved.tasks);
}
#[test]
fn test_global_cache_bool_false() {
let config: UserGlobalCacheConfig = serde_json::from_value(json!(false)).unwrap();
assert_eq!(config, UserGlobalCacheConfig::Bool(false));
let resolved = ResolvedGlobalCacheConfig::resolve_from(Some(&config));
assert!(!resolved.scripts);
assert!(!resolved.tasks);
}
#[test]
fn test_global_cache_detailed_scripts_only() {
let config: UserGlobalCacheConfig =
serde_json::from_value(json!({ "scripts": true })).unwrap();
let resolved = ResolvedGlobalCacheConfig::resolve_from(Some(&config));
assert!(resolved.scripts);
assert!(resolved.tasks); // defaults to true
}
#[test]
fn test_global_cache_detailed_tasks_false() {
let config: UserGlobalCacheConfig =
serde_json::from_value(json!({ "tasks": false })).unwrap();
let resolved = ResolvedGlobalCacheConfig::resolve_from(Some(&config));
assert!(!resolved.scripts); // defaults to false
assert!(!resolved.tasks);
}
#[test]
fn test_global_cache_detailed_both() {
let config: UserGlobalCacheConfig =
serde_json::from_value(json!({ "scripts": true, "tasks": false })).unwrap();
let resolved = ResolvedGlobalCacheConfig::resolve_from(Some(&config));
assert!(resolved.scripts);
assert!(!resolved.tasks);
}
#[test]
fn test_global_cache_none_defaults() {
let resolved = ResolvedGlobalCacheConfig::resolve_from(None);
assert!(!resolved.scripts); // defaults to false
assert!(resolved.tasks); // defaults to true
}
#[test]
fn test_global_cache_detailed_unknown_field() {
assert!(
serde_json::from_value::<UserGlobalCacheConfig>(json!({ "unknown": true })).is_err()
);
}
}