Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ itertools = "0.10.1"
smallvec = { version = "1.7.0", features = ["union"] }
bitflags = "1.3.2"
parcel_sourcemap = "2.0.0"
data-encoding = "2.3.2"
lazy_static = "1.4.0"

[dev-dependencies]
indoc = "1.0.3"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ let {code, map} = css.transform({
filename: 'style.css',
code: Buffer.from('.foo { color: red }'),
minify: true,
source_map: true,
sourceMap: true,
targets: {
// Semver versions are represented using a single 24-bit number, with one component per byte.
// e.g. to represent 13.2.0, the following could be used.
Expand Down
33 changes: 30 additions & 3 deletions node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ export interface TransformOptions {
/** Whether to enable minification. */
minify?: boolean,
/** Whether to output a source map. */
source_map?: boolean,
sourceMap?: boolean,
/** The browser targets for the generated code. */
targets?: Targets,
/** Whether to enable various draft syntax. */
drafts?: Drafts
drafts?: Drafts,
/** Whether to compile this file as a CSS module. */
cssModules?: boolean
}

export interface Drafts {
Expand All @@ -24,7 +26,32 @@ export interface TransformResult {
/** The transformed code. */
code: Buffer,
/** The generated source map, if enabled. */
map: Buffer | void
map: Buffer | void,
/** CSS module exports, if enabled. */
exports: CSSModuleExports | void
}

export type CSSModuleExports = {
/** Maps exported (i.e. original) names to local names. */
[name: string]: CSSModuleExport[]
};

export type CSSModuleExport = LocalCSSModuleExport | DependencyCSSModuleExport;

export interface LocalCSSModuleExport {
type: 'local',
/** The local (compiled) name for this export. */
value: string
}

export interface DependencyCSSModuleExport {
type: 'dependency',
value: {
/** The name to reference within the dependency. */
name: string,
/** The dependency specifier for the referenced file. */
specifier: string
}
}

/**
Expand Down
31 changes: 19 additions & 12 deletions node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;

use serde::{Serialize, Deserialize};
use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions};
use parcel_css::stylesheet::{StyleSheet, StyleAttribute, ParserOptions, PrinterOptions};
use parcel_css::targets::Browsers;
use parcel_css::css_modules::CssModuleExports;

// ---------------------------------------------

Expand Down Expand Up @@ -50,11 +51,13 @@ struct SourceMapJson<'a> {
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TransformResult {
#[serde(with = "serde_bytes")]
code: Vec<u8>,
#[serde(with = "serde_bytes")]
map: Option<Vec<u8>>
map: Option<Vec<u8>>,
exports: Option<CssModuleExports>
}

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

#[derive(Serialize, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Config {
pub filename: String,
#[serde(with = "serde_bytes")]
pub code: Vec<u8>,
pub targets: Option<Browsers>,
pub minify: Option<bool>,
pub source_map: Option<bool>,
pub drafts: Option<Drafts>
pub drafts: Option<Drafts>,
pub css_modules: Option<bool>
}

#[derive(Serialize, Debug, Deserialize, Default)]
Expand All @@ -118,16 +123,17 @@ fn compile<'i>(code: &'i str, config: &Config) -> Result<TransformResult, Compil
nesting: match options {
Some(o) => o.nesting,
None => false
}
},
css_modules: config.css_modules.unwrap_or(false)
})?;
stylesheet.minify(config.targets); // TODO: should this be conditional?
let (res, source_map) = stylesheet.to_css(
config.minify.unwrap_or(false),
config.source_map.unwrap_or(false),
config.targets
)?;
let res = stylesheet.to_css(PrinterOptions {
minify: config.minify.unwrap_or(false),
source_map: config.source_map.unwrap_or(false),
targets: config.targets
})?;

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

Ok(TransformResult {
code: res.into_bytes(),
map
code: res.code.into_bytes(),
map,
exports: res.exports
})
}

Expand Down
5 changes: 5 additions & 0 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
font: 14px monospace;
}

label {
display: block;
}

h3 {
margin-bottom: 4px;
}
Expand All @@ -57,6 +61,7 @@ <h1>Parcel CSS Playground</h1>
<div>
<h3>Options</h3>
<label><input id="minify" type="checkbox" checked> Minify</label>
<label><input id="cssModules" type="checkbox"> CSS modules</label>
<h3>Draft syntax</h3>
<label><input id="nesting" type="checkbox" checked> Nesting</label>
<h3>Targets</h3>
Expand Down
9 changes: 8 additions & 1 deletion playground/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ function reflectPlaygroundState(playgroundState) {
minify.checked = playgroundState.minify;
}

if (typeof playgroundState.cssModules !== 'undefined') {
cssModules.checked = playgroundState.cssModules;
}

if (typeof playgroundState.nesting !== 'undefined') {
nesting.checked = playgroundState.nesting;
}
Expand All @@ -47,6 +51,7 @@ function savePlaygroundState() {
const playgroundState = {
minify: minify.checked,
nesting: nesting.checked,
cssModules: cssModules.checked,
targets: getTargets(),
source: source.value,
};
Expand Down Expand Up @@ -88,10 +93,12 @@ async function update() {
targets: Object.keys(targets).length === 0 ? null : targets,
drafts: {
nesting: nesting.checked
}
},
cssModules: cssModules.checked
});

compiled.value = dec.decode(res.code);
console.log(res.exports)

savePlaygroundState();
}
Expand Down
103 changes: 103 additions & 0 deletions src/css_modules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use data_encoding::{Specification, Encoding};
use lazy_static::lazy_static;
use crate::properties::css_modules::{Composes, ComposesFrom};
use parcel_selectors::SelectorList;
use crate::selector::Selectors;
use serde::Serialize;

#[derive(PartialEq, Eq, Hash, Debug, Clone, Serialize)]
#[serde(tag = "type", content = "value", rename_all = "lowercase")]
pub enum CssModuleExport {
Local(String),
Dependency {
name: String,
specifier: String
}
}

pub type CssModuleExports = HashMap<String, Vec<CssModuleExport>>;

lazy_static! {
static ref ENCODER: Encoding = {
let mut spec = Specification::new();
spec.symbols.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-");
spec.encoding().unwrap()
};
}

pub(crate) struct CssModule<'a> {
pub hash: &'a str,
pub exports: &'a mut CssModuleExports
}

impl<'a> CssModule<'a> {
pub fn add_export(&mut self, name: String, export: CssModuleExport) {
match self.exports.entry(name) {
std::collections::hash_map::Entry::Occupied(mut entry) => {
if !entry.get().contains(&export) {
entry.get_mut().push(export);
}
}
std::collections::hash_map::Entry::Vacant(entry) => {
let mut items = Vec::new();
if !items.contains(&export) {
items.push(export);
}
entry.insert(items);
}
}
}

pub fn add_local(&mut self, exported: &str, local: &str) {
let local = CssModuleExport::Local(format!("{}_{}", local, self.hash));
self.add_export(exported.into(), local);
}

pub fn add_global(&mut self, exported: &str, global: &str) {
self.add_export(exported.into(), CssModuleExport::Local(global.into()))
}

pub fn add_dependency(&mut self, exported: &str, name: &str, specifier: &str) {
let dependency = CssModuleExport::Dependency {
name: name.into(),
specifier: specifier.into()
};
self.add_export(exported.into(), dependency)
}

pub fn handle_composes(&mut self, selectors: &SelectorList<Selectors>, composes: &Composes) -> Result<(), ()> {
for sel in &selectors.0 {
if sel.len() == 1 {
match sel.iter_raw_match_order().next().unwrap() {
parcel_selectors::parser::Component::Class(ref id) => {
for name in &composes.names {
match &composes.from {
None => self.add_local(&id.0, &name.0),
Some(ComposesFrom::Global) => self.add_global(&id.0, &name.0),
Some(ComposesFrom::File(file)) => self.add_dependency(&id.0, &name.0, &file)
}
}
continue;
}
_ => {}
}
}

// The composes property can only be used within a simple class selector.
return Err(()) // TODO: custom error
}

Ok(())
}
}

pub(crate) fn hash(s: &str) -> String {
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
let hash = hasher.finish() as u32;

ENCODER.encode(&hash.to_le_bytes())
}
23 changes: 13 additions & 10 deletions src/declaration.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use cssparser::*;
use crate::properties::Property;
use crate::traits::{PropertyHandler, Parse, ToCss};
use crate::traits::{PropertyHandler, ToCss};
use crate::printer::Printer;
use crate::properties::{
align::AlignHandler,
Expand All @@ -22,15 +22,16 @@ use crate::properties::{
grid::GridHandler,
};
use crate::targets::Browsers;
use crate::parser::ParserOptions;

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

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

struct PropertyDeclarationParser;
struct PropertyDeclarationParser<'a> {
options: &'a ParserOptions
}

/// Parse a declaration within {} block: `color: blue`
impl<'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser {
impl<'a, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a> {
type Declaration = Declaration;
type Error = ();

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

/// Default methods reject all at rules.
impl<'i> AtRuleParser<'i> for PropertyDeclarationParser {
impl<'a, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a> {
type Prelude = ();
type AtRule = Declaration;
type Error = ();
Expand All @@ -112,8 +115,8 @@ pub struct Declaration {
}

impl Declaration {
pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ()>> {
let property = input.parse_until_before(Delimiter::Bang, |input| Property::parse(name, input))?;
pub fn parse<'i, 't>(name: CowRcStr<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result<Self, ParseError<'i, ()>> {
let property = input.parse_until_before(Delimiter::Bang, |input| Property::parse(name, input, options))?;
let important = input.try_parse(|input| {
input.expect_delim('!')?;
input.expect_ident_matching("important")
Expand Down
Loading