Skip to content

Commit 33febb4

Browse files
committed
Add projectRoot option and use relative paths for CSS module hashes
Fixes parcel-bundler#355
1 parent 6a7d19e commit 33febb4

File tree

10 files changed

+170
-18
lines changed

10 files changed

+170
-18
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ parcel_sourcemap = { version = "2.1.1", features = ["json"] }
3737
data-encoding = "2.3.2"
3838
lazy_static = "1.4.0"
3939
const-str = "0.3.1"
40+
pathdiff = "0.2.1"
4041
# CLI deps
4142
clap = { version = "3.0.6", features = ["derive"], optional = true }
42-
pathdiff = { version = "0.2.1", optional = true }
4343
browserslist-rs = { version = "0.7.0", optional = true }
4444
rayon = "1.5.1"
4545
dashmap = "5.0.0"
@@ -59,7 +59,7 @@ serde_json = "1"
5959
[features]
6060
default = ["grid"]
6161
browserslist = ["browserslist-rs"]
62-
cli = ["clap", "serde_json", "pathdiff", "browserslist", "jemallocator"]
62+
cli = ["clap", "serde_json", "browserslist", "jemallocator"]
6363
grid = []
6464
serde = ["smallvec/serde", "cssparser/serde"]
6565

c/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ pub struct ToCssOptions {
195195
source_map: bool,
196196
input_source_map: *const c_char,
197197
input_source_map_len: usize,
198+
project_root: *const c_char,
198199
targets: Targets,
199200
analyze_dependencies: bool,
200201
pseudo_classes: PseudoClasses,
@@ -284,7 +285,7 @@ pub extern "C" fn lightningcss_stylesheet_parse(
284285
error_recovery: options.error_recovery,
285286
source_index: 0,
286287
warnings: Some(warnings.clone()),
287-
at_rule_parser: None
288+
at_rule_parser: None,
288289
};
289290

290291
let stylesheet = unwrap!(StyleSheet::parse(code, opts), error, std::ptr::null_mut());
@@ -324,6 +325,11 @@ pub extern "C" fn lightningcss_stylesheet_to_css(
324325

325326
let opts = PrinterOptions {
326327
minify: options.minify,
328+
project_root: if options.project_root.is_null() {
329+
None
330+
} else {
331+
Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.project_root).to_bytes()) })
332+
},
327333
source_map: source_map.as_mut(),
328334
targets: if options.targets != Targets::default() {
329335
Some(options.targets.into())

node/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export interface TransformOptions {
1111
sourceMap?: boolean,
1212
/** An input source map to extend. */
1313
inputSourceMap?: string,
14+
/**
15+
* An optional project root path, used as the source root in the output source map.
16+
* Also used to generate relative paths for sources used in CSS module hashes.
17+
*/
18+
projectRoot?: string,
1419
/** The browser targets for the generated code. */
1520
targets?: Targets,
1621
/** Whether to enable various draft syntax. */

node/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ fn init(mut exports: JsObject) -> napi::Result<()> {
387387
#[serde(rename_all = "camelCase")]
388388
struct Config {
389389
pub filename: Option<String>,
390+
pub project_root: Option<String>,
390391
#[serde(with = "serde_bytes")]
391392
pub code: Vec<u8>,
392393
pub targets: Option<Browsers>,
@@ -432,6 +433,7 @@ struct CssModulesConfig {
432433
#[serde(rename_all = "camelCase")]
433434
struct BundleConfig {
434435
pub filename: String,
436+
pub project_root: Option<String>,
435437
pub targets: Option<Browsers>,
436438
pub minify: Option<bool>,
437439
pub source_map: Option<bool>,
@@ -479,8 +481,9 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult<'i>, Co
479481
let warnings = Some(Arc::new(RwLock::new(Vec::new())));
480482

481483
let filename = config.filename.clone().unwrap_or_default();
484+
let project_root = config.project_root.as_ref().map(|p| p.as_ref());
482485
let mut source_map = if config.source_map.unwrap_or_default() {
483-
let mut sm = SourceMap::new("/");
486+
let mut sm = SourceMap::new(project_root.unwrap_or("/"));
484487
sm.add_source(&filename);
485488
sm.set_source_content(0, code)?;
486489
Some(sm)
@@ -528,6 +531,7 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult<'i>, Co
528531
stylesheet.to_css(PrinterOptions {
529532
minify: config.minify.unwrap_or_default(),
530533
source_map: source_map.as_mut(),
534+
project_root,
531535
targets: config.targets,
532536
analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
533537
match d {
@@ -578,8 +582,9 @@ fn compile_bundle<'i, P: SourceProvider>(
578582
fs: &'i P,
579583
config: &BundleConfig,
580584
) -> Result<TransformResult<'i>, CompileError<'i, P::Error>> {
585+
let project_root = config.project_root.as_ref().map(|p| p.as_ref());
581586
let mut source_map = if config.source_map.unwrap_or_default() {
582-
Some(SourceMap::new("/"))
587+
Some(SourceMap::new(project_root.unwrap_or("/")))
583588
} else {
584589
None
585590
};
@@ -624,6 +629,7 @@ fn compile_bundle<'i, P: SourceProvider>(
624629
stylesheet.to_css(PrinterOptions {
625630
minify: config.minify.unwrap_or_default(),
626631
source_map: source_map.as_mut(),
632+
project_root,
627633
targets: config.targets,
628634
analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
629635
match d {
@@ -729,6 +735,7 @@ fn compile_attr<'i>(
729735
attr.to_css(PrinterOptions {
730736
minify: config.minify,
731737
source_map: None,
738+
project_root: None,
732739
targets: config.targets,
733740
analyze_dependencies: if config.analyze_dependencies {
734741
Some(DependencyOptions::default())

src/bundler.rs

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ use crate::{
3434
layer::{LayerBlockRule, LayerName},
3535
Location,
3636
},
37-
values::ident::DashedIdentReference, traits::ToCss,
37+
traits::ToCss,
38+
values::ident::DashedIdentReference,
3839
};
3940
use crate::{
4041
error::{Error, ParserError},
@@ -189,7 +190,7 @@ impl<'i, T: std::error::Error> BundleErrorKind<'i, T> {
189190

190191
impl<'a, 'o, 's, P: SourceProvider, T: AtRuleParser<'a> + Clone + Sync + Send> Bundler<'a, 'o, 's, P, T>
191192
where
192-
T::AtRule: Sync + Send + ToCss
193+
T::AtRule: Sync + Send + ToCss,
193194
{
194195
/// Creates a new Bundler using the given source provider.
195196
/// If a source map is given, the content of each source file included in the bundle will
@@ -547,7 +548,11 @@ where
547548
fn order(&mut self) {
548549
process(self.stylesheets.get_mut().unwrap(), 0, &mut HashSet::new());
549550

550-
fn process<'i, T: AtRuleParser<'i>>(stylesheets: &mut Vec<BundleStyleSheet<'i, '_, T>>, source_index: u32, visited: &mut HashSet<u32>) {
551+
fn process<'i, T: AtRuleParser<'i>>(
552+
stylesheets: &mut Vec<BundleStyleSheet<'i, '_, T>>,
553+
source_index: u32,
554+
visited: &mut HashSet<u32>,
555+
) {
551556
if visited.contains(&source_index) {
552557
return;
553558
}
@@ -722,6 +727,7 @@ mod tests {
722727
use indoc::indoc;
723728
use std::collections::HashMap;
724729

730+
#[derive(Clone)]
725731
struct TestProvider {
726732
map: HashMap<PathBuf, String>,
727733
}
@@ -789,7 +795,11 @@ mod tests {
789795
stylesheet.to_css(PrinterOptions::default()).unwrap().code
790796
}
791797

792-
fn bundle_css_module<P: SourceProvider>(fs: P, entry: &str) -> (String, CssModuleExports) {
798+
fn bundle_css_module<P: SourceProvider>(
799+
fs: P,
800+
entry: &str,
801+
project_root: Option<&str>,
802+
) -> (String, CssModuleExports) {
793803
let mut bundler = Bundler::new(
794804
&fs,
795805
None,
@@ -803,7 +813,12 @@ mod tests {
803813
);
804814
let mut stylesheet = bundler.bundle(Path::new(entry)).unwrap();
805815
stylesheet.minify(MinifyOptions::default()).unwrap();
806-
let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
816+
let res = stylesheet
817+
.to_css(PrinterOptions {
818+
project_root,
819+
..PrinterOptions::default()
820+
})
821+
.unwrap();
807822
(res.code, res.exports.unwrap())
808823
}
809824

@@ -1688,6 +1703,7 @@ mod tests {
16881703
},
16891704
},
16901705
"/a.css",
1706+
None,
16911707
);
16921708
assert_eq!(
16931709
code,
@@ -1722,6 +1738,7 @@ mod tests {
17221738
},
17231739
},
17241740
"/a.css",
1741+
None,
17251742
);
17261743
assert_eq!(
17271744
code,
@@ -1763,6 +1780,7 @@ mod tests {
17631780
},
17641781
},
17651782
"/a.css",
1783+
None,
17661784
);
17671785
assert_eq!(
17681786
code,
@@ -1802,6 +1820,7 @@ mod tests {
18021820
},
18031821
},
18041822
"/a.css",
1823+
None,
18051824
);
18061825
assert_eq!(
18071826
code,
@@ -1824,6 +1843,59 @@ mod tests {
18241843
"a" => "_6lixEq_a"
18251844
}
18261845
);
1846+
1847+
// Hashes are stable between project roots.
1848+
let expected = indoc! { r#"
1849+
.dyGcAa_b {
1850+
background: #ff0;
1851+
}
1852+
1853+
.CK9avG_a {
1854+
background: #fff;
1855+
}
1856+
"#};
1857+
1858+
let (code, _) = bundle_css_module(
1859+
TestProvider {
1860+
map: fs! {
1861+
"/foo/bar/a.css": r#"
1862+
@import "b.css";
1863+
.a {
1864+
background: white;
1865+
}
1866+
"#,
1867+
"/foo/bar/b.css": r#"
1868+
.b {
1869+
background: yellow;
1870+
}
1871+
"#
1872+
},
1873+
},
1874+
"/foo/bar/a.css",
1875+
Some("/foo/bar"),
1876+
);
1877+
assert_eq!(code, expected);
1878+
1879+
let (code, _) = bundle_css_module(
1880+
TestProvider {
1881+
map: fs! {
1882+
"/x/y/z/a.css": r#"
1883+
@import "b.css";
1884+
.a {
1885+
background: white;
1886+
}
1887+
"#,
1888+
"/x/y/z/b.css": r#"
1889+
.b {
1890+
background: yellow;
1891+
}
1892+
"#
1893+
},
1894+
},
1895+
"/x/y/z/a.css",
1896+
Some("/x/y/z"),
1897+
);
1898+
assert_eq!(code, expected);
18271899
}
18281900

18291901
#[test]

src/css_modules.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ use crate::properties::css_modules::{Composes, Specifier};
1313
use crate::selector::SelectorList;
1414
use data_encoding::{Encoding, Specification};
1515
use lazy_static::lazy_static;
16+
use pathdiff::diff_paths;
1617
use serde::Serialize;
1718
use smallvec::{smallvec, SmallVec};
19+
use std::borrow::Cow;
1820
use std::collections::hash_map::DefaultHasher;
1921
use std::collections::HashMap;
2022
use std::fmt::Write;
@@ -224,16 +226,32 @@ impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> {
224226
pub fn new(
225227
config: &'a Config<'b>,
226228
sources: &'c Vec<String>,
229+
project_root: Option<&'c str>,
227230
references: &'a mut HashMap<String, CssModuleReference>,
228231
) -> Self {
232+
let project_root = project_root.map(|p| Path::new(p));
233+
let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect();
234+
let hashes = sources
235+
.iter()
236+
.map(|path| {
237+
// Make paths relative to project root so hashes are stable.
238+
let source = match project_root {
239+
Some(project_root) if path.is_absolute() => {
240+
diff_paths(path, project_root).map_or(Cow::Borrowed(*path), Cow::Owned)
241+
}
242+
_ => Cow::Borrowed(*path),
243+
};
244+
hash(
245+
&source.to_string_lossy(),
246+
matches!(config.pattern.segments[0], Segment::Hash),
247+
)
248+
})
249+
.collect();
229250
Self {
230251
config,
231-
sources: sources.iter().map(|filename| Path::new(filename)).collect(),
232-
hashes: sources
233-
.iter()
234-
.map(|source| hash(&source, matches!(config.pattern.segments[0], Segment::Hash)))
235-
.collect(),
236252
exports_by_source_index: sources.iter().map(|_| HashMap::new()).collect(),
253+
sources,
254+
hashes,
237255
references,
238256
}
239257
}

src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20224,6 +20224,45 @@ mod tests {
2022420224
..Default::default()
2022520225
},
2022620226
);
20227+
20228+
// Stable hashes between project roots.
20229+
fn test_project_root(project_root: &str, filename: &str, hash: &str) {
20230+
let stylesheet = StyleSheet::parse(
20231+
r#"
20232+
.foo {
20233+
background: red;
20234+
}
20235+
"#,
20236+
ParserOptions {
20237+
filename: filename.into(),
20238+
css_modules: Some(Default::default()),
20239+
..ParserOptions::default()
20240+
},
20241+
)
20242+
.unwrap();
20243+
let res = stylesheet
20244+
.to_css(PrinterOptions {
20245+
project_root: Some(project_root),
20246+
..PrinterOptions::default()
20247+
})
20248+
.unwrap();
20249+
assert_eq!(
20250+
res.code,
20251+
format!(
20252+
indoc! {r#"
20253+
.{}_foo {{
20254+
background: red;
20255+
}}
20256+
"#},
20257+
hash
20258+
)
20259+
);
20260+
}
20261+
20262+
test_project_root("/foo/bar", "/foo/bar/test.css", "EgL3uq");
20263+
test_project_root("/foo", "/foo/test.css", "EgL3uq");
20264+
test_project_root("/foo/bar", "/foo/bar/baz/test.css", "xLEkNW");
20265+
test_project_root("/foo", "/foo/baz/test.css", "xLEkNW");
2022720266
}
2022820267

2022920268
#[test]

0 commit comments

Comments
 (0)