use super::*;
use crate::{css_modules, printer::PrinterOptions, stylesheet::MinifyOptions};
use serde_json::Value;
use std::{collections::HashMap, io, sync::Condvar, time::Duration};
struct Provider {
files: HashMap<PathBuf, String>,
aliases: HashMap<String, PathBuf>,
entry: PathBuf,
first: Option<String>,
target: Option<PathBuf>,
threads: usize,
read: Mutex<bool>,
wake: Condvar,
}
impl SourceProvider for Provider {
type Error = io::Error;
fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
if self.target.as_deref() == Some(file) {
*self.read.lock().unwrap() = true;
self.wake.notify_all();
}
self
.files
.get(file)
.map(String::as_str)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("fixture missing {}", file.display())))
}
fn resolve(&self, specifier: &str, origin: &Path) -> Result<ResolveResult, Self::Error> {
if self.threads > 1 && origin == self.entry && self.first.as_deref() == Some(specifier) {
let (read, _) = self
.wake
.wait_timeout_while(self.read.lock().unwrap(), Duration::from_secs(3), |read| !*read)
.unwrap();
assert!(*read, "the later physical read must precede the first resolve");
}
if specifier.starts_with("https:") {
Ok(ResolveResult::External(specifier.to_owned()))
} else {
Ok(
self
.aliases
.get(specifier)
.cloned()
.unwrap_or_else(|| origin.with_file_name(specifier))
.into(),
)
}
}
}
fn provider(case: &Value, threads: usize) -> Provider {
Provider {
files: case["files"]
.as_object()
.unwrap()
.iter()
.map(|(name, source)| (PathBuf::from(name), source.as_str().unwrap().to_owned()))
.collect(),
aliases: case["aliases"]
.as_object()
.unwrap()
.iter()
.map(|(name, path)| (name.clone(), PathBuf::from(path.as_str().unwrap())))
.collect(),
entry: PathBuf::from(case["entry"].as_str().unwrap()),
first: case["forceReadBeforeResolve"]["specifier"].as_str().map(str::to_owned),
target: case["forceReadBeforeResolve"]["path"].as_str().map(PathBuf::from),
threads,
read: Mutex::new(false),
wake: Condvar::new(),
}
}
#[test]
fn whole_original_and_forced_authored_contexts() {
let inputs: Value =
serde_json::from_str(include_str!("../../tests/vize_fixtures/bundler-before-inputs.json")).unwrap();
assert_eq!(inputs["cases"].as_array().unwrap().len(), 21);
for threads in [1, 8, 32] {
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
for case in inputs["cases"].as_array().unwrap() {
pool.install(|| {
let fs = provider(case, threads);
let modules = case["cssModules"].as_bool().unwrap_or(false);
let mut map = SourceMap::new("/");
let mut bundler = Bundler::new(&fs, Some(&mut map), ParserOptions {
css_modules: modules.then(|| css_modules::Config {
dashed_idents: true, pattern: css_modules::Pattern::parse("[hash]_[local]").unwrap(), ..Default::default()
}), ..Default::default()
});
let id = case["id"].as_str().unwrap();
let result = bundler.bundle(&fs.entry);
if let Err(error) = result {
let (kind, filename, line, column) = match id {
"competing-io-layer-error" => ("UnsupportedLayerCombination", "/a.css", 1, 1),
"competing-parser-condition-error" => ("UnsupportedImportCondition", "/a.css", 1, 1),
"external-after-bundled-constraint" => ("ExternalImportAfterBundledImport", "", 1, 1),
"css-module-competing-external-error" => ("ResolverError(Custom { kind: NotFound, error: \"fixture missing /missing.css\" })", "/a.css", 0, 1),
_ => panic!("unexpected {id}/pool{threads}: {error:?}"),
};
assert_eq!(format!("{:?}", error.kind), kind);
assert_eq!(error.loc, Some(ErrorLocation { filename: filename.to_owned(), line, column }));
let display = match id {
"competing-io-layer-error" => "Unsupported layer combination in @import at /a.css:1:1",
"competing-parser-condition-error" => "Unsupported import condition at /a.css:1:1",
"external-after-bundled-constraint" => "An external `@import` was found after a bundled `@import`. This may result in unintended selector order. at :1:1",
"css-module-competing-external-error" => "fixture missing /missing.css at /a.css:0:1",
_ => unreachable!(),
};
assert_eq!(error.to_string(), display);
return;
}
let mut stylesheet = result.unwrap();
if case["minify"].as_bool().unwrap_or(false) {
stylesheet.minify(MinifyOptions::default()).unwrap();
}
let printed = stylesheet.to_css(PrinterOptions { source_map: Some(&mut map), ..Default::default() }).unwrap();
assert!(!matches!(id, "competing-io-layer-error" | "competing-parser-condition-error" | "external-after-bundled-constraint" | "css-module-competing-external-error"), "expected a complete error for {id}/pool{threads}");
let delayed_expected = if id == "delayed-multifile-map" {
let members = ["! entry license ", "! b license ", "! c license ", "! d license "];
assert_eq!(stylesheet.license_comments.len(), members.len());
for member in members {
assert_eq!(stylesheet.license_comments.iter().filter(|value| value.as_ref() == member).count(), 1);
}
let mut expected = String::new();
for member in &stylesheet.license_comments {
expected.push_str(&format!("/*{member}*/\n"));
}
expected.push_str(".d {\n color: #000;\n}\n\n.b {\n color: green;\n}\n\n.c {\n color: #00f;\n}\n\n.a {\n color: red;\n}\n");
Some(expected)
} else {
None
};
let expected = delayed_expected.as_deref().or_else(|| case["wholeExpected"].as_str()).unwrap_or_else(|| match id {
"first-context-diamond" => "@media print, screen {\n .b {\n color: #ff0;\n }\n}\n\n@media print and (color), screen and (color) {\n .c {\n color: green;\n }\n}\n\n@media screen {\n .d {\n color: #00f;\n }\n}\n\n.a {\n color: red;\n}\n",
"nested-layer-statements" => "@layer foo.one, foo.two;\n@layer foo.bar.three;\n\n@layer foo.bar {\n .c {\n color: #00f;\n }\n}\n\n@layer foo {\n .b {\n color: green;\n }\n}\n",
"css-module-mixed-import-composes" => "._9z6RGq_x {\n color: green;\n}\n\n._6lixEq_a {\n color: red;\n}\n",
"ordinary-self-import" => ".a {\n color: red;\n}\n",
"ordinary-two-file-cycle" => ".b {\n color: green;\n}\n\n.a {\n color: red;\n}\n",
"css-module-first-root-self-composes" => "._6lixEq_a {\n color: red;\n}\n\n._6lixEq_b {\n color: green;\n}\n",
_ => panic!("no explicit source-order reference for {id}"),
});
assert_eq!(printed.code, expected, "{id}/pool{threads}");
if let Some(exports) = printed.exports {
assert!(modules);
let self_composes = id == "css-module-first-root-self-composes";
assert_eq!(exports.len(), if self_composes { 2 } else { 1 });
let a = exports.get("a").unwrap();
assert_eq!(a.name, "_6lixEq_a");
if self_composes {
assert!(a.composes.is_empty());
} else {
assert_eq!(a.composes, vec![css_modules::CssModuleReference::Local {
name: "_9z6RGq_x".to_owned(),
}]);
}
assert!(!a.is_referenced);
if self_composes {
let b = exports.get("b").unwrap();
assert_eq!(b.name, "_6lixEq_b");
assert!(b.composes.is_empty());
assert!(!b.is_referenced);
}
assert!(printed.references.as_ref().is_some_and(|references| references.is_empty()));
} else {
assert!(!modules);
assert!(printed.references.is_none());
}
assert!(printed.dependencies.is_none());
let json = map.to_json(None).unwrap();
let mut decoded = SourceMap::from_json("/", &json).unwrap();
assert_eq!(decoded.to_json(None).unwrap(), json);
assert_eq!(decoded.get_mappings(), map.get_mappings());
assert_eq!(decoded.get_sources(), map.get_sources());
assert_eq!(decoded.get_sources_content(), map.get_sources_content());
assert_eq!(decoded.get_names(), map.get_names());
});
}
}
}
#[test]
fn same_owner_backward_composes_preserves_transitive_exports() {
for threads in [1, 8, 32] {
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
pool.install(|| {
let case = serde_json::json!({
"entry": "/a.css", "aliases": {}, "files": {
"/a.css": "@import \"b.css\";.root{composes:a from \"b.css\";color:black}",
"/b.css": ".c{color:red}.b{composes:c;color:blue}.a{composes:b from \"b.css\";color:green}"
}
});
let fs = provider(&case, threads);
let mut bundler = Bundler::new(&fs, None, ParserOptions {
css_modules: Some(css_modules::Config {
pattern: css_modules::Pattern::parse("[hash]_[local]").unwrap(), ..Default::default()
}), ..Default::default()
});
let mut sheet = bundler.bundle(&fs.entry).unwrap();
sheet.minify(MinifyOptions::default()).unwrap();
let printed = sheet.to_css(PrinterOptions::default()).unwrap();
assert_eq!(printed.code, "._9z6RGq_c {\n color: red;\n}\n\n._9z6RGq_b {\n color: #00f;\n}\n\n._9z6RGq_a {\n color: green;\n}\n\n._6lixEq_root {\n color: #000;\n}\n");
let exports = printed.exports.unwrap();
assert_eq!(exports.len(), 1);
let root = exports.get("root").unwrap();
assert_eq!(root.name, "_6lixEq_root");
assert!(!root.is_referenced);
assert_eq!(root.composes, ["a", "b", "c"].map(|name| css_modules::CssModuleReference::Local {
name: format!("_9z6RGq_{name}"),
}));
assert!(printed.references.unwrap().is_empty());
assert!(printed.dependencies.is_none());
});
}
}
#[test]
fn same_owner_dashed_reference_keeps_export_usage_flags() {
for threads in [1, 8, 32] {
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
pool.install(|| {
let case = serde_json::json!({
"entry": "/a.css", "aliases": {}, "files": {
"/a.css": ":root{--tone:red}.a{color:var(--tone from \"a.css\")}" }
});
let fs = provider(&case, threads);
let mut bundler = Bundler::new(
&fs,
None,
ParserOptions {
css_modules: Some(css_modules::Config {
dashed_idents: true,
pattern: css_modules::Pattern::parse("[hash]_[local]").unwrap(),
..Default::default()
}),
..Default::default()
},
);
let mut sheet = bundler.bundle(&fs.entry).unwrap();
sheet.minify(MinifyOptions::default()).unwrap();
let printed = sheet.to_css(PrinterOptions::default()).unwrap();
assert_eq!(
printed.code,
":root {\n --_6lixEq_tone: red;\n}\n\n._6lixEq_a {\n color: var(--_6lixEq_tone);\n}\n"
);
let exports = printed.exports.unwrap();
assert_eq!(exports.len(), 2);
for (key, name) in [("a", "_6lixEq_a"), ("--tone", "--_6lixEq_tone")] {
let export = exports.get(key).unwrap();
assert_eq!(export.name, name);
assert!(export.composes.is_empty());
assert!(!export.is_referenced);
}
assert!(printed.references.unwrap().is_empty());
assert!(printed.dependencies.is_none());
});
}
}