Skip to content

Commit 39deb6d

Browse files
authored
Dependency analysis (parcel-bundler#25)
1 parent df5d12f commit 39deb6d

File tree

15 files changed

+352
-75
lines changed

15 files changed

+352
-75
lines changed

node/index.d.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ export interface TransformOptions {
1414
/** Whether to enable various draft syntax. */
1515
drafts?: Drafts,
1616
/** Whether to compile this file as a CSS module. */
17-
cssModules?: boolean
17+
cssModules?: boolean,
18+
/**
19+
* Whether to analyze dependencies (e.g. `@import` and `url()`).
20+
* When enabled, `@import` rules are removed, and `url()` dependencies
21+
* are replaced with hashed placeholders that can be replaced with the final
22+
* urls later (after bundling). Dependencies are returned as part of the result.
23+
*/
24+
analyzeDependencies?: boolean
1825
}
1926

2027
export interface Drafts {
@@ -28,7 +35,9 @@ export interface TransformResult {
2835
/** The generated source map, if enabled. */
2936
map: Buffer | void,
3037
/** CSS module exports, if enabled. */
31-
exports: CSSModuleExports | void
38+
exports: CSSModuleExports | void,
39+
/** `@import` and `url()` dependencies, if enabled. */
40+
dependencies: Dependency[] | void
3241
}
3342

3443
export type CSSModuleExports = {
@@ -54,6 +63,42 @@ export interface DependencyCSSModuleExport {
5463
}
5564
}
5665

66+
export type Dependency = ImportDependency | UrlDependency;
67+
68+
export interface ImportDependency {
69+
type: 'import',
70+
/** The url of the `@import` dependency. */
71+
url: string,
72+
/** The media query for the `@import` rule. */
73+
media: string | null,
74+
/** The `supports()` query for the `@import` rule. */
75+
supports: string | null,
76+
/** The source location where the `@import` rule was found. */
77+
loc: SourceLocation
78+
}
79+
80+
export interface UrlDependency {
81+
type: 'url',
82+
/** The url of the dependency. */
83+
url: string,
84+
/** The source location where the `url()` was found. */
85+
loc: SourceLocation
86+
}
87+
88+
export interface SourceLocation {
89+
/** The start location of the dependency. */
90+
start: Location,
91+
/** The end location (inclusive) of the dependency. */
92+
end: Location
93+
}
94+
95+
export interface Location {
96+
/** The line number (1-based). */
97+
line: number,
98+
/** The column number (0-based). */
99+
column: number
100+
}
101+
57102
/**
58103
* Compiles a CSS file, including optionally minifying and lowering syntax to the given
59104
* targets. A source map may also be generated, but this is not enabled by default.
@@ -66,10 +111,24 @@ export interface TransformAttributeOptions {
66111
/** Whether to enable minification. */
67112
minify?: boolean,
68113
/** The browser targets for the generated code. */
69-
targets?: Targets
114+
targets?: Targets,
115+
/**
116+
* Whether to analyze `url()` dependencies.
117+
* When enabled, `url()` dependencies are replaced with hashed placeholders
118+
* that can be replaced with the final urls later (after bundling).
119+
* Dependencies are returned as part of the result.
120+
*/
121+
analyzeDependencies?: boolean
122+
}
123+
124+
export interface TransformAttributeResult {
125+
/** The transformed code. */
126+
code: Buffer,
127+
/** `@import` and `url()` dependencies, if enabled. */
128+
dependencies: Dependency[] | void
70129
}
71130

72131
/**
73132
* Compiles a single CSS declaration list, such as an inline style attribute in HTML.
74133
*/
75-
export declare function transformStyleAttribute(options: TransformAttributeOptions): Buffer;
134+
export declare function transformStyleAttribute(options: TransformAttributeOptions): TransformAttributeResult;

node/src/lib.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use serde::{Serialize, Deserialize};
66
use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions, PrinterOptions};
77
use parcel_css::targets::Browsers;
88
use parcel_css::css_modules::CssModuleExports;
9+
use parcel_css::dependencies::Dependency;
910

