Skip to content

Commit a4df425

Browse files
authored
Implement CSS modules (parcel-bundler#17)
1 parent 67f89ea commit a4df425

33 files changed

+1047
-239
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ itertools = "0.10.1"
2828
smallvec = { version = "1.7.0", features = ["union"] }
2929
bitflags = "1.3.2"
3030
parcel_sourcemap = "2.0.0"
31+
data-encoding = "2.3.2"
32+
lazy_static = "1.4.0"
3133

3234
[dev-dependencies]
3335
indoc = "1.0.3"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ let {code, map} = css.transform({
4848
filename: 'style.css',
4949
code: Buffer.from('.foo { color: red }'),
5050
minify: true,
51-
source_map: true,
51+
sourceMap: true,
5252
targets: {
5353
// Semver versions are represented using a single 24-bit number, with one component per byte.
5454
// e.g. to represent 13.2.0, the following could be used.

node/index.d.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ export interface TransformOptions {
88
/** Whether to enable minification. */
99
minify?: boolean,
1010
/** Whether to output a source map. */
11-
source_map?: boolean,
11+
sourceMap?: boolean,
1212
/** The browser targets for the generated code. */
1313
targets?: Targets,
1414
/** Whether to enable various draft syntax. */
15-
drafts?: Drafts
15+
drafts?: Drafts,
16+
/** Whether to compile this file as a CSS module. */
17+
cssModules?: boolean
1618
}
1719

1820
export interface Drafts {
@@ -24,7 +26,32 @@ export interface TransformResult {
2426
/** The transformed code. */
2527
code: Buffer,
2628
/** The generated source map, if enabled. */
27-
map: Buffer | void
29+
map: Buffer | void,
30+
/** CSS module exports, if enabled. */
31+
exports: CSSModuleExports | void
32+
}
33+
34+
export type CSSModuleExports = {
35+
/** Maps exported (i.e. original) names to local names. */
36+
[name: string]: CSSModuleExport[]
37+
};
38+
39+
export type CSSModuleExport = LocalCSSModuleExport | DependencyCSSModuleExport;
40+
41+
export interface LocalCSSModuleExport {
42+
type: 'local',
43+
/** The local (compiled) name for this export. */
44+
value: string
45+
}
46+
47+
export interface DependencyCSSModuleExport {
48+
type: 'dependency',
49+
value: {
50+
/** The name to reference within the dependency. */
51+
name: string,
52+
/** The dependency specifier for the referenced file. */
53+
specifier: string
54+
}
2855
}
2956

3057
/**

node/src/lib.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
44

55
use serde::{Serialize, Deserialize};
6-
use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions};
6+
use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions, PrinterOptions};
77
use parcel_css::targets::Browsers;
8+
use parcel_css::css_modules::CssModuleExports;
89

910
// ---------------------------------------------
1011

@@ -50,11 +51,13 @@ struct SourceMapJson<'a> {
5051
}
5152

5253
#[derive(Serialize)]
54+
#[serde(rename_all = "camelCase")]
5355
struct TransformResult {
5456
#[serde(with = "serde_bytes")]
5557
code: Vec<u8>,
5658
#[serde(with = "serde_bytes")]
57-
map: Option<Vec<u8>>
59+
map: Option<Vec<u8>>,
60+
exports: Option<CssModuleExports>
5861
}
5962

6063
#[cfg(not(target_arch = "wasm32"))]
@@ -97,14 +100,16 @@ fn init(mut exports: JsObject) -> napi::Result<()> {
97100
// ---------------------------------------------
98101

99102
#[derive(Serialize, Debug, Deserialize)]
103+
#[serde(rename_all = "camelCase")]
100104
struct Config {
101105
pub filename: String,
102106
#[serde(with = "serde_bytes")]
103107
pub code: Vec<u8>,
104108
pub targets: Option<Browsers>,
105109
pub minify: Option<bool>,
106110
pub source_map: Option<bool>,
107-
pub drafts: Option<Drafts>
111+
pub drafts: Option<Drafts>,
112+
pub css_modules: Option<bool>
108113
}
109114

110115
#[derive(Serialize, Debug, Deserialize, Default)]
@@ -118,16 +123,17 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult, Compil
118123
nesting: match options {
119124
Some(o) => o.nesting,
120125
None => false
121-
}
126+
},
127+
css_modules: config.css_modules.unwrap_or(false)
122128
})?;
123129
stylesheet.minify(config.targets); // TODO: should this be conditional?
124-
let (res, source_map) = stylesheet.to_css(
125-
config.minify.unwrap_or(false),
126-
config.source_map.unwrap_or(false),
127-
config.targets
128-
)?;
130+
let res = stylesheet.to_css(PrinterOptions {
131+
minify: config.minify.unwrap_or(false),
132+
source_map: config.source_map.unwrap_or(false),
133+
targets: config.targets
134+
})?;
129135

130-
let map = if let Some(mut source_map) = source_map {
136+
let map = if let Some(mut source_map) = res.source_map {
131137
source_map.set_source_content(0, code)?;
132138
let mut vlq_output: Vec<u8> = Vec::new();
133139
source_map.write_vlq(&mut vlq_output)?;
@@ -146,8 +152,9 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult, Compil
146152
};
147153

148154
Ok(TransformResult {
149-
code: res.into_bytes(),
150-
map
155+
code: res.code.into_bytes(),
156+
map,
157+
exports: res.exports
151158
})
152159
}
153160

playground/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
font: 14px monospace;
4343
}
4444

45+
label {
46+
display: block;
47+
}
48+
4549
h3 {
4650
margin-bottom: 4px;
4751
}
@@ -57,6 +61,7 @@ <h1>Parcel CSS Playground</h1>
5761
<div>
5862
<h3>Options</h3>
5963
<label><input id="minify" type="checkbox" checked> Minify</label>
64+
<label><input id="cssModules" type="checkbox"> CSS modules</label>
6065
<h3>Draft syntax</h3>
6166
<label><input id="nesting" type="checkbox" checked> Nesting</label>
6267
<h3>Targets</h3>

playground/playground.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ function reflectPlaygroundState(playgroundState) {
2626
minify.checked = playgroundState.minify;
2727
}
2828

29+
if (typeof playgroundState.cssModules !== 'undefined') {
30+
cssModules.checked = playgroundState.cssModules;
31+
}
32+
2933
if (typeof playgroundState.nesting !== 'undefined') {
3034
nesting.checked = playgroundState.nesting;
3135
}
@@ -47,6 +51,7 @@ function savePlaygroundState() {
4751
const playgroundState = {
4852
minify: minify.checked,
4953
nesting: nesting.checked,
54+
cssModules: cssModules.checked,
5055
targets: getTargets(),
5156
source: source.value,
5257
};
@@ -88,10 +93,12 @@ async function update() {
8893
targets: Object.keys(targets).length === 0 ? null : targets,
8994
drafts: {
9095
nesting: nesting.checked
91-
}
96+
},
97+
cssModules: cssModules.checked
9298
});
9399

94100
compiled.value = dec.decode(res.code);
101+
console.log(res.exports)
95102

96103
savePlaygroundState();
97104
}

src/css_modules.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use std::collections::HashMap;
2+
use std::collections::hash_map::DefaultHasher;
3+
use std::hash::{Hash, Hasher};
4+
use data_encoding::{Specification, Encoding};
5+
use lazy_static::lazy_static;
6+
use crate::properties::css_modules::{Composes, ComposesFrom};
7+
use parcel_selectors::SelectorList;
8+
use crate::selector::Selectors;
9+
use serde::Serialize;
10+
11+
#[derive(PartialEq, Eq, Hash, Debug, Clone, Serialize)]
12+
#[serde(tag = "type", content = "value", rename_all = "lowercase")]
13+
pub enum CssModuleExport {
14+
Local(String),
15+
Dependency {
16+
name: String,
17+
specifier: String
18+
}
19+
}
20+
21+
pub type CssModuleExports = HashMap<String, Vec<CssModuleExport>>;
22+
23+
lazy_static! {
24+
static ref ENCODER: Encoding = {
25+
let mut spec = Specification::new();
26+
spec.symbols.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-");
27+
spec.encoding().unwrap()
28+
};
29+
}
30+
31+
pub(crate) struct CssModule<'a> {
32+
pub hash: &'a str,
33+
pub exports: &'a mut CssModuleExports
34+
}
35+
36+
impl<'a> CssModule<'a> {
37+
pub fn add_export(&mut self, name: String, export: CssModuleExport) {
38+
match self.exports.entry(name) {
39+
std::collections::hash_map::Entry::Occupied(mut entry) => {
40+
if !entry.get().contains(&export) {
41+
entry.get_mut().push(export);
42+
}
43+
}
44+
std::collections::hash_map::Entry::Vacant(entry) => {
45+
let mut items = Vec::new();
46+
if !items.contains(&export) {
47+
items.push(export);
48+
}
49+
entry.insert(items);
50+
}
51+
}
52+
}
53+
54+
pub fn add_local(&mut self, exported: &str, local: &str) {
55+
let local = CssModuleExport::Local(format!("{}_{}", local, self.hash));
56+
self.add_export(exported.into(), local);
57+
}
58+
59+
pub fn add_global(&mut self, exported: &str, global: &str) {
60+
self.add_export(exported.into(), CssModuleExport::Local(global.into()))
61+
}
62+
63+
pub fn add_dependency(&mut self, exported: &str, name: &str, specifier: &str) {
64+
let dependency = CssModuleExport::Dependency {
65+
name: name.into(),
66+
specifier: specifier.into()
67+
};
68+
self.add_export(exported.into(), dependency)
69+
}
70+
71+
pub fn handle_composes(&mut self, selectors: &SelectorList<Selectors>, composes: &Composes) -> Result<(), ()> {
72+
for sel in &selectors.0 {
73+
if sel.len() == 1 {
74+
match sel.iter_raw_match_order().next().unwrap() {
75+
parcel_selectors::parser::Component::Class(ref id) => {
76+
for name in &composes.names {
77+
match &composes.from {
78+
None => self.add_local(&id.0, &name.0),
79+
Some(ComposesFrom::Global) => self.add_global(&id.0, &name.0),
80+
Some(ComposesFrom::File(file)) => self.add_dependency(&id.0, &name.0, &file)
81+
}
82+
}
83+
continue;
84+
}
85+
_ => {}
86+
}
87+
}
88+
89+
// The composes property can only be used within a simple class selector.
90+
return Err(()) // TODO: custom error
91+
}
92+
93+
Ok(())
94+
}
95+
}
96+
97+
pub(crate) fn hash(s: &str) -> String {
98+
let mut hasher = DefaultHasher::new();
99+
s.hash(&mut hasher);
100+
let hash = hasher.finish() as u32;
101+
102+
ENCODER.encode(&hash.to_le_bytes())
103+
}

src/declaration.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use cssparser::*;
22
use crate::properties::Property;
3-
use crate::traits::{PropertyHandler, Parse, ToCss};
3+
use crate::traits::{PropertyHandler, ToCss};
44
use crate::printer::Printer;
55
use crate::properties::{
66
align::AlignHandler,
@@ -22,15 +22,16 @@ use crate::properties::{
2222
grid::GridHandler,
2323
};
2424
use crate::targets::Browsers;
25+
use crate::parser::ParserOptions;
2526

2627
#[derive(Debug, PartialEq)]
2728
pub struct DeclarationBlock {
2829
pub declarations: Vec<Declaration>
2930
}
3031

31-
impl Parse for DeclarationBlock {
32-
fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ()>> {
33-
let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser);
32+
impl DeclarationBlock {
33+
pub fn parse<'i, 't>(input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result<Self, ParseError<'i, ()>> {
34+
let mut parser = DeclarationListParser::new(input, PropertyDeclarationParser { options });
3435
let mut declarations = vec![];
3536
while let Some(decl) = parser.next() {
3637
if let Ok(decl) = decl {
@@ -82,10 +83,12 @@ impl DeclarationBlock {
8283
}
8384
}
8485

85-
struct PropertyDeclarationParser;
86+
struct PropertyDeclarationParser<'a> {
87+
options: &'a ParserOptions
88+
}
8689

8790
/// Parse a declaration within {} block: `color: blue`
88-
impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser {
91+
impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a> {
8992
type Declaration = Declaration;
9093
type Error = ();
9194

@@ -94,12 +97,12 @@ impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser {
9497
name: CowRcStr<'i>,
9598
input: &mut cssparser::Parser<'i, 't>,
9699
) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
97-
Declaration::parse(name, input)
100+
Declaration::parse(name, input, self.options)
98101
}
99102
}
100103

101104
/// Default methods reject all at rules.
102-
impl<'i> AtRuleParser<'i> for PropertyDeclarationParser {
105+
impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> {
103106
type Prelude = ();
104107
type AtRule = Declaration;
105108
type Error = ();
@@ -112,8 +115,8 @@ pub struct Declaration {
112115
}
113116

114117
impl Declaration {
115-
pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ()>> {
116-
let property = input.parse_until_before(Delimiter::Bang, |input| Property::parse(name, input))?;
118+
pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result<Self, ParseError<'i, ()>> {
119+
let property = input.parse_until_before(Delimiter::Bang, |input| Property::parse(name, input, options))?;
117120
let important = input.try_parse(|input| {
118121
input.expect_delim('!')?;
119122
input.expect_ident_matching("important")

0 commit comments

Comments
 (0)