|
| 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 | +} |
0 commit comments