1011
// ---------------------------------------------
1112

@@ -26,11 +27,12 @@ pub fn transform(config_val: JsValue) -> Result<JsValue, JsValue> {
2627

2728
#[cfg(target_arch = "wasm32")]
2829
#[wasm_bindgen(js_name = "transformStyleAttribute")]
29-
pub fn transform_style_attribute(config_val: JsValue) -> Result<Vec<u8>, JsValue> {
30+
pub fn transform_style_attribute(config_val: JsValue) -> Result<JsValue, JsValue> {
3031
let config: AttrConfig = from_value(config_val).map_err(JsValue::from)?;
3132
let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
3233
let res = compile_attr(code, &config)?;
33-
Ok(res)
34+
let serializer = Serializer::new().serialize_maps_as_objects(true);
35+
res.serialize(&serializer).map_err(JsValue::from)
3436
}
3537

3638
// ---------------------------------------------
@@ -57,7 +59,8 @@ struct TransformResult {
5759
code: Vec<u8>,
5860
#[serde(with = "serde_bytes")]
5961
map: Option<Vec<u8>>,
60-
exports: Option<CssModuleExports>
62+
exports: Option<CssModuleExports>,
63+
dependencies: Option<Vec<Dependency>>
6164
}
6265

6366
#[cfg(not(target_arch = "wasm32"))]
@@ -83,7 +86,7 @@ fn transform_style_attribute(ctx: CallContext) -> napi::Result<JsUnknown> {
8386
let res = compile_attr(code, &config);
8487

8588
match res {
86-
Ok(res) => Ok(ctx.env.create_buffer_with_data(res)?.into_unknown()),
89+
Ok(res) => ctx.env.to_js_value(&res),
8790
Err(err) => err.throw(ctx, None, code)
8891
}
8992
}
@@ -109,7 +112,8 @@ struct Config {
109112
pub minify: Option<bool>,
110113
pub source_map: Option<bool>,
111114
pub drafts: Option<Drafts>,
112-
pub css_modules: Option<bool>
115+
pub css_modules: Option<bool>,
116+
pub analyze_dependencies: Option<bool>
113117
}
114118

115119
#[derive(Serialize, Debug, Deserialize, Default)]
@@ -130,7 +134,8 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult, Compil
130134
let res = stylesheet.to_css(PrinterOptions {
131135
minify: config.minify.unwrap_or(false),
132136
source_map: config.source_map.unwrap_or(false),
133-
targets: config.targets
137+
targets: config.targets,
138+
analyze_dependencies: config.analyze_dependencies.unwrap_or(false)
134139
})?;
135140

136141
let map = if let Some(mut source_map) = res.source_map {
@@ -154,23 +159,42 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult, Compil
154159
Ok(TransformResult {
155160
code: res.code.into_bytes(),
156161
map,
157-
exports: res.exports
162+
exports: res.exports,
163+
dependencies: res.dependencies
158164
})
159165
}
160166

161167
#[derive(Serialize, Debug, Deserialize)]
168+
#[serde(rename_all = "camelCase")]
162169
struct AttrConfig {
163170
#[serde(with = "serde_bytes")]
164171
pub code: Vec<u8>,
165172
pub targets: Option<Browsers>,
166-
pub minify: Option<bool>
173+
pub minify: Option<bool>,
174+
pub analyze_dependencies: Option<bool>
167175
}
168176

169-
fn compile_attr<'i>(code: &'i str, config: &AttrConfig) -> Result<Vec<u8>, CompileError<'i>> {
177+
#[derive(Serialize)]
178+
#[serde(rename_all = "camelCase")]
179+
struct AttrResult {
180+
#[serde(with = "serde_bytes")]
181+
code: Vec<u8>,
182+
dependencies: Option<Vec<Dependency>>
183+
}
184+
185+
fn compile_attr<'i>(code: &'i str, config: &AttrConfig) -> Result<AttrResult, CompileError<'i>> {
170186
let mut attr = StyleAttribute::parse(&code)?;
171187
attr.minify(config.targets); // TODO: should this be conditional?
172-
let res = attr.to_css(config.minify.unwrap_or(false), config.targets)?;
173-
Ok(res.into_bytes())
188+
let res = attr.to_css(PrinterOptions {
189+
minify: config.minify.unwrap_or(false),
190+
source_map: false,
191+
targets: config.targets,
192+
analyze_dependencies: config.analyze_dependencies.unwrap_or(false)
193+
})?;
194+
Ok(AttrResult {
195+
code: res.code.into_bytes(),
196+
dependencies: res.dependencies
197+
})
174198
}
175199

