Skip to content

Commit

Permalink
introduce ScopedVariableReference
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon committed Oct 7, 2024
1 parent 0229891 commit 86773cf
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 62 deletions.
65 changes: 33 additions & 32 deletions packages/yak-swc/yak_swc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use serde::Deserialize;
use std::path::Path;
use std::vec;
use swc_core::atoms::atom;

use swc_core::atoms::Atom;
use swc_core::common::comments::Comment;
use swc_core::common::comments::Comments;
use swc_core::common::{Spanned, SyntaxContext, DUMMY_SP};
Expand All @@ -21,7 +23,7 @@ use utils::ast_helper::{extract_ident_and_parts, is_valid_tagged_tpl, TemplateIt
use utils::encode_module_import::{encode_module_import, ImportKind};

mod variable_visitor;
use variable_visitor::VariableVisitor;
use variable_visitor::{ScopedVariableReference, VariableVisitor};
mod yak_imports;
use yak_imports::YakImportVisitor;
mod yak_file_visitor;
Expand Down Expand Up @@ -68,7 +70,7 @@ where
/// All css declarations of the current root css expression
current_declaration: Vec<Declaration>,
/// e.g Button in const Button = styled.button`color: red;`
current_variable_name: Option<Id>,
current_variable_name: Option<ScopedVariableReference>,
/// Current condition to name nested css expressions
current_condition: Vec<String>,
/// Current css expression is exported
Expand All @@ -86,7 +88,7 @@ where
/// e.g. const Rotation = keyframes`...` -> Rotation\
/// e.g. const Button = styled.button`...` -> Button\
/// Used to replace expressions with the actual class name or keyframes name
variable_name_selector_mapping: FxHashMap<Id, String>,
variable_name_selector_mapping: FxHashMap<ScopedVariableReference, String>,
/// Naming convention to generate unique css identifiers
naming_convention: NamingConvention,
/// Expression replacement to replace a yak library call with the transformed one
Expand Down Expand Up @@ -129,11 +131,13 @@ where

/// Try to get the component id of the current styled component mixin or animation
/// e.g. const Button = styled.button`color: red;` -> Button#1
fn get_current_component_id(&self) -> Id {
self
.current_variable_name
.clone()
.unwrap_or_else(|| Id::from((atom!("yak"), SyntaxContext::empty())))
fn get_current_component_id(&self) -> ScopedVariableReference {
self.current_variable_name.clone().unwrap_or_else(|| {
ScopedVariableReference::new(
Id::from((atom!("yak"), SyntaxContext::empty())),
vec![atom!("yak")],
)
})
}

/// Get the current filename without extension or path e.g. "App" from "/path/to/App.tsx
Expand Down Expand Up @@ -208,8 +212,7 @@ where
// Handle constants in css expressions
// e.g. styled.button`color: ${primary};` (Ident)
// e.g. styled.button`color: ${colors.primary};` (MemberExpression)
if let Some((id, member_expr_parts)) = extract_ident_and_parts(expr) {
let scoped_name = id.to_id();
if let Some(scoped_name) = extract_ident_and_parts(expr) {
// Known StyledComponents, Mixin or Animations in the same file
if let Some(referenced_yak_css) = self.variable_name_selector_mapping.get(&scoped_name) {
let (new_state, new_declarations) = parse_css(referenced_yak_css, css_state);
Expand All @@ -221,7 +224,7 @@ where
// import { colors } from "./theme";
// styled.button`color: ${colors.primary};`
else if let Some((_import_source_type, module_path)) =
self.variables.get_imported_variable(&scoped_name)
self.variables.get_imported_variable(&scoped_name.id)
{
let import_kind: ImportKind = match find_char(
&quasis[pair.index..]
Expand All @@ -247,17 +250,14 @@ where
None => ImportKind::Mixin,
};
let cross_file_import_token =
encode_module_import(module_path.as_str(), member_expr_parts, import_kind);
encode_module_import(module_path.as_str(), scoped_name.parts, import_kind);

// TODO Track Dynamic Mixins as runtime dependency to pass props: `runtime_expressions.push(*expr.clone());`
let (new_state, _) = parse_css(&cross_file_import_token, css_state);
css_state = Some(new_state);
}
// Constants
else if let Some(value) = self
.variables
.get_const_value(&scoped_name, member_expr_parts)
{
else if let Some(value) = self.variables.get_const_value(&scoped_name) {
// e.g.:
// const primary = "red";
// styled.button`color: ${primary};`
Expand All @@ -281,10 +281,10 @@ where
HANDLER.with(|handler| {
handler
.struct_span_err(
id.span,
expr.span(),
&format!(
"Unsupported \"{}\" template literal in css expression - only css`` mixins are allowed",
scoped_name.0
scoped_name.to_readable_string()
),
)
.emit();
Expand All @@ -294,10 +294,10 @@ where
HANDLER.with(|handler| {
handler
.struct_span_err(
id.span,
expr.span(),
&format!(
"The value for constant \"{}\" is not a valid css value or css mixin",
scoped_name.0
scoped_name.to_readable_string()
),
)
.emit();
Expand All @@ -310,10 +310,10 @@ where
HANDLER.with(|handler| {
handler
.struct_span_err(
id.span,
expr.span(),
&format!(
"The value for variable \"{}\" could not be found in the top scope",
scoped_name.0
scoped_name.id.0
),
)
.emit();
Expand Down Expand Up @@ -368,9 +368,10 @@ where
// Current variable name of the StyledComponent or Mixin
// e.g. Button for const Button = styled.button`color: red;`
self
.current_variable_name
.clone()
.map_or(atom!("yak"), |name| name.0),
// TODO: check if parts should also be used for the name:
.get_current_component_id()
.id
.0,
// Current property name
// e.g. color for styled.button`color: red;`
css_state.as_ref().unwrap().current_declaration.property
Expand Down Expand Up @@ -511,7 +512,10 @@ where
for decl in &mut n.decls {
if let Pat::Ident(BindingIdent { id, .. }) = &decl.name {
let previous_variable_name = self.current_variable_name.clone();
self.current_variable_name = Some(id.to_id());
self.current_variable_name = Some(ScopedVariableReference::new(
id.to_id(),
vec![id.sym.clone()],
));
decl.init.visit_mut_with(self);
self.current_variable_name = previous_variable_name;
}
Expand All @@ -525,11 +529,8 @@ where
// const highlight = css`color: red;`
// const Button = styled.button`${({$active}) => $active && highlight};`
if self.is_inside_css_expression() {
if let Some((id, member_expr_parts)) = extract_ident_and_parts(n) {
if let Some(constant_value) = self
.variables
.get_const_value(&id.to_id(), member_expr_parts)
{
if let Some(scoped_name) = extract_ident_and_parts(n) {
if let Some(constant_value) = self.variables.get_const_value(&scoped_name) {
if let Expr::TaggedTpl(tpl) = *constant_value {
if is_valid_tagged_tpl(&tpl, self.yak_library_imports.yak_css_idents.clone()) {
let replacement_before = self.expression_replacement.clone();
Expand Down Expand Up @@ -644,7 +645,7 @@ where

let current_variable_id = self.get_current_component_id();
// Remove the scope postfix to make the variable name easier to read
let current_variable_name = current_variable_id.0.to_string();
let current_variable_name = current_variable_id.id.0.to_string();

// Current css parser state to parse an incomplete css code from a quasi
// In css-in-js the outer css scope is missing e.g.:
Expand Down
31 changes: 21 additions & 10 deletions packages/yak-swc/yak_swc/src/utils/ast_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use rustc_hash::{FxHashMap, FxHashSet};
use swc_core::atoms::Atom;
use swc_core::{common::DUMMY_SP, ecma::ast::*, plugin::errors::HANDLER};

use crate::variable_visitor::ScopedVariableReference;

/// Convert a HashMap to an Object expression
pub fn expr_hash_map_to_object(values: FxHashMap<String, Expr>) -> Expr {
let properties = values
Expand Down Expand Up @@ -88,17 +90,26 @@ pub fn create_member_prop_from_string(s: String) -> MemberProp {
/// There are two use cases:
/// 1. Member expressions (e.g., `colors.primary`) -> Some((colors#0, ["colors", "primary"]))
/// 2. Simple identifiers (e.g., `primaryColor`) -> Some((primaryColor#0, ["primaryColor"]))
pub fn extract_ident_and_parts(expr: &Expr) -> Option<(Ident, Vec<Atom>)> {
pub fn extract_ident_and_parts(expr: &Expr) -> Option<ScopedVariableReference> {
match &expr {
Expr::Member(member) => member_expr_to_strings(member).or_else(|| {
HANDLER.with(|handler| {
handler
.struct_span_err(member.span, "Could not parse member expression")
.emit();
});
None
}),
Expr::Ident(ident) => Some((ident.clone(), vec![ident.sym.clone()])),
Expr::Member(member) => member_expr_to_strings(member).map_or_else(
|| {
HANDLER.with(|handler| {
handler
.struct_span_err(member.span, "Could not parse member expression")
.emit();
});
None
},
|member_exp_strings| {
let (root_ident, props) = member_exp_strings;
Some(ScopedVariableReference::new(root_ident.to_id(), props))
},
),
Expr::Ident(ident) => Some(ScopedVariableReference::new(
ident.to_id(),
vec![ident.sym.clone()],
)),
_ => None,
}
}
Expand Down
52 changes: 39 additions & 13 deletions packages/yak-swc/yak_swc/src/variable_visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@ pub struct VariableVisitor {
imports: FxHashMap<Id, Atom>,
}

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
/// ScopedVariableReference stores the swc reference name to
/// - a variable e.g. foo -> (foo#3, [foo])
/// - a member expression e.g. foo.bar -> (foo#3, [foo, bar])
pub struct ScopedVariableReference {
/// The swc id of the variable
pub id: Id,
/// The parts of the variable reference
/// - e.g. foo.bar.baz -> [foo, bar, baz]
/// - e.g. foo -> [foo]
pub parts: Vec<Atom>,
}
impl ScopedVariableReference {
pub fn new(id: Id, parts: Vec<Atom>) -> Self {
Self { id, parts }
}
pub fn to_readable_string(&self) -> String {
self
.parts
.iter()
.map(|atom| atom.as_str())
.collect::<Vec<&str>>()
.join(".")
}
}

impl VariableVisitor {
pub fn new() -> Self {
Self {
Expand All @@ -32,13 +58,13 @@ impl VariableVisitor {

/// Try to get a constant value for a variable id
/// Supports normal constant values, object properties and array elements
/// e.g. get_const_value("primary#0", vec![atom!("primary"), atom!("red")])
pub fn get_const_value(&mut self, name: &Id, parts: Vec<Atom>) -> Option<Box<Expr>> {
if let Some(expr) = self.variables.get_mut(name) {
/// e.g. get_const_value(("primary#0", vec![atom!("primary"), atom!("red")]))
pub fn get_const_value(&mut self, scoped_name: &ScopedVariableReference) -> Option<Box<Expr>> {
if let Some(expr) = self.variables.get_mut(&scoped_name.id) {
// Start with the initial expression
let mut current_expr: &Expr = expr;
// Iterate over the parts (skipping the first one as it's the variable name)
for part in parts.iter().skip(1) {
for part in scoped_name.parts.iter().skip(1) {
match current_expr {
Expr::Object(obj) => {
// For object expressions, look for a property with matching key
Expand Down Expand Up @@ -188,10 +214,10 @@ mod tests {
);
let duration = get_expr_value(
&visitor
.get_const_value(
&Id::from((Atom::from("duration"), SyntaxContext::from_u32(0))),
.get_const_value(&ScopedVariableReference::new(
Id::from((Atom::from("duration"), SyntaxContext::from_u32(0))),
vec![],
)
))
.unwrap(),
);
assert_eq!(duration, Some("34".to_string()));
Expand Down Expand Up @@ -219,20 +245,20 @@ mod tests {
// Test accessing a nested property
let nested_value = get_expr_value(
&visitor
.get_const_value(
&Id::from((Atom::from("obj"), SyntaxContext::from_u32(0))),
.get_const_value(&ScopedVariableReference::new(
Id::from((Atom::from("obj"), SyntaxContext::from_u32(0))),
vec![atom!("obj"), atom!("prop1"), atom!("nestedProp")],
)
))
.unwrap(),
);

assert_eq!(nested_value, Some("fancy".to_string()));

// Test accessing an array element
let array_elem = &visitor.get_const_value(
&Id::from((Atom::from("obj"), SyntaxContext::from_u32(0))),
let array_elem = &visitor.get_const_value(&ScopedVariableReference::new(
Id::from((Atom::from("obj"), SyntaxContext::from_u32(0))),
vec![atom!("obj"), atom!("prop2"), atom!("1")],
);
));
let array_value = get_expr_value(array_elem.as_ref().unwrap());
assert_eq!(array_value, Some("2".to_string()));
}
Expand Down
21 changes: 14 additions & 7 deletions packages/yak-swc/yak_swc/src/yak_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ impl VisitMut for YakImportVisitor {
#[cfg(test)]
mod tests {
use super::*;
use swc_core::ecma::transforms::testing::test_transform;
use swc_core::ecma::visit::as_folder;
use swc_core::atoms::atom;
use swc_core::common::SyntaxContext;
use swc_core::ecma::transforms::testing::test_transform;
use swc_core::ecma::visit::as_folder;

#[test]
fn test_yak_import_visitor_no_yak() {
Expand Down Expand Up @@ -216,10 +216,11 @@ mod tests {
"#,
true,
);
assert!(visitor.yak_css_idents.contains(&Id::from((atom!("css"), SyntaxContext::empty()))));
assert!(visitor
.yak_css_idents
.contains(&Id::from((atom!("css"), SyntaxContext::empty()))));
}


#[test]
fn test_yak_import_visitor_renamed_css_ident() {
let mut visitor = YakImportVisitor::new();
Expand All @@ -236,7 +237,9 @@ mod tests {
"#,
true,
);
assert!(visitor.yak_css_idents.contains(&Id::from((atom!("myCss"), SyntaxContext::empty()))));
assert!(visitor
.yak_css_idents
.contains(&Id::from((atom!("myCss"), SyntaxContext::empty()))));
}

#[test]
Expand All @@ -255,7 +258,9 @@ mod tests {
"#,
true,
);
assert!(visitor.yak_keyframes_idents.contains(&Id::from((atom!("keyframes"), SyntaxContext::empty()))));
assert!(visitor
.yak_keyframes_idents
.contains(&Id::from((atom!("keyframes"), SyntaxContext::empty()))));
}

#[test]
Expand All @@ -274,7 +279,9 @@ mod tests {
"#,
true,
);
assert!(visitor.yak_keyframes_idents.contains(&Id::from((atom!("myKeyframes"), SyntaxContext::empty()))));
assert!(visitor
.yak_keyframes_idents
.contains(&Id::from((atom!("myKeyframes"), SyntaxContext::empty()))));
}

#[test]
Expand Down

0 comments on commit 86773cf

Please sign in to comment.