use std::{
collections::{BTreeMap, BTreeSet},
fs::{self, OpenOptions},
io::{self, Write},
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
time::{Instant, SystemTime, UNIX_EPOCH},
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
js_instrumenter::{
CandidateBranch, CandidateDecision, CandidateError, CandidateLimitation, CandidatePoint,
instrument_with_import_policy,
},
project_discovery::CoverageProject,
source_discovery::{SourceLimitation, SourceScope},
};
const RUNTIME_INSTANCE_MARKER: &str = "__SUPERCOV_RUNTIME_INSTANCE__";
const FRONTEND_CACHE_SCHEMA_VERSION: u32 = 2;
const FRONTEND_CACHE_FILE: &str = ".supercov/frontend-cache.json";
const FRONTEND_CACHE_DIRECTORY: &str = ".supercov/frontend-cache-artifacts";
const AUTHORED_DIRECTORY: &str = ".supercov/node_modules/.authored";
const AUTHORED_LIST: &str = ".supercov/node_modules/authored-sources.json";
const CHANGED_DIRECTORY: &str = ".supercov/node_modules/.changed";
const RUNTIME_FILES: &[&str] = &[
"atomic.mjs",
"ava.mjs",
"bootstrap.cjs",
"capability.mjs",
"jest.cjs",
"jest.config.mjs",
"jestReporter.mjs",
"jestRuntime.cjs",
"launchSupervisor.mjs",
"mocha.mjs",
"nodeAssert.mjs",
"nodeAssertAdapter.mjs",
"nodeAssertStrict.mjs",
"nodeTest.mjs",
"playwright.mjs",
"playwrightReporter.mjs",
"provenance.mjs",
"register.mjs",
"resolve-loader.mjs",
"runnerEvidence.mjs",
"runtime.mjs",
"tap.mjs",
"transport.mjs",
"vitest.mjs",
"vitestBrowser.mjs",
"vitestReporter.mjs",
];
static UNIQUE: AtomicU64 = AtomicU64::new(0);
struct SetupAccounting {
files: AtomicU64,
bytes: AtomicU64,
create_ns: AtomicU64,
write_ns: AtomicU64,
rename_ns: AtomicU64,
directories: AtomicU64,
directory_retries: AtomicU64,
directory_ns: AtomicU64,
runtime_ns: AtomicU64,
config_ns: AtomicU64,
sources_ns: AtomicU64,
instrument_ns: AtomicU64,
assertion_ns: AtomicU64,
cache_ns: AtomicU64,
}
impl SetupAccounting {
const fn new() -> Self {
Self {
files: AtomicU64::new(0),
bytes: AtomicU64::new(0),
create_ns: AtomicU64::new(0),
write_ns: AtomicU64::new(0),
rename_ns: AtomicU64::new(0),
directories: AtomicU64::new(0),
directory_retries: AtomicU64::new(0),
directory_ns: AtomicU64::new(0),
runtime_ns: AtomicU64::new(0),
config_ns: AtomicU64::new(0),
sources_ns: AtomicU64::new(0),
instrument_ns: AtomicU64::new(0),
assertion_ns: AtomicU64::new(0),
cache_ns: AtomicU64::new(0),
}
}
}
static SETUP: SetupAccounting = SetupAccounting::new();
fn account(counter: &AtomicU64, started: Instant) {
counter.fetch_add(started.elapsed().as_nanos() as u64, Ordering::Relaxed);
}
fn accounted_ms(counter: &AtomicU64) -> f64 {
counter.load(Ordering::Relaxed) as f64 / 1_000_000.0
}
fn counted(counter: &AtomicU64) -> u64 {
counter.load(Ordering::Relaxed)
}
fn timed<T>(counter: &AtomicU64, operation: impl FnOnce() -> T) -> T {
let started = Instant::now();
let value = operation();
account(counter, started);
value
}
pub fn setup_timing_detail() -> Option<String> {
if std::env::var("SUPERCOV_PHASE_TIMING").as_deref() != Ok("1") {
return None;
}
Some(format!(
"setup detail runtime={:.1}ms configs={:.1}ms sources={:.1}ms (instrument={:.1}ms) \
assertions={:.1}ms cache={:.1}ms | files={} bytes={} create={:.1}ms write={:.1}ms \
rename={:.1}ms | directories={} retries={} directory-wait={:.1}ms",
accounted_ms(&SETUP.runtime_ns),
accounted_ms(&SETUP.config_ns),
accounted_ms(&SETUP.sources_ns),
accounted_ms(&SETUP.instrument_ns),
accounted_ms(&SETUP.assertion_ns),
accounted_ms(&SETUP.cache_ns),
counted(&SETUP.files),
counted(&SETUP.bytes),
accounted_ms(&SETUP.create_ns),
accounted_ms(&SETUP.write_ns),
accounted_ms(&SETUP.rename_ns),
counted(&SETUP.directories),
counted(&SETUP.directory_retries),
accounted_ms(&SETUP.directory_ns),
))
}
#[derive(Debug)]
pub enum JavascriptFrontendError {
Io {
path: PathBuf,
source: io::Error,
},
Instrument {
file: String,
source: CandidateError,
},
MissingRuntimeMarker,
Serialize(serde_json::Error),
UnsafeSourcePath(String),
}
impl std::fmt::Display for JavascriptFrontendError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
Self::Instrument { file, source } => {
write!(formatter, "failed to instrument {file}: {source}")
}
Self::MissingRuntimeMarker => write!(
formatter,
"generated Supercov runtime is missing its instance marker"
),
Self::Serialize(error) => write!(formatter, "failed to serialize manifest: {error}"),
Self::UnsafeSourcePath(file) => write!(formatter, "unsafe source path: {file}"),
}
}
}
impl std::error::Error for JavascriptFrontendError {}
fn io_error(path: &Path, source: io::Error) -> JavascriptFrontendError {
JavascriptFrontendError::Io {
path: path.to_owned(),
source,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct JavascriptManifest {
pub decisions: Vec<CandidateDecision>,
pub points: Vec<CandidatePoint>,
pub branches: Vec<CandidateBranch>,
pub limitations: Vec<CandidateLimitation>,
pub scope: SourceScope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreparedJavascriptFrontend {
pub manifest: JavascriptManifest,
pub manifest_path: PathBuf,
pub preload_path: PathBuf,
pub playwright_config_path: PathBuf,
pub vitest_config_path: PathBuf,
pub assertion_calls: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct JavascriptFrontendCache {
schema_version: u32,
key: String,
assertion_calls: usize,
artifacts: Vec<JavascriptFrontendCacheArtifact>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct JavascriptFrontendCacheArtifact {
path: String,
cache_file: String,
sha256: String,
}
fn safe_relative(path: &Path) -> bool {
path.components().next().is_some()
&& path
.components()
.all(|component| matches!(component, std::path::Component::Normal(_)))
}
fn regular_file(workspace: &Path, relative: &str) -> bool {
safe_relative(Path::new(relative))
&& fs::symlink_metadata(workspace.join(relative))
.is_ok_and(|metadata| metadata.file_type().is_file())
}
fn valid_cached_artifact(workspace: &Path, artifact: &JavascriptFrontendCacheArtifact) -> bool {
let expected_cache_file = format!("{FRONTEND_CACHE_DIRECTORY}/{}", artifact.sha256);
if !safe_relative(Path::new(&artifact.path))
|| !safe_relative(Path::new(&artifact.cache_file))
|| artifact.cache_file != expected_cache_file
|| artifact.sha256.len() != 64
|| !artifact
.sha256
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return false;
}
let Ok(contents) = fs::read(workspace.join(&artifact.cache_file)) else {
return false;
};
format!("{:x}", Sha256::digest(&contents)) == artifact.sha256
}
pub fn read_javascript_frontend_cache(
workspace: &Path,
key: &str,
) -> Option<JavascriptFrontendCache> {
let metadata: JavascriptFrontendCache =
serde_json::from_slice(&fs::read(workspace.join(FRONTEND_CACHE_FILE)).ok()?).ok()?;
if metadata.schema_version != FRONTEND_CACHE_SCHEMA_VERSION
|| metadata.key != key
|| metadata.artifacts.is_empty()
|| metadata
.artifacts
.iter()
.any(|artifact| !valid_cached_artifact(workspace, artifact))
{
return None;
}
Some(metadata)
}
pub fn javascript_frontend_reuse_paths(cache: &JavascriptFrontendCache) -> Vec<PathBuf> {
let _ = cache;
vec![
PathBuf::from(FRONTEND_CACHE_FILE),
PathBuf::from(FRONTEND_CACHE_DIRECTORY),
]
}
fn restore_cached_file(path: &Path, contents: &[u8]) -> Result<(), JavascriptFrontendError> {
let parent = path
.parent()
.ok_or_else(|| JavascriptFrontendError::UnsafeSourcePath(path.display().to_string()))?;
create_directory_all(parent)?;
let temporary = parent.join(format!(".supercov-restore-{}", unique()));
let result = (|| {
fs::write(&temporary, contents).map_err(|source| io_error(&temporary, source))?;
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_file() || metadata.file_type().is_symlink() => {
fs::remove_file(path).map_err(|source| io_error(path, source))?;
}
Ok(_) => {
return Err(JavascriptFrontendError::UnsafeSourcePath(
path.display().to_string(),
));
}
Err(source) if source.kind() == io::ErrorKind::NotFound => {}
Err(source) => return Err(io_error(path, source)),
}
fs::rename(&temporary, path).map_err(|source| io_error(path, source))
})();
if result.is_err() {
let _ = fs::remove_file(&temporary);
}
result
}
pub fn load_cached_javascript_frontend(
workspace: &Path,
cache: &JavascriptFrontendCache,
) -> Result<PreparedJavascriptFrontend, JavascriptFrontendError> {
for artifact in &cache.artifacts {
let cache_path = workspace.join(&artifact.cache_file);
let contents = fs::read(&cache_path).map_err(|source| io_error(&cache_path, source))?;
if format!("{:x}", Sha256::digest(&contents)) != artifact.sha256 {
return Err(JavascriptFrontendError::UnsafeSourcePath(format!(
"corrupt frontend cache artifact {}",
artifact.cache_file
)));
}
restore_cached_file(&workspace.join(&artifact.path), &contents)?;
}
let generated = workspace.join(".supercov");
let manifest_path = generated.join("manifest.json");
let manifest = serde_json::from_slice(
&fs::read(&manifest_path).map_err(|source| io_error(&manifest_path, source))?,
)
.map_err(JavascriptFrontendError::Serialize)?;
Ok(PreparedJavascriptFrontend {
manifest,
manifest_path,
preload_path: generated.join("node_modules/register.mjs"),
playwright_config_path: generated.join("playwright.config.mjs"),
vitest_config_path: generated.join("vitest.config.mjs"),
assertion_calls: cache.assertion_calls,
})
}
fn frontend_artifact_paths(workspace: &Path, project: &CoverageProject) -> Vec<String> {
let mut artifacts = vec![
".supercov/node_modules/package.json".to_owned(),
".supercov/node_modules/runtime.d.mts".to_owned(),
".supercov/playwright.config.mjs".to_owned(),
".supercov/vitest.config.mjs".to_owned(),
".supercov/vitestCommands.mjs".to_owned(),
".supercov/manifest.json".to_owned(),
".supercov/statement-exclusions.json".to_owned(),
".supercov/instrumentation-complete".to_owned(),
];
artifacts.extend(
RUNTIME_FILES
.iter()
.map(|name| format!(".supercov/node_modules/{name}")),
);
artifacts.extend(project.source_files.iter().cloned());
artifacts.extend(
project
.source_scope
.entries
.iter()
.map(|entry| entry.file.clone()),
);
for root in &project.source_roots {
let host = if workspace.join(root).is_file() {
Path::new(root).parent().unwrap_or_else(|| Path::new(""))
} else {
Path::new(root)
};
for name in ["package.json", "runtime.mjs", "runtime.d.mts"] {
let path = host.join(".supercov/node_modules").join(name);
if let Some(path) = path.to_str() {
artifacts.push(path.replace('\\', "/"));
}
}
}
let rewritten = project
.source_files
.iter()
.chain(project.source_scope.entries.iter().map(|entry| &entry.file))
.map(|file| format!("{AUTHORED_DIRECTORY}/{file}"))
.collect::<Vec<_>>();
artifacts.extend(rewritten);
artifacts.push(AUTHORED_LIST.to_owned());
artifacts.sort();
artifacts.dedup();
artifacts.retain(|path| regular_file(workspace, path));
artifacts
}
pub fn authored_path(workspace: &Path, file: &Path) -> PathBuf {
workspace.join(AUTHORED_DIRECTORY).join(file)
}
pub fn changed_path(workspace: &Path, file: &Path) -> PathBuf {
workspace.join(CHANGED_DIRECTORY).join(file)
}
pub fn changed_directory(workspace: &Path) -> PathBuf {
workspace.join(CHANGED_DIRECTORY)
}
pub fn rewritten_files(workspace: &Path) -> Vec<String> {
fs::read(workspace.join(AUTHORED_LIST))
.ok()
.and_then(|contents| serde_json::from_slice(&contents).ok())
.unwrap_or_default()
}
fn keep_authored(
workspace: &Path,
authored: &mut BTreeSet<String>,
file: &str,
source: &str,
) -> Result<(), JavascriptFrontendError> {
if authored.insert(file.to_owned()) {
let path = checked_source_path(&workspace.join(AUTHORED_DIRECTORY), file)?;
if let Some(parent) = path.parent() {
create_directory_all(parent)?;
}
atomic_write(&path, source.as_bytes())?;
}
Ok(())
}
fn write_javascript_frontend_cache(
workspace: &Path,
project: &CoverageProject,
key: &str,
assertion_calls: usize,
) -> Result<(), JavascriptFrontendError> {
let cache_directory = workspace.join(FRONTEND_CACHE_DIRECTORY);
create_directory_all(&cache_directory)?;
let mut artifacts = Vec::new();
for path in frontend_artifact_paths(workspace, project) {
let contents = fs::read(workspace.join(&path))
.map_err(|source| io_error(&workspace.join(&path), source))?;
let sha256 = format!("{:x}", Sha256::digest(&contents));
let cache_file = format!("{FRONTEND_CACHE_DIRECTORY}/{sha256}");
let destination = workspace.join(&cache_file);
if !destination.is_file() {
atomic_write(&destination, &contents)?;
}
artifacts.push(JavascriptFrontendCacheArtifact {
path,
cache_file,
sha256,
});
}
let cache = JavascriptFrontendCache {
schema_version: FRONTEND_CACHE_SCHEMA_VERSION,
key: key.to_owned(),
assertion_calls,
artifacts,
};
let mut encoded =
serde_json::to_vec_pretty(&cache).map_err(JavascriptFrontendError::Serialize)?;
encoded.push(b'\n');
atomic_write(&workspace.join(FRONTEND_CACHE_FILE), &encoded)
}
fn embedded_runtime(name: &str) -> Option<&'static [u8]> {
match name {
"atomic.mjs" => Some(include_bytes!("../runtime-assets/javascript/atomic.mjs")),
"ava.mjs" => Some(include_bytes!("../runtime-assets/javascript/ava.mjs")),
"bootstrap.cjs" => Some(include_bytes!("../runtime-assets/javascript/bootstrap.cjs")),
"capability.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/capability.mjs"
)),
"jest.cjs" => Some(include_bytes!("../runtime-assets/javascript/jest.cjs")),
"jest.config.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/jest.config.mjs"
)),
"jestReporter.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/jestReporter.mjs"
)),
"jestRuntime.cjs" => Some(include_bytes!(
"../runtime-assets/javascript/jestRuntime.cjs"
)),
"launchSupervisor.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/launchSupervisor.mjs"
)),
"mocha.mjs" => Some(include_bytes!("../runtime-assets/javascript/mocha.mjs")),
"nodeAssert.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/nodeAssert.mjs"
)),
"nodeAssertAdapter.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/nodeAssertAdapter.mjs"
)),
"nodeAssertStrict.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/nodeAssertStrict.mjs"
)),
"nodeTest.mjs" => Some(include_bytes!("../runtime-assets/javascript/nodeTest.mjs")),
"playwright.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/playwright.mjs"
)),
"playwrightReporter.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/playwrightReporter.mjs"
)),
"provenance.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/provenance.mjs"
)),
"register.mjs" => Some(include_bytes!("../runtime-assets/javascript/register.mjs")),
"resolve-loader.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/resolve-loader.mjs"
)),
"runnerEvidence.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/runnerEvidence.mjs"
)),
"runtime.mjs" => Some(include_bytes!("../runtime-assets/javascript/runtime.mjs")),
"tap.mjs" => Some(include_bytes!("../runtime-assets/javascript/tap.mjs")),
"transport.mjs" => Some(include_bytes!("../runtime-assets/javascript/transport.mjs")),
"vitest.mjs" => Some(include_bytes!("../runtime-assets/javascript/vitest.mjs")),
"vitestBrowser.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/vitestBrowser.mjs"
)),
"vitestReporter.mjs" => Some(include_bytes!(
"../runtime-assets/javascript/vitestReporter.mjs"
)),
_ => None,
}
}
fn unique() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!(
"{}-{nanos}-{}",
std::process::id(),
UNIQUE.fetch_add(1, Ordering::Relaxed)
)
}
#[cfg(not(windows))]
fn create_directory_all(path: &Path) -> Result<(), JavascriptFrontendError> {
SETUP.directories.fetch_add(1, Ordering::Relaxed);
timed(&SETUP.directory_ns, || {
fs::create_dir_all(path).map_err(|source| io_error(path, source))
})
}
#[cfg(windows)]
fn create_directory_all(path: &Path) -> Result<(), JavascriptFrontendError> {
const ATTEMPTS: usize = 16;
let started = std::time::Instant::now();
SETUP.directories.fetch_add(1, Ordering::Relaxed);
let mut delay = std::time::Duration::from_millis(20);
for attempt in 0..ATTEMPTS {
match fs::create_dir_all(path) {
Ok(()) => {
account(&SETUP.directory_ns, started);
return Ok(());
}
Err(source)
if source.kind() == io::ErrorKind::PermissionDenied && attempt + 1 < ATTEMPTS =>
{
SETUP.directory_retries.fetch_add(1, Ordering::Relaxed);
std::thread::sleep(delay);
delay = (delay * 2).min(std::time::Duration::from_millis(500));
}
Err(source) => {
account(&SETUP.directory_ns, started);
let detail = format!(
"{source} (after {} attempt(s) over {:?}; ancestors: {})",
attempt + 1,
started.elapsed(),
describe_ancestors(path)
);
return Err(io_error(path, io::Error::new(source.kind(), detail)));
}
}
}
unreachable!("the final directory-creation attempt always returns")
}
#[cfg_attr(not(windows), allow(dead_code))]
fn describe_ancestors(path: &Path) -> String {
let mut current = PathBuf::new();
let mut parts = Vec::new();
for component in path.components() {
current.push(component.as_os_str());
let state = match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => "link",
Ok(metadata) if metadata.file_type().is_dir() => "dir",
Ok(_) => "file",
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
"missing"
}
Err(error) => return format!("{} -> {error}", current.display()),
};
parts.push(format!("{}={state}", current.display()));
}
parts.join("; ")
}
fn atomic_write(path: &Path, contents: &[u8]) -> Result<(), JavascriptFrontendError> {
let parent = path
.parent()
.ok_or_else(|| JavascriptFrontendError::UnsafeSourcePath(path.display().to_string()))?;
let temporary = parent.join(format!(".supercov-write-{}", unique()));
let result = (|| {
let open = || {
OpenOptions::new()
.write(true)
.create_new(true)
.open(&temporary)
};
let opened = timed(&SETUP.create_ns, open);
let mut output = match opened {
Ok(output) => output,
Err(source) if source.kind() == io::ErrorKind::NotFound => {
create_directory_all(parent)?;
timed(&SETUP.create_ns, open).map_err(|source| io_error(&temporary, source))?
}
Err(source) => return Err(io_error(&temporary, source)),
};
timed(&SETUP.write_ns, || output.write_all(contents))
.map_err(|source| io_error(&temporary, source))?;
timed(&SETUP.rename_ns, || fs::rename(&temporary, path))
.map_err(|source| io_error(path, source))?;
SETUP.files.fetch_add(1, Ordering::Relaxed);
SETUP
.bytes
.fetch_add(contents.len() as u64, Ordering::Relaxed);
Ok(())
})();
if result.is_err() {
let _ = fs::remove_file(&temporary);
}
result
}
fn checked_source_path(workspace: &Path, file: &str) -> Result<PathBuf, JavascriptFrontendError> {
let relative = Path::new(file);
if relative.is_absolute()
|| relative
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(JavascriptFrontendError::UnsafeSourcePath(file.to_owned()));
}
Ok(workspace.join(relative))
}
fn runtime_specifier(file: &str, name: &str) -> Result<String, JavascriptFrontendError> {
let relative = Path::new(file);
if relative.is_absolute()
|| relative
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(JavascriptFrontendError::UnsafeSourcePath(file.to_owned()));
}
let depth = relative
.parent()
.map_or(0, |parent| parent.components().count());
Ok(if depth == 0 {
format!("./.supercov/node_modules/{name}")
} else {
format!("{}.supercov/node_modules/{name}", "../".repeat(depth))
})
}
const GENERATED_SOURCE_BANNER: &str =
"/* eslint-disable */\n// @ts-nocheck -- generated coverage workspace only\n";
fn generated_source_banner(code: &str) -> String {
let Some(rest) = code.strip_prefix("#!") else {
return format!("{GENERATED_SOURCE_BANNER}{code}");
};
let (line, remainder) = rest.split_once('\n').unwrap_or((rest, ""));
format!("#!{line}\n{GENERATED_SOURCE_BANNER}{remainder}")
}
fn isolate_runtime(source: &str, collector_id: &str) -> Result<String, JavascriptFrontendError> {
let source = format!("/* eslint-disable */\n{source}");
let source = source.as_str();
let double = format!("runtimeInstanceToken = \"{RUNTIME_INSTANCE_MARKER}\"");
let single = format!("runtimeInstanceToken = '{RUNTIME_INSTANCE_MARKER}'");
if let Some(index) = source.find(&double) {
let mut isolated = source.to_owned();
isolated.replace_range(
index..index + double.len(),
&format!("runtimeInstanceToken = \"{collector_id}\""),
);
return Ok(isolated);
}
if let Some(index) = source.find(&single) {
let mut isolated = source.to_owned();
isolated.replace_range(
index..index + single.len(),
&format!("runtimeInstanceToken = '{collector_id}'"),
);
return Ok(isolated);
}
Err(JavascriptFrontendError::MissingRuntimeMarker)
}
fn inline_instrumentation_map(
code: &str,
map: Option<&serde_json::Value>,
original_path: &Path,
original_source: &str,
) -> Option<String> {
let map = map?.clone();
let mut map = map;
let object = map.as_object_mut()?;
let banner_lines = code
.lines()
.skip(usize::from(code.starts_with("#!")))
.take_while(|line| {
line.starts_with("/* eslint-disable */") || line.starts_with("// @ts-nocheck")
})
.count();
let mut mappings = object.get("mappings")?.as_str()?.to_owned();
if banner_lines > 0 {
mappings = format!("{}{mappings}", ";".repeat(banner_lines));
}
let mut code = std::borrow::Cow::Borrowed(code);
if !code.starts_with("#!") && mappings.starts_with(';') {
mappings.insert_str(0, "AAAA,C");
if code.starts_with(crate::js_instrumenter::RUNTIME_LOADER_START) {
code = std::borrow::Cow::Owned(format!("\n{code}"));
mappings.insert(6, ';');
}
}
let directive_line = code.matches('\n').count() + 1;
let mapped_lines = mappings.matches(';').count();
if directive_line > mapped_lines
&& mappings.contains(|c: char| c.is_ascii_alphanumeric() || c == '+' || c == '/')
{
mappings.push_str(&";".repeat(directive_line - mapped_lines));
mappings.push_str("AAAA");
}
object.insert("mappings".into(), serde_json::Value::String(mappings));
object.insert(
"sources".into(),
serde_json::json!([original_path.display().to_string()]),
);
object.insert(
"sourcesContent".into(),
serde_json::json!([original_source]),
);
let payload = serde_json::to_string(&map).ok()?;
use base64::Engine as _;
let encoded = base64::engine::general_purpose::STANDARD.encode(payload);
Some(format!(
"{code}\n//# sourceMappingURL=data:application/json;base64,{encoded}\n"
))
}
fn strip_source_map_reference(mut bytes: Vec<u8>) -> Vec<u8> {
const MARKER: &[u8] = b"\n//# sourceMappingURL=";
if let Some(index) = bytes
.windows(MARKER.len())
.rposition(|window| window == MARKER)
{
let suffix = &bytes[index + MARKER.len()..];
let suffix = suffix.strip_suffix(b"\n").unwrap_or(suffix);
let suffix = suffix.strip_suffix(b"\r").unwrap_or(suffix);
if !suffix.contains(&b'\n') && !suffix.contains(&b'\r') {
bytes.truncate(index + 1);
}
}
bytes
}
fn copy_runtime(generated: &Path, collector_id: &str) -> Result<(), JavascriptFrontendError> {
create_directory_all(generated)?;
atomic_write(
&generated.join("package.json"),
b"{\"private\":true,\"type\":\"module\"}\n",
)?;
for name in RUNTIME_FILES {
let destination = generated.join(name);
let source_path = PathBuf::from(format!("embedded:{name}"));
let bytes = embedded_runtime(name)
.expect("every declared runtime file must have an embedded asset")
.to_vec();
let bytes = strip_source_map_reference(bytes);
if *name == "runtime.mjs" {
let text = String::from_utf8(bytes).map_err(|source| {
io_error(
&source_path,
io::Error::new(io::ErrorKind::InvalidData, source),
)
})?;
atomic_write(
&destination,
isolate_runtime(&text, collector_id)?.as_bytes(),
)?;
} else {
atomic_write(&destination, &bytes)?;
}
}
atomic_write(
&generated.join("runtime.d.mts"),
runtime_declarations().as_bytes(),
)?;
Ok(())
}
fn runtime_declarations() -> String {
const VALUES: &[(&str, usize)] = &[
("selectionRight", 1),
("selectionEnd", 1),
("optionalSelect", 2),
("optionalCallReached", 1),
("optionalCallEnd", 1),
("defaultSelected", 1),
("tryCatch", 1),
("mcdcCondition", 2),
("mcdcEnd", 1),
("defaultSelectedV2", 2),
("optionalSelectV2", 2),
("selectShortV2", 2),
("selectRightV2", 2),
("selectNamedRightV2", 2),
("selectAssignEndV2", 2),
("selectPathV2", 1),
("selectEnd2V2", 2),
("selectEndV2", 2),
("optionalCallEndV2", 2),
("renderedValueV2", 2),
("parenthesizedAssignmentValue", 0),
("withRequestPhase", 0),
];
const OTHERS: &[&str] = &[
"coverageHit",
"selectionBegin",
"optionalCallBegin",
"optionalCallContinued",
"defaultEntered",
"defaultEnteredV2",
"tryBegin",
"tryEnd",
"loopBegin",
"loopEntered",
"loopEnd",
"mcdcBegin",
"registerProbeV2",
"coverageHitV2",
"mcdcEndV2",
];
let mut text = String::from("/* eslint-disable */\n");
for (name, before) in VALUES {
let leading = (0..*before)
.map(|index| format!("a{index}: any, "))
.collect::<String>();
text.push_str(&format!(
"export declare function {name}<T>({leading}value: T, ...rest: any[]): T;\n"
));
}
for name in OTHERS {
text.push_str(&format!(
"export declare function {name}(...args: any[]): any;\n"
));
}
text
}
fn limitation_from_source(value: &SourceLimitation) -> CandidateLimitation {
CandidateLimitation {
id: value.id.clone(),
kind: value.kind.clone(),
file: value.file.clone(),
line: value.line,
column: value.column,
source: value.source.clone(),
reason: value.reason.clone(),
}
}
fn bootstrap_loader(
output: &mut crate::js_instrumenter::CandidateOutput,
file: &str,
) -> Result<(), JavascriptFrontendError> {
let Some(binding) = output.runtime.as_ref() else {
return Ok(());
};
let direct = format!(
"{} = globalThis.__SUPERCOV_DIRECT_RUNTIME__.mcdcBegin",
binding.mcdc_begin
);
let Some(found) = output.code.find(&direct) else {
return Ok(());
};
let loader = format!(
"{}\n",
crate::js_instrumenter::runtime_loader(&runtime_specifier(file, "bootstrap.cjs")?)
);
let offset = output.code[..found]
.rfind('\n')
.map_or(0, |newline| newline + 1);
let line = output.code[..offset].matches('\n').count();
if let Some(serde_json::Value::String(mappings)) =
output.map.as_mut().and_then(|map| map.get_mut("mappings"))
{
let mut lines: Vec<&str> = mappings.split(';').collect();
if line <= lines.len() {
lines.insert(line, "");
*mappings = lines.join(";");
}
}
output.code.insert_str(offset, &loader);
Ok(())
}
fn bootstrap_runtime(
output: &mut crate::js_instrumenter::CandidateOutput,
runtime_source: &str,
browser_suite: bool,
) {
let Some(binding) = output.runtime.as_ref() else {
return;
};
let script = format!(
"{} = globalThis.__supercovRuntime.mcdcBegin",
binding.mcdc_begin
);
let direct = format!(
"{} = globalThis.__SUPERCOV_DIRECT_RUNTIME__.mcdcBegin",
binding.mcdc_begin
);
let Some(found) = output
.code
.find(&script)
.or_else(|| browser_suite.then(|| output.code.find(&direct)).flatten())
else {
return;
};
let Some(export_start) = runtime_source.rfind("\nexport {") else {
return;
};
let offset = output.code[..found]
.rfind('\n')
.map_or(0, |newline| newline + 1);
let prelude = format!(
"(() => {{ if (!globalThis.__SUPERCOV_DIRECT_RUNTIME__) {{\n{}\n}} if (globalThis.__supercovRuntime == null) globalThis.__supercovRuntime = globalThis.__SUPERCOV_DIRECT_RUNTIME__; }})();\n",
&runtime_source[..export_start]
);
let line = output.code[..offset].matches('\n').count();
let added = prelude.matches('\n').count();
if let Some(serde_json::Value::String(mappings)) =
output.map.as_mut().and_then(|map| map.get_mut("mappings"))
{
let mut lines: Vec<&str> = mappings.split(';').collect();
if line <= lines.len() {
lines.splice(line..line, std::iter::repeat_n("", added));
*mappings = lines.join(";");
}
}
output.code.insert_str(offset, &prelude);
}
fn relocated_project_file(
workspace: &Path,
project: &CoverageProject,
source: Option<&PathBuf>,
) -> Option<PathBuf> {
let source = source?;
let relative = source.strip_prefix(&project.root).ok()?;
Some(workspace.join(relative))
}
fn write_vitest_config(
workspace: &Path,
project: &CoverageProject,
generated: &Path,
) -> Result<PathBuf, JavascriptFrontendError> {
let path = generated.join("vitest.config.mjs");
let original = relocated_project_file(workspace, project, project.vitest_config.as_ref())
.map(|path| path.display().to_string());
let original = serde_json::to_string(&original).map_err(JavascriptFrontendError::Serialize)?;
let source = format!(
"import {{ createRequire }} from 'node:module';\n\
import {{ pathToFileURL }} from 'node:url';\n\
// pnpm's strict layout does not hoist vite to the project root: it\n\
// lives inside vitest's virtual store, so a bare 'vite' specifier\n\
// resolved from this generated file fails. Vitest depends on vite, so\n\
// fall back to resolving it through vitest's own tree rather than\n\
// requiring the project to hoist anything.\n\
const supercovRequire = createRequire(import.meta.url);\n\
const supercovLoadVite = async () => {{\n\
try {{\n\
return await import('vite');\n\
}} catch (error) {{\n\
let entry;\n\
try {{\n\
entry = createRequire(supercovRequire.resolve('vitest')).resolve('vite');\n\
}} catch {{\n\
throw error;\n\
}}\n\
return await import(pathToFileURL(entry).href);\n\
}}\n\
}};\n\
const viteNamespace = await supercovLoadVite();\n\
import {{ resolve }} from 'node:path';\n\
import SupercovVitestReporter from './node_modules/vitestReporter.mjs';\n\
import {{ supercovBrowserCommands }} from './vitestCommands.mjs';\n\
const vite = viteNamespace.default ?? viteNamespace;\n\
const {{ loadConfigFromFile, mergeConfig }} = vite;\n\
const discoveredConfig = {original};\n\
// Browser Mode runs the test file in the browser, so the node setup --\n\
// which reaches node:fs to write evidence -- cannot load there. Vite\n\
// externalises it and the suite fails before collecting a test.\n\
const supercovBrowserMode = (config) => Boolean(config?.test?.browser?.enabled);\n\
const supercovSetupFile = (config) =>\n\
supercovBrowserMode(config)\n\
? '.supercov/node_modules/vitestBrowser.mjs'\n\
: '.supercov/node_modules/vitest.mjs';\n\
// Evidence leaves the browser over Vitest's own command channel, so\n\
// the command is registered only for the mode that uses it.\n\
const supercovBrowserTest = (config) =>\n\
supercovBrowserMode(config)\n\
? {{ browser: {{ commands: supercovBrowserCommands(process.cwd()) }} }}\n\
: {{}};\n\
// Child projects do not inherit root setupFiles. Resolve each project's\n\
// environment after Vitest has loaded file/glob/inline configs and\n\
// applied --project, preserving the runner's own config semantics.\n\
const configureProjects = (projects) => {{\n\
const nodeSetup = resolve(process.cwd(), '.supercov/node_modules/vitest.mjs');\n\
const browserSetup = resolve(process.cwd(), '.supercov/node_modules/vitestBrowser.mjs');\n\
for (const project of projects ?? []) {{\n\
const test = project.config;\n\
const browser = Boolean(test.browser?.enabled);\n\
const setup = browser ? browserSetup : nodeSetup;\n\
test.setupFiles = [setup, ...(test.setupFiles ?? []).filter(file => file !== nodeSetup && file !== browserSetup)];\n\
test.maxConcurrency = 1;\n\
if (browser) {{\n\
const commands = supercovBrowserCommands(process.cwd());\n\
test.browser.commands = {{ ...test.browser.commands, ...commands }};\n\
// Browser instances can already exist when reporters initialize.\n\
// Their parent generates the browser command exports.\n\
const server = project.browser?.parent ?? project.browser;\n\
if (server?.commands) Object.assign(server.commands, commands);\n\
}}\n\
}}\n\
}};\n\
// Vitest's coverage measures the instrumented copy, probes included:\n\
// a suite at 100% read 87.5% of branches, and autoUpdate would write\n\
// what it measured into this file. Coverage is still collected and\n\
// reported (see register.mjs). Vitest 0.x kept thresholds in coverage.\n\
const supercovThresholdKeys = ['thresholds', 'lines', 'functions', 'branches', 'statements', 'perFile', '100', 'thresholdAutoUpdate'];\n\
const supercovSkipCoverageThresholds = (coverage) => {{\n\
const configured = supercovThresholdKeys.filter((key) => coverage?.[key] !== undefined);\n\
for (const key of configured) delete coverage[key];\n\
const collecting = coverage?.enabled === true ||\n\
process.argv.some((argument) => /^--coverage(?:\\.enabled)?(?:=true)?$/.test(argument));\n\
if (configured.length && collecting) process.__SUPERCOV_SKIPPED_COVERAGE_THRESHOLDS__?.('Vitest');\n\
}};\n\
export default async function supercovVitestConfig(env) {{\n\
const originalPath = process.env.SUPERCOV_ORIGINAL_VITEST_CONFIG || discoveredConfig;\n\
const loaded = originalPath ? await loadConfigFromFile(env, originalPath, process.cwd()) : undefined;\n\
const config = mergeConfig(loaded?.config ?? {{}}, {{\n\
cacheDir: resolve(process.cwd(), '.supercov/vitest-cache'),\n\
test: {{ setupFiles: [resolve(process.cwd(), supercovSetupFile(loaded?.config))], maxConcurrency: 1, ...supercovBrowserTest(loaded?.config) }},\n\
}});\n\
supercovSkipCoverageThresholds(config.test?.coverage);\n\
const configuredReporters = loaded?.config?.test?.reporters;\n\
config.test ??= {{}};\n\
config.test.reporters = configuredReporters\n\
? [...(Array.isArray(configuredReporters) ? configuredReporters : [configuredReporters]), new SupercovVitestReporter(configureProjects)]\n\
: ['default', new SupercovVitestReporter(configureProjects)];\n\
return config;\n\
}}\n"
);
atomic_write(&path, source.as_bytes())?;
Ok(path)
}
fn configure_playwright_runtime(
generated: &Path,
project: &CoverageProject,
) -> Result<(), JavascriptFrontendError> {
let adapter_path = generated.join("playwright.mjs");
let mut adapter =
fs::read_to_string(&adapter_path).map_err(|source| io_error(&adapter_path, source))?;
adapter = adapter
.replace("__SUPERCOV_PLAYWRIGHT_MODULE__", &project.playwright_module)
.replace(
"__SUPERCOV_PLAYWRIGHT_TEST_EXPORT__",
&project.playwright_test_export,
)
.replace(
"__SUPERCOV_PHASE_TIMING__",
if std::env::var("SUPERCOV_PHASE_TIMING").as_deref() == Ok("1") {
"1"
} else {
"0"
},
);
if project.playwright_module != "@playwright/test" {
let facade = serde_json::to_string(&project.playwright_module)
.expect("serializing a module specifier cannot fail");
adapter = adapter.replace(
"export * from \"@playwright/test\";",
&format!("export * from {facade};"),
);
}
let mut exports = Vec::new();
if project.playwright_test_export != "test" {
exports.push(format!(
"export {{ instrumentedTest as {} }};",
project.playwright_test_export
));
}
exports.extend(
project
.playwright_exports
.iter()
.filter(|name| {
name.as_str() != "test"
&& name.as_str() != "expect"
&& *name != &project.playwright_test_export
})
.map(|name| {
let encoded = serde_json::to_string(name)
.expect("serializing a JavaScript export name cannot fail");
format!("export const {name} = __supercovAdapterExport(adapter[{encoded}]);")
}),
);
adapter = adapter.replace("/*__SUPERCOV_ADAPTER_EXPORTS__*/", &exports.join("\n"));
atomic_write(&adapter_path, adapter.as_bytes())?;
let loader_path = generated.join("resolve-loader.mjs");
let loader = fs::read_to_string(&loader_path)
.map_err(|source| io_error(&loader_path, source))?
.replace("__SUPERCOV_PLAYWRIGHT_MODULE__", &project.playwright_module);
atomic_write(&loader_path, loader.as_bytes())
}
fn write_playwright_config(
workspace: &Path,
project: &CoverageProject,
generated: &Path,
) -> Result<PathBuf, JavascriptFrontendError> {
let path = generated.join("playwright.config.mjs");
let original = relocated_project_file(workspace, project, project.playwright_config.as_ref());
let original_import = if let Some(original) = &original {
let relative = original
.strip_prefix(workspace)
.map_err(|_| JavascriptFrontendError::UnsafeSourcePath(original.display().to_string()))?
.to_string_lossy()
.replace('\\', "/");
let specifier = serde_json::to_string(&format!("../{relative}"))
.map_err(JavascriptFrontendError::Serialize)?;
format!(
"import {{ existsSync, readFileSync }} from 'node:fs';\n\
import {{ createRequire }} from 'node:module';\n\
const originalFile = fileURLToPath(new URL({specifier}, import.meta.url));\n\
const originalIsModule = (() => {{\n\
if (/\\.m[jt]s$/.test(originalFile)) return true;\n\
if (/\\.c[jt]s$/.test(originalFile)) return false;\n\
for (let folder = dirname(originalFile); ; folder = dirname(folder)) {{\n\
const manifest = resolve(folder, 'package.json');\n\
if (existsSync(manifest)) {{\n\
try {{ return JSON.parse(readFileSync(manifest, 'utf8')).type === 'module'; }} catch {{ return false; }}\n\
}}\n\
if (dirname(folder) === folder) return false;\n\
}}\n\
}})();\n\
const original = originalIsModule\n\
? (await import(pathToFileURL(originalFile).href)).default\n\
: createRequire(import.meta.url)(originalFile);\n"
)
} else {
"const original = {};\n".into()
};
let source = format!(
"import './node_modules/register.mjs';\n\
import {{ dirname, isAbsolute, relative, resolve }} from 'node:path';\n\
import {{ fileURLToPath, pathToFileURL }} from 'node:url';\n\
{original_import}\
const configExport = original && original.__esModule === true && Object.prototype.hasOwnProperty.call(original, 'default') ? original.default : original;\n\
const resolvedValue = typeof configExport === 'function' ? await configExport({{ command: 'test', mode: 'test' }}) : configExport;\n\
const resolved = resolvedValue ?? {{}};\n\
const runtimeProjectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\n\
const originalDirectory = {};
const sourceProjectRoot = process.env.SUPERCOV_SOURCE_PROJECT_ROOT;\n\
const runtimePath = value => {{\n\
if (!value) return value;\n\
const absolute = isAbsolute(value) ? value : resolve(originalDirectory, value);\n\
const local = relative(runtimeProjectRoot, absolute);\n\
if (local === '' || (!local.startsWith('..') && !isAbsolute(local))) return absolute;\n\
if (sourceProjectRoot) {{\n\
const sourceLocal = relative(sourceProjectRoot, absolute);\n\
if (sourceLocal === '' || (!sourceLocal.startsWith('..') && !isAbsolute(sourceLocal))) return resolve(runtimeProjectRoot, sourceLocal);\n\
}}\n\
throw new Error('Supercov refuses a Playwright output/cwd outside the isolated project: ' + absolute);\n\
}};\n\
const normalizeWebServer = server => server ? ({{ ...server, cwd: runtimePath(server.cwd ?? originalDirectory) }}) : server;\n\
// How the configuration resolved, for a later merge: a run of new
// tests with settings of their own is added to this one only where
// nothing that decides what a test does changed. Paths inside the
// copy are written relative to it; every run has a copy of its own.
if (process.env.SUPERCOV_EVIDENCE_DIR && process.env.TEST_WORKER_INDEX === undefined) {{\n\
try {{\n\
const {{ mkdirSync, renameSync, writeFileSync }} = await import('node:fs');\n\
const seen = new WeakSet();\n\
let text = JSON.stringify(resolved, (key, value) => {{\n\
if (typeof value === 'function' || value instanceof RegExp) return String(value);\n\
if (value && typeof value === 'object') {{ if (seen.has(value)) return undefined; seen.add(value); }}\n\
return value;\n\
}});\n\
for (const root of [runtimeProjectRoot, sourceProjectRoot].filter(Boolean)) text = text.split(JSON.stringify(root).slice(1, -1)).join('.');\n\
const directory = resolve(process.cwd(), process.env.SUPERCOV_EVIDENCE_DIR);\n\
mkdirSync(directory, {{ recursive: true }});\n\
const file = resolve(directory, 'playwright-configuration.json');\n\
writeFileSync(`${{file}}.${{process.pid}}`, text);\n\
renameSync(`${{file}}.${{process.pid}}`, file);\n\
}} catch {{}}\n\
}}\n\
const normalized = {{ ...resolved,\n\
testDir: runtimePath(resolved.testDir),\n\
outputDir: runtimePath(resolved.outputDir),\n\
snapshotDir: runtimePath(resolved.snapshotDir),\n\
projects: resolved.projects?.map(project => ({{ ...project, testDir: runtimePath(project.testDir), outputDir: runtimePath(project.outputDir), snapshotDir: runtimePath(project.snapshotDir) }})),\n\
webServer: Array.isArray(resolved.webServer) ? resolved.webServer.map(normalizeWebServer) : normalizeWebServer(resolved.webServer),\n\
}};\n\
const configuredReporters = normalized.reporter;\n\
const reporters = configuredReporters\n\
? (typeof configuredReporters === 'string' ? [[configuredReporters]] : (Array.isArray(configuredReporters[0]) ? configuredReporters : [configuredReporters]))\n\
: [['list']];\n\
const coverageReporter = resolve(runtimeProjectRoot, '.supercov/node_modules/playwrightReporter.mjs');\n\
export default {{ ...normalized, reporter: [...reporters, [coverageReporter]] }};\n",
serde_json::to_string(
&original
.as_ref()
.and_then(|path| path.parent())
.unwrap_or(workspace)
.display()
.to_string()
)
.map_err(JavascriptFrontendError::Serialize)?
);
atomic_write(&path, source.as_bytes())?;
Ok(path)
}
fn write_vitest_commands(generated: &Path) -> Result<(), JavascriptFrontendError> {
let adapter = "import { mkdirSync } from 'node:fs';\n\
import { relative, resolve, sep } from 'node:path';\n\
import { atomicWriteFileSync } from './node_modules/atomic.mjs';\n\
import { inferTestProvenance } from './node_modules/provenance.mjs';\n\
// Browser Mode runs the test file in the browser, where none of this is\n\
// reachable: no node:fs to write evidence, no project root to relativise a\n\
// path against, no env to read. Vitest's own browser command channel carries\n\
// what the browser observed to node, which completes the same payload the node\n\
// setup writes -- so one contract keeps one implementation. The command is\n\
// awaited as part of the test, and node learns the test file from Vitest\n\
// rather than from the browser realm, which cannot be trusted to name it.\n\
const supercovEvidenceSuffix = /^[A-Za-z0-9._-]+$/;\n\
export function supercovBrowserCommands(root) {\n\
return {\n\
__supercovEvidence(context, payload, suffix) {\n\
const evidenceDirectory = process.env['SUPERCOV_EVIDENCE_DIR'];\n\
if (!evidenceDirectory) return;\n\
// A suffix names one directory under the evidence directory. Anything\n\
// else is refused rather than resolved, so a malformed one cannot send\n\
// the write outside the directory it is supposed to stay in.\n\
if (typeof suffix !== 'string' || !supercovEvidenceSuffix.test(suffix))\n\
throw new Error('Supercov refused an unusable evidence suffix: ' + suffix);\n\
const absolute = context.testPath;\n\
const testFile = absolute ? relative(root, absolute).split(sep).join('/') : undefined;\n\
const projectName = payload.projectName ?? context.project?.name;\n\
delete payload.projectName;\n\
// The browser has no environment to read, so the run identity is stamped\n\
// here rather than sent from a realm that cannot know it.\n\
const runId = process.env['SUPERCOV_RUN_ID'];\n\
if (runId && payload.scope) payload.scope.runId = runId;\n\
const complete = Object.assign({}, payload, testFile ? { testFile } : {}, {\n\
provenance: inferTestProvenance({\n\
runner: 'vitest',\n\
file: testFile,\n\
project: projectName,\n\
explicitKind: process.env['SUPERCOV_TEST_KIND'],\n\
}),\n\
});\n\
const directory = resolve(root, evidenceDirectory, suffix);\n\
mkdirSync(directory, { recursive: true });\n\
atomicWriteFileSync(resolve(directory, 'mcdc.json'), JSON.stringify(complete) + '\\n');\n\
},\n\
};\n\
}\n";
atomic_write(&generated.join("vitestCommands.mjs"), adapter.as_bytes())
}
pub fn prepare_javascript_frontend(
workspace: &Path,
project: &CoverageProject,
collector_id: &str,
cache_key: &str,
command: &[String],
) -> Result<PreparedJavascriptFrontend, JavascriptFrontendError> {
let generated = workspace.join(".supercov");
let runtime_directory = generated.join("node_modules");
timed(&SETUP.runtime_ns, || {
copy_runtime(&runtime_directory, collector_id)
})?;
let configuration_started = Instant::now();
configure_playwright_runtime(&runtime_directory, project)?;
let playwright_config_path = write_playwright_config(workspace, project, &generated)?;
let vitest_config_path = write_vitest_config(workspace, project, &generated)?;
account(&SETUP.config_ns, configuration_started);
let mut exclusions = Vec::new();
let mut decisions = BTreeMap::new();
let mut points = BTreeMap::new();
let mut branches = BTreeMap::new();
let mut limitations = BTreeMap::new();
for limitation in &project.source_limitations {
limitations.insert(limitation.id.clone(), limitation_from_source(limitation));
}
let runtime_path = runtime_directory.join("runtime.mjs");
let standalone_runtime =
fs::read_to_string(&runtime_path).map_err(|source| io_error(&runtime_path, source))?;
let browser_suite = project.playwright_config.is_some() || project.browser_runner;
let mut authored = BTreeSet::new();
let sources_started = Instant::now();
for file in &project.source_files {
let path = checked_source_path(workspace, file)?;
let source = fs::read_to_string(&path).map_err(|source| io_error(&path, source))?;
let capability_wrapper = runtime_specifier(file, "capability.mjs")?;
let elide = crate::typescript_imports::elides_type_imports(workspace, file, command);
let instrumented = timed(&SETUP.instrument_ns, || {
instrument_with_import_policy(&source, file, &capability_wrapper, elide)
});
let mut output = match instrumented {
Ok(output) => output,
Err(crate::js_instrumenter::CandidateError::Parse(messages)) => {
let limitation = CandidateLimitation {
id: format!("javascript-file-not-parsed#{file}"),
kind: "source-scope".into(),
file: file.clone(),
line: 1,
column: 0,
source: String::new(),
reason: format!(
"Supercov could not parse this file ({}); it runs as written and nothing in it is measured",
messages.join("; ")
),
};
limitations.insert(limitation.id.clone(), limitation);
continue;
}
Err(source) => {
return Err(JavascriptFrontendError::Instrument {
file: file.clone(),
source,
});
}
};
if output
.code
.contains(crate::js_instrumenter::RUNTIME_TYPES_MODULE)
{
output.code = output.code.replace(
crate::js_instrumenter::RUNTIME_TYPES_MODULE,
&runtime_specifier(file, "runtime.mjs")?,
);
}
bootstrap_runtime(&mut output, &standalone_runtime, browser_suite);
if !browser_suite {
bootstrap_loader(&mut output, file)?;
}
if matches!(
path.extension().and_then(|value| value.to_str()),
Some("ts" | "tsx" | "mts" | "cts")
) {
output.code = generated_source_banner(&output.code);
}
let code = match inline_instrumentation_map(
&output.code,
output.map.as_ref(),
&project.root.join(file),
&source,
) {
Some(code) => code,
None => output.code.clone(),
};
keep_authored(workspace, &mut authored, file, &source)?;
atomic_write(&path, code.as_bytes())?;
exclusions.extend(output.excluded_statements);
for value in output.decisions {
decisions.insert(value.id.clone(), value);
}
for value in output.points {
points.insert(value.id.clone(), value);
}
for value in output.branches {
branches.insert(value.id.clone(), value);
}
for value in output.coverage_limitations {
limitations.insert(value.id.clone(), value);
}
}
account(&SETUP.sources_ns, sources_started);
write_vitest_commands(&generated)?;
let assertions_started = Instant::now();
let mut assertion_calls = 0;
for entry in &project.source_scope.entries {
let path = checked_source_path(workspace, &entry.file)?;
let Ok(source) = fs::read_to_string(&path) else {
continue;
};
let capability_wrapper = (!project.source_files.contains(&entry.file))
.then(|| runtime_specifier(&entry.file, "capability.mjs"))
.transpose()?;
let assertion_runtime = runtime_specifier(&entry.file, "bootstrap.cjs")?;
let output =
match crate::js_instrumenter::instrument_node_assertion_phases_with_runtime_imports(
&source,
&entry.file,
std::slice::from_ref(&project.playwright_module),
capability_wrapper.as_deref(),
Some(&assertion_runtime),
) {
Ok(output) => output,
Err(crate::js_instrumenter::CandidateError::Parse(_)) => continue,
Err(source) => {
return Err(JavascriptFrontendError::Instrument {
file: entry.file.clone(),
source,
});
}
};
if output.assertions > 0 || output.capability_imports > 0 {
keep_authored(workspace, &mut authored, &entry.file, &source)?;
atomic_write(&path, output.code.as_bytes())?;
assertion_calls += output.assertions;
}
}
account(&SETUP.assertion_ns, assertions_started);
let mut manifest = JavascriptManifest {
decisions: decisions.into_values().collect(),
points: points.into_values().collect(),
branches: branches.into_values().collect(),
limitations: limitations.into_values().collect(),
scope: project.source_scope.clone(),
};
manifest.decisions.sort_by_key(|value| {
(
value.file.clone(),
value.line,
value.column,
value.id.clone(),
)
});
manifest.points.sort_by_key(|value| {
(
value.file.clone(),
value.line,
value.column,
value.id.clone(),
)
});
manifest.branches.sort_by_key(|value| {
(
value.file.clone(),
value.line,
value.column,
value.id.clone(),
)
});
manifest.limitations.sort_by_key(|value| {
(
value.file.clone(),
value.line,
value.column,
value.id.clone(),
)
});
atomic_write(
&workspace.join(AUTHORED_LIST),
&serde_json::to_vec(&authored).map_err(JavascriptFrontendError::Serialize)?,
)?;
atomic_write(
&generated.join("statement-exclusions.json"),
&serde_json::to_vec(&exclusions).map_err(JavascriptFrontendError::Serialize)?,
)?;
let manifest_path = generated.join("manifest.json");
let mut encoded =
serde_json::to_vec_pretty(&manifest).map_err(JavascriptFrontendError::Serialize)?;
encoded.push(b'\n');
atomic_write(&manifest_path, &encoded)?;
atomic_write(
&generated.join("instrumentation-complete"),
b"coverage-completeness-v2\n",
)?;
timed(&SETUP.cache_ns, || {
write_javascript_frontend_cache(workspace, project, cache_key, assertion_calls)
})?;
Ok(PreparedJavascriptFrontend {
manifest,
manifest_path,
preload_path: generated.join("node_modules/register.mjs"),
playwright_config_path,
vitest_config_path,
assertion_calls,
})
}
#[cfg(test)]
mod tests {
#[test]
fn ancestor_description_names_each_component_and_its_state() {
let root = std::env::temp_dir().join(format!("supercov-ancestors-{}", unique()));
fs::create_dir_all(root.join("present")).unwrap();
fs::write(root.join("present/file.txt"), b"x").unwrap();
let described = super::describe_ancestors(&root.join("present/file.txt/child"));
assert!(described.contains("present=dir"), "{described}");
assert!(described.contains("file.txt=file"), "{described}");
assert!(described.ends_with("child=missing"), "{described}");
fs::remove_dir_all(&root).unwrap();
}
use super::*;
use crate::project_discovery::discover_coverage_project;
fn temporary(name: &str) -> PathBuf {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../target/supercov-test-fixtures")
.join(format!("javascript-frontend-{name}-{}", unique()));
fs::create_dir_all(&path).unwrap();
crate::workspace::canonicalize_simplified(path).unwrap()
}
#[test]
fn browser_mode_gets_a_browser_setup_and_the_command_that_carries_its_evidence() {
let generated = temporary("browser-mode-config");
write_vitest_commands(&generated).unwrap();
let adapter = fs::read_to_string(generated.join("vitestCommands.mjs")).unwrap();
assert!(adapter.contains("export function supercovBrowserCommands(root)"));
assert!(adapter.contains("__supercovEvidence(context, payload, suffix)"));
assert!(adapter.contains("const absolute = context.testPath;"));
assert!(!adapter.contains("configureServer"), "{adapter}");
assert!(!adapter.contains("__supercov/evidence"), "{adapter}");
assert!(adapter.contains("supercovEvidenceSuffix = /^[A-Za-z0-9._-]+$/"));
assert!(adapter.contains("Supercov refused an unusable evidence suffix"));
}
#[test]
fn a_browser_mode_project_loads_the_setup_that_can_run_in_a_browser() {
let workspace = temporary("browser-mode-setup");
let generated = workspace.join(".supercov");
fs::create_dir_all(&generated).unwrap();
fs::write(workspace.join("package.json"), "{\"type\":\"module\"}\n").unwrap();
fs::create_dir_all(workspace.join("src")).unwrap();
fs::write(
workspace.join("src/example.mjs"),
"export const one = () => 1;\n",
)
.unwrap();
let project = discover_coverage_project(
&workspace,
&BTreeMap::new(),
&["npx".into(), "vitest".into(), "run".into()],
)
.unwrap();
let path = write_vitest_config(&workspace, &project, &generated).unwrap();
let config = fs::read_to_string(path).unwrap();
assert!(config.contains("config?.test?.browser?.enabled"));
assert!(config.contains("'.supercov/node_modules/vitestBrowser.mjs'"));
assert!(config.contains("'.supercov/node_modules/vitest.mjs'"));
assert!(config.contains("browser: { commands: supercovBrowserCommands(process.cwd()) }"));
assert!(config.contains("...supercovBrowserTest(loaded?.config)"));
}
#[test]
fn runtime_isolation_replaces_only_the_assignment_marker() {
let source = concat!(
"const runtimeInstanceToken = \"__SUPERCOV_RUNTIME_INSTANCE__\";\n",
"const selected = runtimeInstanceToken === \"__SUPERCOV_\" + \"RUNTIME_INSTANCE__\";\n"
);
let isolated = isolate_runtime(source, "collector-123").unwrap();
assert!(isolated.contains("runtimeInstanceToken = \"collector-123\""));
assert!(isolated.contains("=== \"__SUPERCOV_\" + \"RUNTIME_INSTANCE__\""));
}
#[test]
fn copied_runtime_does_not_reference_unshipped_source_maps() {
let generated = temporary("runtime-source-maps");
copy_runtime(&generated, "collector-test").unwrap();
for name in ["vitest.mjs", "provenance.mjs", "atomic.mjs"] {
let contents = fs::read_to_string(generated.join(name)).unwrap();
assert!(
!contents.contains("sourceMappingURL"),
"runtime shim retained a source-map directive: {name}"
);
}
fs::remove_dir_all(generated).unwrap();
}
#[test]
fn prepares_sorted_complete_manifest_without_touching_source_project() {
let source_root = temporary("source");
let workspace = temporary("workspace");
fs::create_dir_all(source_root.join("src")).unwrap();
fs::write(
source_root.join("src/example.mjs"),
"export function value(a, b) { if (a || b) return 1; return 0; }\n",
)
.unwrap();
fs::write(source_root.join("package.json"), "{\"type\":\"module\"}\n").unwrap();
fs::create_dir_all(workspace.join("src")).unwrap();
fs::create_dir_all(workspace.join(".supercov")).unwrap();
fs::copy(
source_root.join("src/example.mjs"),
workspace.join("src/example.mjs"),
)
.unwrap();
let project = discover_coverage_project(
&source_root,
&BTreeMap::new(),
&["node".into(), "--test".into()],
)
.unwrap();
let original = fs::read_to_string(source_root.join("src/example.mjs")).unwrap();
let prepared =
prepare_javascript_frontend(&workspace, &project, "collector-test", "cache-test", &[])
.unwrap();
assert_eq!(
fs::read_to_string(source_root.join("src/example.mjs")).unwrap(),
original
);
let transformed = fs::read_to_string(workspace.join("src/example.mjs")).unwrap();
assert!(transformed.contains("__SUPERCOV_DIRECT_RUNTIME__"));
assert_eq!(prepared.manifest.decisions.len(), 1);
assert!(!prepared.manifest.points.is_empty());
assert_eq!(prepared.manifest.scope, project.source_scope);
assert!(prepared.manifest_path.is_file());
assert!(prepared.preload_path.is_file());
assert!(prepared.playwright_config_path.is_file());
assert!(prepared.vitest_config_path.is_file());
assert_eq!(prepared.assertion_calls, 0);
let cache = read_javascript_frontend_cache(&workspace, "cache-test").unwrap();
assert_eq!(
javascript_frontend_reuse_paths(&cache),
[
PathBuf::from(".supercov/frontend-cache.json"),
PathBuf::from(".supercov/frontend-cache-artifacts"),
]
);
assert!(
cache
.artifacts
.iter()
.all(|artifact| !artifact.cache_file.contains("src/")
&& !artifact.cache_file.contains("tests/"))
);
fs::write(workspace.join("src/example.mjs"), &original).unwrap();
fs::remove_file(&prepared.manifest_path).unwrap();
let restored = load_cached_javascript_frontend(&workspace, &cache).unwrap();
assert_eq!(restored.manifest, prepared.manifest);
assert_eq!(
fs::read_to_string(workspace.join("src/example.mjs")).unwrap(),
transformed
);
fs::write(workspace.join(&cache.artifacts[0].cache_file), "corrupt").unwrap();
assert!(read_javascript_frontend_cache(&workspace, "cache-test").is_none());
fs::remove_dir_all(source_root).unwrap();
fs::remove_dir_all(workspace).unwrap();
}
#[test]
fn embedded_runtime_contains_every_declared_shim() {
for name in RUNTIME_FILES {
let bytes = embedded_runtime(name).unwrap();
assert!(!bytes.is_empty(), "embedded runtime is empty: {name}");
}
assert!(
std::str::from_utf8(embedded_runtime("runtime.mjs").unwrap())
.unwrap()
.contains(RUNTIME_INSTANCE_MARKER)
);
}
}