176200
enum CompileError<'i> {

src/dependencies.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use crate::rules::import::ImportRule;
2+
use crate::values::url::Url;
3+
use serde::Serialize;
4+
use cssparser::SourceLocation;
5+
use crate::printer::Printer;
6+
use crate::traits::ToCss;
7+
use crate::css_modules::hash;
8+
9+
#[derive(Serialize)]
10+
#[serde(tag = "type", rename_all = "lowercase")]
11+
pub enum Dependency {
12+
Import(ImportDependency),
13+
Url(UrlDependency)
14+
}
15+
16+
impl From<&ImportRule> for Dependency {
17+
fn from(rule: &ImportRule) -> Dependency {
18+
Dependency::Import(rule.into())
19+
}
20+
}
21+
22+
#[derive(Serialize)]
23+
pub struct ImportDependency {
24+
pub url: String,
25+
pub supports: Option<String>,
26+
pub media: Option<String>,
27+
pub loc: SourceRange
28+
}
29+
30+
impl From<&ImportRule> for ImportDependency {
31+
fn from(rule: &ImportRule) -> ImportDependency {
32+
let supports = if let Some(supports) = &rule.supports {
33+
let mut s = String::new();
34+
let mut printer = Printer::new("", &mut s, None, false, None);
35+
supports.to_css(&mut printer).unwrap();
36+
Some(s)
37+
} else {
38+
None
39+
};
40+
41+
let media = if !rule.media.media_queries.is_empty() {
42+
let mut s = String::new();
43+
let mut printer = Printer::new("", &mut s, None, false, None);
44+
rule.media.to_css(&mut printer).unwrap();
45+
Some(s)
46+
} else {
47+
None
48+
};
49+
50+
ImportDependency {
51+
url: rule.url.clone(),
52+
supports,
53+
media,
54+
loc: SourceRange::new(rule.loc, 8, rule.url.len() + 2) // TODO: what about @import url(...)?
55+
}
56+
}
57+
}
58+
59+
#[derive(Serialize)]
60+
pub struct UrlDependency {
61+
pub url: String,
62+
pub placeholder: String,
63+
pub loc: SourceRange
64+
}
65+
66+
impl UrlDependency {
67+
pub fn new(url: &Url, filename: &str) -> UrlDependency {
68+
let placeholder = hash(&format!("{}_{}", filename, url.url));
69+
UrlDependency {
70+
url: url.url.clone(),
71+
placeholder,
72+
loc: SourceRange::new(url.loc, 4, url.url.len())
73+
}
74+
}
75+
}
76+
77+
#[derive(Serialize)]
78+
pub struct SourceRange {
79+
pub start: Location,
80+
pub end: Location,
81+
}
82+
83+
#[derive(Serialize)]
84+
pub struct Location {
85+
pub line: u32,
86+
pub column: u32
87+
}
88+
89+
impl SourceRange {
90+
fn new(loc: SourceLocation, offset: u32, len: usize) -> SourceRange {
91+
SourceRange {
92+
start: Location {
93+
line: loc.line + 1,
94+
column: loc.column + offset
95+
},
96+
end: Location {
97+
line: loc.line + 1,
98+
column: loc.column + offset + (len as u32) - 1
99+
}
100+
}
101+
}
102+
}

src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod prefixes;
1414
pub mod vendor_prefix;
1515
pub mod targets;
1616
pub mod css_modules;
17+
pub mod dependencies;
1718

