swc_css_modules 0.30.0

CSS modules
Documentation
use rustc_hash::FxHashMap;
use swc_atoms::JsWord;
use swc_common::{util::take::Take, Span};
use swc_css_ast::{
    ComplexSelector, ComplexSelectorChildren, ComponentValue, Declaration, DeclarationName,
    Delimiter, DelimiterValue, FunctionName, Ident, KeyframesName, PseudoClassSelectorChildren,
    QualifiedRule, QualifiedRulePrelude, Stylesheet, SubclassSelector,
};
use swc_css_visit::{VisitMut, VisitMutWith};

pub mod imports;

/// Various configurations for the css modules.
///
/// # Note
///
/// This is a trait rather than a struct because api like `fn() -> String` is
/// too restricted and `Box<Fn() -> String` is (needlessly) slow.
pub trait TransformConfig {
    /// Creates a class name for the given `local_name`.
    fn new_name_for(&self, local: &JsWord) -> JsWord;

    // /// Used for `@value` imports.
    // fn get_value(&self, import_source: &str, value_name: &JsWord) ->
    // ComponentValue;
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CssClassName {
    Local {
        /// Tranformed css class name
        name: Ident,
    },
    Global {
        name: Ident,
    },
    Import {
        /// The exported class name. This is the value specified by the user.
        name: Ident,
        /// The module specifier.
        from: JsWord,
    },
}

#[derive(Debug, Clone)]
pub struct TransformResult {
    /// A map of js class name to css class names.
    pub renamed: FxHashMap<JsWord, Vec<CssClassName>>,
}

/// Returns a map from local name to exported name.
pub fn compile<'a>(ss: &mut Stylesheet, config: impl 'a + TransformConfig) -> TransformResult {
    let mut compiler = Compiler {
        config,
        data: Default::default(),
        result: TransformResult {
            renamed: Default::default(),
        },
    };

    ss.visit_mut_with(&mut compiler);

    fn add(result: &mut TransformResult, data: &Data, key: &JsWord, composes: &[CssClassName]) {
        let mut extra_classes = vec![];
        {
            let class_names = result.renamed.entry(key.clone()).or_default();

            class_names.extend(composes.iter().cloned());
        }

        for composed_class_name in composes.iter() {
            if let CssClassName::Local { name } = composed_class_name {
                if let Some(original_class_name) = data.renamed_to_orig.get(&name.value) {
                    extra_classes.extend(
                        result
                            .renamed
                            .entry(original_class_name.clone())
                            .or_default()
                            .split_at(1)
                            .1
                            .to_vec(),
                    );
                }
            }
        }

        {
            let class_names = result.renamed.entry(key.clone()).or_default();

            class_names.extend(extra_classes);
        }
    }

    let composes = compiler.data.composes_inherit.take();

    for (key, composes) in &composes {
        add(&mut compiler.result, &compiler.data, key, composes);
    }
    for (key, composes) in &composes {
        add(&mut compiler.result, &compiler.data, key, composes);
    }
    compiler.result.renamed.iter_mut().for_each(|(_, v)| {
        v.sort();
        v.dedup();
    });

    compiler.result
}

struct Compiler<C>
where
    C: TransformConfig,
{
    config: C,
    data: Data,
    result: TransformResult,
}

#[derive(Default)]
struct Data {
    /// Context for `composes`
    composes_for_current: Option<Vec<CssClassName>>,
    composes_inherit: Vec<(JsWord, Vec<CssClassName>)>,

    renamed_to_orig: FxHashMap<JsWord, JsWord>,
    orig_to_renamed: FxHashMap<JsWord, JsWord>,

    is_global_mode: bool,
    is_in_local_pseudo_class: bool,
}

