Skip to content

Commit 3dd0bf7

Browse files
committed
feat(check): add .mjs sidecar helper for --no-type-check override
1 parent 4e40a3f commit 3dd0bf7

3 files changed

Lines changed: 122 additions & 1 deletion

File tree

packages/cli/binding/src/check/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod analysis;
2+
mod sidecar;
23

34
use std::{ffi::OsStr, sync::Arc, time::Instant};
45

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//! Transient `.mjs` config that overrides `lint.options.typeCheck` to `false`.
2+
//!
3+
//! oxlint loads the `-c <path>` target with dynamic `import()` and reads
4+
//! `.default.lint` when `VP_VERSION` is set (the vite-plus invocation path).
5+
//! Writing a sidecar module with that shape lets a single invocation opt out
6+
//! of type-check without mutating the project's `vite.config.ts`.
7+
8+
use std::{
9+
fs,
10+
sync::atomic::{AtomicU64, Ordering},
11+
time::{SystemTime, UNIX_EPOCH},
12+
};
13+
14+
use serde_json::Value;
15+
use vite_error::Error;
16+
use vite_path::AbsolutePathBuf;
17+
18+
use crate::cli::ResolvedUniversalViteConfig;
19+
20+
/// Override returned by [`write_no_type_check_sidecar`]. The `_guard` field
21+
/// deletes the sidecar file on drop; callers must keep the override alive
22+
/// until oxlint has finished reading the config.
23+
pub(super) struct SidecarOverride {
24+
pub(super) config: ResolvedUniversalViteConfig,
25+
_guard: SidecarCleanup,
26+
}
27+
28+
struct SidecarCleanup {
29+
path: AbsolutePathBuf,
30+
}
31+
32+
impl Drop for SidecarCleanup {
33+
fn drop(&mut self) {
34+
// Best-effort: ignore errors (file already gone, permission denied, etc.).
35+
let _ = fs::remove_file(self.path.as_path());
36+
}
37+
}
38+
39+
static SIDECAR_COUNTER: AtomicU64 = AtomicU64::new(0);
40+
41+
/// Write a sidecar `.mjs` that mirrors the project's lint config with
42+
/// `options.typeCheck = false`, and return a clone of
43+
/// `ResolvedUniversalViteConfig` pointing at the sidecar path.
44+
///
45+
/// Returns `Ok(None)` when the resolved config lacks either a `configFile` or
46+
/// a `lint` entry — there is nothing for the override to replace and the
47+
/// caller should run lint unchanged.
48+
///
49+
/// `typeAware` is intentionally untouched: the flag only disables type-check
50+
/// (tsgolint), not type-aware lint rules.
51+
pub(super) fn write_no_type_check_sidecar(
52+
resolved_vite_config: &ResolvedUniversalViteConfig,
53+
) -> Result<Option<SidecarOverride>, Error> {
54+
if resolved_vite_config.config_file.is_none() {
55+
return Ok(None);
56+
}
57+
let Some(lint) = resolved_vite_config.lint.as_ref() else {
58+
return Ok(None);
59+
};
60+
61+
let mut lint_clone: Value = lint.clone();
62+
let Some(options) = lint_clone.as_object_mut().and_then(|map| {
63+
map.entry("options")
64+
.or_insert_with(|| Value::Object(serde_json::Map::new()))
65+
.as_object_mut()
66+
}) else {
67+
// Lint value isn't a JSON object — unexpected shape. Skip the
68+
// override rather than guess at a structure.
69+
return Ok(None);
70+
};
71+
options.insert("typeCheck".to_string(), Value::Bool(false));
72+
73+
// Contract check: `resolveUniversalViteConfig` returns the lint subtree at
74+
// the top level, so the clone must not be re-wrapped under another `lint`
75+
// key. Enforced in release too — a regression here would silently empty
76+
// the sidecar when oxlint reads `.default.lint`.
77+
if lint_clone.get("lint").is_some() {
78+
return Err(Error::Anyhow(anyhow::anyhow!(
79+
"resolved lint config unexpectedly wrapped under another `lint` key"
80+
)));
81+
}
82+
83+
let pid = std::process::id();
84+
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
85+
let counter = SIDECAR_COUNTER.fetch_add(1, Ordering::Relaxed);
86+
let filename = vite_str::format!("vite-plus-no-type-check-{pid}-{nanos}-{counter}.mjs");
87+
let filename_str: &str = filename.as_ref();
88+
let raw_temp_path = std::env::temp_dir().join(filename_str);
89+
let sidecar_path = AbsolutePathBuf::new(raw_temp_path).ok_or_else(|| {
90+
Error::Anyhow(anyhow::anyhow!("system temp dir resolved to a non-absolute path"))
91+
})?;
92+
93+
// `serde_json::to_string` quotes keys and escapes string values, producing
94+
// output that is also a valid JS object literal on Node ≥ 10 (the minimum
95+
// already required by vite-plus).
96+
let lint_json = serde_json::to_string(&lint_clone).map_err(|e| {
97+
Error::Anyhow(anyhow::anyhow!("failed to serialize lint config for sidecar: {e}"))
98+
})?;
99+
let content = vite_str::format!("export default {{ lint: {lint_json} }};\n");
100+
let content_str: &str = content.as_ref();
101+
102+
fs::write(sidecar_path.as_path(), content_str.as_bytes()).map_err(|e| {
103+
Error::Anyhow(anyhow::anyhow!(
104+
"failed to write sidecar at {}: {e}",
105+
sidecar_path.as_path().display()
106+
))
107+
})?;
108+
109+
// Keep the override internally consistent: any future reader of
110+
// `config.lint` (e.g., cache-key hashing, logging) will see the same
111+
// `typeCheck: false` value that oxlint reads from the sidecar file.
112+
let mut config_override = resolved_vite_config.clone();
113+
config_override.config_file = Some(sidecar_path.as_path().to_string_lossy().into_owned());
114+
config_override.lint = Some(lint_clone);
115+
116+
Ok(Some(SidecarOverride {
117+
config: config_override,
118+
_guard: SidecarCleanup { path: sidecar_path },
119+
}))
120+
}

packages/cli/binding/src/cli/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ pub(crate) use execution::resolve_and_capture_output;
1717
// Re-exports for lib.rs and check/mod.rs
1818
pub use resolver::SubcommandResolver;
1919
use rustc_hash::FxHashMap;
20-
pub(crate) use types::CapturedCommandOutput;
2120
pub use types::{
2221
BoxedResolverFn, CliOptions, ResolveCommandResult, SynthesizableSubcommand,
2322
ViteConfigResolverFn,
2423
};
24+
pub(crate) use types::{CapturedCommandOutput, ResolvedUniversalViteConfig};
2525
use vite_error::Error;
2626
use vite_path::{AbsolutePath, AbsolutePathBuf};
2727
pub use vite_shared::init_tracing;

0 commit comments

Comments
 (0)