1819
#[cfg(test)]
1920
mod tests {
@@ -47,8 +48,8 @@ mod tests {
4748
fn attr_test(source: &str, expected: &str, minify: bool) {
4849
let mut attr = StyleAttribute::parse(source).unwrap();
4950
attr.minify(None);
50-
let res = attr.to_css(minify, None).unwrap();
51-
assert_eq!(res, expected);
51+
let res = attr.to_css(PrinterOptions { minify, ..PrinterOptions::default() }).unwrap();
52+
assert_eq!(res.code, expected);
5253
}
5354

5455
fn nesting_test(source: &str, expected: &str) {

src/printer.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ use parcel_sourcemap::{SourceMap, OriginalLocation};
44
use crate::vendor_prefix::VendorPrefix;
55
use crate::targets::Browsers;
66
use crate::css_modules::CssModule;
7+
use crate::dependencies::Dependency;
78

89
pub(crate) struct Printer<'a, W> {
10+
pub filename: &'a str,
911
dest: &'a mut W,
1012
source_map: Option<&'a mut SourceMap>,
1113
indent: u8,
@@ -17,17 +19,20 @@ pub(crate) struct Printer<'a, W> {
1719
/// the vendor prefix of whatever is being printed.
1820
pub vendor_prefix: VendorPrefix,
1921
pub in_calc: bool,
20-
pub css_module: Option<CssModule<'a>>
22+
pub css_module: Option<CssModule<'a>>,
23+
pub dependencies: Option<&'a mut Vec<Dependency>>
2124
}
2225

2326
impl<'a, W: Write + Sized> Printer<'a, W> {
2427
pub fn new(
28+
filename: &'a str,
2529
dest: &'a mut W,
2630
source_map: Option<&'a mut SourceMap>,
2731
minify: bool,
2832
targets: Option<Browsers>
2933
) -> Printer<'a, W> {
3034
Printer {
35+
filename,
3136
dest,
3237
source_map,
3338
indent: 0,
@@ -37,7 +42,8 @@ impl<'a, W: Write + Sized> Printer<'a, W> {
3742
targets,
3843
vendor_prefix: VendorPrefix::empty(),
3944
in_calc: false,
40-
css_module: None
45+
css_module: None,
46+
dependencies: None
4147
}
4248
}
4349

src/properties/transform.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ impl ToCss for TransformList {
4848
if let Some(matrix) = self.to_matrix() {
4949
// Generate based on the original transforms.
5050
let mut base = String::new();
51-
self.to_css_base(&mut Printer::new(&mut base, None, true, None))?;
51+
self.to_css_base(&mut Printer::new(dest.filename, &mut base, None, true, None))?;
5252

5353
// Decompose the matrix into transform functions if possible.
5454
// If the resulting length is shorter than the original, use it.
5555
if let Some(d) = matrix.decompose() {
5656
let mut decomposed = String::new();
57-
d.to_css_base(&mut Printer::new(&mut decomposed, None, true, None))?;
57+
d.to_css_base(&mut Printer::new(dest.filename, &mut decomposed, None, true, None))?;
5858
if decomposed.len() < base.len() {
5959
base = decomposed;
6060
}
@@ -63,9 +63,9 @@ impl ToCss for TransformList {
6363
// Also generate a matrix() or matrix3d() representation and compare that.
6464
let mut mat = String::new();
6565
if let Some(matrix) = matrix.to_matrix2d() {
66-
Transform::Matrix(matrix).to_css(&mut Printer::new(&mut mat, None, true, None))?
66+
Transform::Matrix(matrix).to_css(&mut Printer::new(dest.filename, &mut mat, None, true, None))?
6767
} else {
68-
Transform::Matrix3d(matrix).to_css(&mut Printer::new(&mut mat, None, true, None))?
68+
Transform::Matrix3d(matrix).to_css(&mut Printer::new(dest.filename, &mut mat, None, true, None))?
6969
}
7070

7171
if mat.len() < base.len() {

0 commit comments

Comments
 (0)