impl<C> VisitMut for Compiler<C>
where
    C: TransformConfig,
{
    // TODO handle `@counter-style`, CSS modules doesn't support it, but we should
    // to fix it
    fn visit_mut_keyframes_name(&mut self, n: &mut KeyframesName) {
        match n {
            KeyframesName::CustomIdent(n) if !self.data.is_global_mode => {
                n.raw = None;

                rename(
                    n.span,
                    &mut self.config,
                    &mut self.result,
                    &mut self.data.orig_to_renamed,
                    &mut self.data.renamed_to_orig,
                    &mut n.value,
                );
            }
            KeyframesName::Str(n) if !self.data.is_global_mode => {
                n.raw = None;

                rename(
                    n.span,
                    &mut self.config,
                    &mut self.result,
                    &mut self.data.orig_to_renamed,
                    &mut self.data.renamed_to_orig,
                    &mut n.value,
                );
            }
            KeyframesName::PseudoFunction(pseudo_function)
                if pseudo_function.pseudo.value == "local" =>
            {
                match &pseudo_function.name {
                    KeyframesName::CustomIdent(custom_ident) => {
                        *n = KeyframesName::CustomIdent(custom_ident.clone());
                    }
                    KeyframesName::Str(string) => {
                        *n = KeyframesName::Str(string.clone());
                    }
                    _ => {
                        unreachable!();
                    }
                }

                n.visit_mut_with(self);

                return;
            }
            KeyframesName::PseudoPrefix(pseudo_prefix) if pseudo_prefix.pseudo.value == "local" => {
                match &pseudo_prefix.name {
                    KeyframesName::CustomIdent(custom_ident) => {
                        *n = KeyframesName::CustomIdent(custom_ident.clone());
                    }
                    KeyframesName::Str(string) => {
                        *n = KeyframesName::Str(string.clone());
                    }
                    _ => {
                        unreachable!();
                    }
                }

                n.visit_mut_with(self);

                return;
            }
            KeyframesName::PseudoFunction(pseudo_function)
                if pseudo_function.pseudo.value == "global" =>
            {
                match &pseudo_function.name {
                    KeyframesName::CustomIdent(custom_ident) => {
                        *n = KeyframesName::CustomIdent(custom_ident.clone());
                    }
                    KeyframesName::Str(string) => {
                        *n = KeyframesName::Str(string.clone());
                    }
                    _ => {
                        unreachable!();
                    }
                }

                return;
            }
            KeyframesName::PseudoPrefix(pseudo_prefix)
                if pseudo_prefix.pseudo.value == "global" =>
            {
                match &pseudo_prefix.name {
                    KeyframesName::CustomIdent(custom_ident) => {
                        *n = KeyframesName::CustomIdent(custom_ident.clone());
                    }
                    KeyframesName::Str(string) => {
                        *n = KeyframesName::Str(string.clone());
                    }
                    _ => {
                        unreachable!();
                    }
                }

                return;
            }
            _ => {}
        }

        n.visit_mut_children_with(self);
    }

    fn visit_mut_qualified_rule(&mut self, n: &mut QualifiedRule) {
        let old_compose_stack = self.data.composes_for_current.take();

        self.data.composes_for_current = Some(Default::default());

        n.visit_mut_children_with(self);

        if let QualifiedRulePrelude::SelectorList(sel) = &n.prelude {
            let composes = self.data.composes_for_current.take();

            for child in &sel.children {
                if let ComplexSelectorChildren::CompoundSelector(sel) = &child.children[0] {
                    for subclass_sel in &sel.subclass_selectors {
                        if let SubclassSelector::Class(class_sel) = &subclass_sel {
                            if let Some(composes) = &composes {
                                let key = self
                                    .data
                                    .renamed_to_orig
                                    .get(&class_sel.text.value)
                                    .cloned();

                                if let Some(key) = key {
                                    self.data.composes_inherit.push((key, composes.clone()));
                                }
                            }
                        }
                    }
                }
            }
        }

        self.data.composes_for_current = old_compose_stack;
    }

    fn visit_mut_component_values(&mut self, n: &mut Vec<ComponentValue>) {
        n.visit_mut_children_with(self);

        n.retain(|v| match v {
            ComponentValue::Declaration(d) => {
                if let DeclarationName::Ident(ident) = &d.name {
                    if &*ident.value == "composes" {
                        return false;
                    }
                }

                true
            }
            _ => true,
        });
    }

    /// Handles `composes`
    fn visit_mut_declaration(&mut self, n: &mut Declaration) {
        n.visit_mut_children_with(self);

        if let Some(composes_for_current) = &mut self.data.composes_for_current {
            if let DeclarationName::Ident(name) = &n.name {
                if &*name.value == "composes" {
                    // composes: name from 'foo.css'
                    if n.value.len() >= 3 {
                        match (&n.value[n.value.len() - 2], &n.value[n.value.len() - 1]) {
                            (ComponentValue::Ident(ident), ComponentValue::Str(import_source))
                                if ident.value == "from" =>
                            {
                                for class_name in n.value.iter().take(n.value.len() - 2) {
                                    if let ComponentValue::Ident(value) = class_name {
                                        composes_for_current.push(CssClassName::Import {
                                            name: *value.clone(),
                                            from: import_source.value.clone(),
                                        });
                                    }
                                }

                                return;
                            }
                            (ComponentValue::Ident(from), ComponentValue::Ident(global))
                                if from.value == "from" && global.value == "global" =>
                            {
                                for class_name in n.value.iter().take(n.value.len() - 2) {
                                    if let ComponentValue::Ident(value) = class_name {
                                        composes_for_current.push(CssClassName::Global {
                                            name: *value.clone(),
                                        });
                                    }
                                }
                                return;
                            }
                            _ => (),
                        }
                    }

                    for class_name in n.value.iter_mut() {
                        if let ComponentValue::Ident(ident) = class_name {
                            let Ident { span, value, .. } = &mut **ident;
                            let orig = value.clone();
                            rename(
                                *span,
                                &mut self.config,
                                &mut self.result,
                                &mut self.data.orig_to_renamed,
                                &mut self.data.renamed_to_orig,
                                value,
                            );

                            if let Some(new_name) = self.data.orig_to_renamed.get(&orig) {
                                composes_for_current.push(CssClassName::Local {
                                    name: Ident {
                                        span: *span,
                                        value: new_name.clone(),
                                        raw: None,
                                    },
                                });
                            }
                        }
                    }
                }
            }
        }

        if let DeclarationName::Ident(name) = &n.name {
            match &*name.value {
                "animation" => {
                    let mut can_change = true;

                    let mut iteration_count_visited = false;
                    let mut fill_mode_visited = false;
                    let mut direction_visited = false;
                    let mut easing_function_visited = false;
                    let mut play_state_visited = false;

                    for v in &mut n.value {
                        match v {
                            ComponentValue::Ident(ident) => {
                                if !can_change {
                                    continue;
                                }

                                let Ident {
                                    span, value, raw, ..
                                } = &mut **ident;

                                match &**value {
                                    // iteration-count
                                    "infinite" => {
                                        if !iteration_count_visited {
                                            iteration_count_visited = true;
                                            continue;
                                        }
                                    }
                                    // fill-mode
                                    // NOTE: `animation: none:` will be trapped here
                                    "none" | "forwards" | "backwards" | "both" => {
                                        if !fill_mode_visited {
                                            fill_mode_visited = true;
                                            continue;
                                        }
                                    }
                                    // direction
                                    "normal" | "reverse" | "alternate" | "alternate-reverse" => {
                                        if !direction_visited {
                                            direction_visited = true;
                                            continue;
                                        }
                                    }
                                    // easing-function
                                    "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out"
                                    | "step-start" | "step-end" => {
                                        if !easing_function_visited {
                                            easing_function_visited = true;
                                            continue;
                                        }
                                    }
                                    // play-state
                                    "running" | "paused" => {
                                        if !play_state_visited {
                                            play_state_visited = true;
                                            continue;
                                        }
                                    }
                                    _ => {}
                                }

                                *raw = None;

                                rename(
                                    *span,
                                    &mut self.config,
                                    &mut self.result,
                                    &mut self.data.orig_to_renamed,
                                    &mut self.data.renamed_to_orig,
                                    value,
                                );
                                can_change = false;
                            }
                            ComponentValue::Integer(_) => {
                                iteration_count_visited = true;
                            }
                            ComponentValue::Function(f) => {
                                if let FunctionName::Ident(ident) = &f.name {
                                    match &*ident.value {
                                        // easing-function
                                        "steps" | "cubic-bezier" | "linear" => {
                                            easing_function_visited = true;
                                        }
                                        _ => {
                                            // should be syntax error
                                        }
                                    }
                                }
                            }
                            ComponentValue::Delimiter(delimiter) => {
                                if matches!(
                                    &**delimiter,
                                    Delimiter {
                                        value: DelimiterValue::Comma,
                                        ..
                                    }
                                ) {
                                    can_change = true;

                                    // reset all flags
                                    iteration_count_visited = false;
                                    fill_mode_visited = false;
                                    direction_visited = false;
                                    easing_function_visited = false;
                                    play_state_visited = false;
                                }
                            }
                            _ => (),
                        }
                    }
                }
                "animation-name" => {
                    for v in &mut n.value {
                        if let ComponentValue::Ident(ident) = v {
                            let Ident {
                                span, value, raw, ..
                            } = &mut **ident;
                            *raw = None;

                            rename(
                                *span,
                                &mut self.config,
                                &mut self.result,
                                &mut self.data.orig_to_renamed,
                                &mut self.data.renamed_to_orig,
                                value,
                            );
                        }
                    }
                }
                _ => {}
            }
        }
    }

    fn visit_mut_complex_selector(&mut self, n: &mut ComplexSelector) {
        let mut new_children = Vec::with_capacity(n.children.len());

        let old_is_global_mode = self.data.is_global_mode;

        'complex: for mut n in n.children.take() {
            if let ComplexSelectorChildren::CompoundSelector(selector) = &mut n {
                for sel in selector.subclass_selectors.iter_mut() {
                    match sel {
                        SubclassSelector::Class(..) | SubclassSelector::Id(..) => {
                            if !self.data.is_global_mode {
                                process_local(
                                    &mut self.config,
                                    &mut self.result,
                                    &mut self.data.orig_to_renamed,
                                    &mut self.data.renamed_to_orig,
                                    sel,
                                );
                            }
                        }

                        _ => {}
                    }
                }

                for (sel_index, sel) in selector.subclass_selectors.iter_mut().enumerate() {
                    if let SubclassSelector::PseudoClass(class_sel) = sel {
                        match &*class_sel.name.value {
                            "local" => {
                                if let Some(children) = &mut class_sel.children {
                                    if let Some(PseudoClassSelectorChildren::ComplexSelector(
                                        complex_selector,
                                    )) = children.get_mut(0)
                                    {
                                        let old_is_global_mode = self.data.is_global_mode;
                                        let old_inside = self.data.is_global_mode;

                                        self.data.is_global_mode = false;
                                        self.data.is_in_local_pseudo_class = true;

                                        complex_selector.visit_mut_with(self);

                                        let mut complex_selector_children =
                                            complex_selector.children.clone();
                                        prepend_left_subclass_selectors(
                                            &mut complex_selector_children,
                                            &mut selector.subclass_selectors,
                                            sel_index,
                                        );
                                        new_children.extend(complex_selector_children);

                                        self.data.is_global_mode = old_is_global_mode;
                                        self.data.is_in_local_pseudo_class = old_inside;
                                    }
                                } else {
                                    if sel_index > 0 {
                                        if let Some(n) = n.as_mut_compound_selector() {
                                            n.subclass_selectors.remove(sel_index);
                                        }
                                        new_children.push(n);
                                    }
                                    self.data.is_global_mode = false;
                                }

                                continue 'complex;
                            }
                            "global" => {
                                if let Some(children) = &mut class_sel.children {
                                    if let Some(PseudoClassSelectorChildren::ComplexSelector(
                                        complex_selector,
                                    )) = children.get_mut(0)
                                    {
                                        let mut complex_selector_children =
                                            complex_selector.children.clone();
                                        prepend_left_subclass_selectors(
                                            &mut complex_selector_children,
                                            &mut selector.subclass_selectors,
                                            sel_index,
                                        );
                                        new_children.extend(complex_selector_children);
                                    }
                                } else {
                                    if sel_index > 0 {
                                        if let Some(n) = n.as_mut_compound_selector() {
                                            n.subclass_selectors.remove(sel_index);
                                        }
                                        new_children.push(n);
                                    }
                                    self.data.is_global_mode = true;
                                }

                                continue 'complex;
                            }
                            _ => {}
                        }
                    }
                }
            }

            new_children.push(n);
        }

        n.children = new_children;

        self.data.is_global_mode = old_is_global_mode;

        if self.data.is_in_local_pseudo_class {
            return;
        }

        n.visit_mut_children_with(self);
    }

    fn visit_mut_complex_selectors(&mut self, n: &mut Vec<ComplexSelector>) {
        n.visit_mut_children_with(self);

        n.retain_mut(|s| !s.children.is_empty());
    }
}

fn rename<C>(
    span: Span,
    config: &mut C,
    result: &mut TransformResult,
    orig_to_renamed: &mut FxHashMap<JsWord, JsWord>,
    renamed_to_orig: &mut FxHashMap<JsWord, JsWord>,
    name: &mut JsWord,
) where
    C: TransformConfig,
{
    if let Some(renamed) = orig_to_renamed.get(name) {
        *name = renamed.clone();
        return;
    }

    let new = config.new_name_for(name);

    orig_to_renamed.insert(name.clone(), new.clone());
    renamed_to_orig.insert(new.clone(), name.clone());

    {
        let e = result.renamed.entry(name.clone()).or_default();

        let v = CssClassName::Local {
            name: Ident {
                span,
                value: new.clone(),
                raw: None,
            },
        };
        if !e.contains(&v) {
            e.push(v);
        }
    }

    *name = new;
}

fn process_local<C>(
    config: &mut C,
    result: &mut TransformResult,
    orig_to_renamed: &mut FxHashMap<JsWord, JsWord>,
    renamed_to_orig: &mut FxHashMap<JsWord, JsWord>,
    sel: &mut SubclassSelector,
) where
    C: TransformConfig,
{
    match sel {
        SubclassSelector::Id(sel) => {
            sel.text.raw = None;

            rename(
                sel.span,
                config,
                result,
                orig_to_renamed,
                renamed_to_orig,
                &mut sel.text.value,
            );
        }
        SubclassSelector::Class(sel) => {
            sel.text.raw = None;

            rename(
                sel.span,
                config,
                result,
                orig_to_renamed,
                renamed_to_orig,
                &mut sel.text.value,
            );
        }
        SubclassSelector::Attribute(_) => {}
        SubclassSelector::PseudoClass(_) => {}
        SubclassSelector::PseudoElement(_) => {}
    }
}

fn prepend_left_subclass_selectors(
    complex_selector_children: &mut [ComplexSelectorChildren],
    sels: &mut Vec<SubclassSelector>,
    mut sel_index: usize,
) {
    sels.remove(sel_index);

    for c in complex_selector_children
        .iter_mut()
        .filter_map(|c| c.as_mut_compound_selector())
    {
        c.subclass_selectors.splice(0..0, sels.drain(..sel_index));

        if !sels.is_empty() {
            c.subclass_selectors.extend(sels[..].iter().cloned());
        }

        sel_index = 0;
    }
}