diff --git a/packages/yak-swc/yak_swc/src/lib.rs b/packages/yak-swc/yak_swc/src/lib.rs index 6504946a..e90ca34e 100644 --- a/packages/yak-swc/yak_swc/src/lib.rs +++ b/packages/yak-swc/yak_swc/src/lib.rs @@ -6,7 +6,6 @@ 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}; @@ -277,6 +276,24 @@ where self.process_yak_literal(&mut tagged_tpl.clone(), css_state.clone()); runtime_expressions.extend(inline_runtime_exprs); runtime_css_variables.extend(inline_runtime_css_vars); + } + // keyframes - of animations which have not been parsed yet + // const Button = styled.button`animation: ${highlight};` + // const highlight = keyframes`from { color: red; }` + else if is_valid_tagged_tpl( + &tagged_tpl, + self.yak_library_imports.yak_keyframes_idents.clone(), + ) { + // Create a unique name for the keyframe + let keyframe_name = self + .naming_convention + .generate_unique_name_for_variable(&scoped_name); + // Store the keyframe for the later keyframe declaration + self + .variable_name_selector_mapping + .insert(scoped_name.clone(), keyframe_name.clone()); + let (new_state, _) = parse_css(keyframe_name.as_str(), css_state); + css_state = Some(new_state); } else { HANDLER.with(|handler| { handler @@ -522,6 +539,38 @@ where } } + /// Visit object member value declarations + /// To store the current name which can be used for class names + /// e.g. Button for const obj = { Button: styled.button`color: red;` } + fn visit_mut_object_lit(&mut self, n: &mut ObjectLit) { + if !self.yak_library_imports.is_using_next_yak() { + return; + } + if self.current_variable_name.is_none() { + n.visit_mut_children_with(self); + return; + } + let current_variable_name = self.current_variable_name.clone().unwrap(); + for props_or_spread in &mut n.props { + if let PropOrSpread::Prop(prop) = props_or_spread { + if let Some(key_value) = prop.as_key_value() { + if let PropName::Ident(value) = &key_value.key { + let mut new_parts = current_variable_name.parts.clone(); + new_parts.push(value.sym.clone()); + self.current_variable_name = Some(ScopedVariableReference::new( + current_variable_name.id.clone(), + new_parts, + )); + prop.visit_mut_with(self); + self.current_variable_name = Some(current_variable_name.clone()); + continue; + } + } + } + props_or_spread.visit_mut_with(self); + } + } + // Visit ternary expressions // To store the current condition which can be used for class names of nested css expressions fn visit_mut_expr(&mut self, n: &mut Expr) { @@ -614,12 +663,17 @@ where } let is_top_level = !self.is_inside_css_expression(); + let current_variable_id = self.get_current_component_id(); let mut transform: Box = match yak_library_function_name.as_deref() { // Styled Components transform works only on top level Some("styled") if is_top_level => Box::new(TransformStyled::new()), // Keyframes transform works only on top level - Some("keyframes") if is_top_level => Box::new(TransformKeyframes::new()), + Some("keyframes") if is_top_level => Box::new(TransformKeyframes::new( + self + .variable_name_selector_mapping + .get(¤t_variable_id), + )), // CSS Mixin e.g. const highlight = css`color: red;` Some("css") if is_top_level => Box::new(TransformCssMixin::new(self.current_exported)), // CSS Inline mixin e.g. styled.button`${() => css`color: red;`}` @@ -643,7 +697,6 @@ 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.id.0.to_string(); diff --git a/packages/yak-swc/yak_swc/src/naming_convention.rs b/packages/yak-swc/yak_swc/src/naming_convention.rs index a3295e36..4449cf24 100644 --- a/packages/yak-swc/yak_swc/src/naming_convention.rs +++ b/packages/yak-swc/yak_swc/src/naming_convention.rs @@ -1,4 +1,4 @@ -use crate::utils::murmur_hash::murmurhash2_32_gc; +use crate::{utils::murmur_hash::murmurhash2_32_gc, variable_visitor::ScopedVariableReference}; use rustc_hash::FxHashMap; pub struct NamingConvention { @@ -52,6 +52,15 @@ impl NamingConvention { } } + // Generate a unique name for a variable reference + // e.g "foo.bar" -> "foo_bar-01" + pub fn generate_unique_name_for_variable( + &mut self, + variable: &ScopedVariableReference, + ) -> String { + self.generate_unique_name(&variable.to_readable_string()) + } + /// Generate a unique CSS variable name based on the file name and a base name pub fn get_css_variable_name(&mut self, base_name: &str, dev_mode: bool) -> String { let name: &str = if dev_mode { @@ -75,12 +84,12 @@ fn escape_css_identifier(input: &str) -> String { for c in chars { match c { 'a'..='z' | 'A'..='Z' | '-' | '_' | '$' | '\\' => result.push(c), - // Whitespace - ' ' | '\t' => { + // Whitespace and member expression separator + ' ' | '\t' | '.' => { result.push('_'); } // Remove control characters - '\0'..='\x1F' | '\x7F' | '.' => continue, + '\0'..='\x1F' | '\x7F' => continue, // Escape Unicode characters c if c > '\u{00FF}' => { result.push('\\'); @@ -118,6 +127,7 @@ mod tests { assert_eq!(escape_css_identifier("foo\\bar"), "foo\\bar"); assert_eq!(escape_css_identifier("foo💩bar"), "foo\\💩bar"); assert_eq!(escape_css_identifier("foo bar"), "foo_bar"); + assert_eq!(escape_css_identifier("foo.bar"), "foo_bar"); assert_eq!(escape_css_identifier("foo\tbar"), "foo_bar"); assert_eq!(escape_css_identifier("foo\nbar"), "foobar"); assert_eq!(escape_css_identifier("1foo"), "_1foo"); diff --git a/packages/yak-swc/yak_swc/src/variable_visitor.rs b/packages/yak-swc/yak_swc/src/variable_visitor.rs index 3e79da98..9fdadb2c 100644 --- a/packages/yak-swc/yak_swc/src/variable_visitor.rs +++ b/packages/yak-swc/yak_swc/src/variable_visitor.rs @@ -27,7 +27,7 @@ pub struct VariableVisitor { /// - 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 + /// The swc id of the variable pub id: Id, /// The parts of the variable reference /// - e.g. foo.bar.baz -> [foo, bar, baz] diff --git a/packages/yak-swc/yak_swc/src/yak_transforms.rs b/packages/yak-swc/yak_swc/src/yak_transforms.rs index 86542489..baf6c5bd 100644 --- a/packages/yak-swc/yak_swc/src/yak_transforms.rs +++ b/packages/yak-swc/yak_swc/src/yak_transforms.rs @@ -314,9 +314,9 @@ pub struct TransformKeyframes { } impl TransformKeyframes { - pub fn new() -> TransformKeyframes { + pub fn new(animation_name: Option<&String>) -> TransformKeyframes { TransformKeyframes { - animation_name: None, + animation_name: animation_name.cloned(), } } } @@ -328,8 +328,13 @@ impl YakTransform for TransformKeyframes { declaration_name: &str, _previous_parser_state: Option, ) -> ParserState { - let css_identifier = naming_convention.generate_unique_name(declaration_name); - self.animation_name = Some(css_identifier.clone()); + let css_identifier = if self.animation_name.is_none() { + let new_identifier = naming_convention.generate_unique_name(declaration_name); + self.animation_name = Some(new_identifier.clone()); + new_identifier + } else { + self.animation_name.clone().unwrap() + }; let mut parser_state = ParserState::new(); parser_state.current_scopes = vec![CssScope { name: format!("@keyframes {}", css_identifier), diff --git a/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-function/input.tsx b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-function/input.tsx new file mode 100644 index 00000000..7b219564 --- /dev/null +++ b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-function/input.tsx @@ -0,0 +1,30 @@ +import { styled, css, keyframes } from "next-yak"; + +export const FadeInText = styled.p<{ $reverse?: boolean }>` + ${({ $reverse }) => $reverse ? css` + animation: ${fadeOut} 1s ease-in; + ` : css` + animation: ${fadeIn} 1s ease-in; + `} + + font-size: 18px; + color: #333; +`; + +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const fadeOut = keyframes` + from { + opacity: 1; + } + to { + opacity: 0; + } +`; \ No newline at end of file diff --git a/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-function/output.tsx b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-function/output.tsx new file mode 100644 index 00000000..b2d54cbc --- /dev/null +++ b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-function/output.tsx @@ -0,0 +1,34 @@ +import { styled, css, keyframes } from "next-yak/internal"; +import __styleYak from "./input.yak.module.css!=!./input?./input.yak.module.css"; +export const FadeInText = /*YAK Extracted CSS: +.FadeInText__$reverse { + animation: fadeOut 1s ease-in; +} +.FadeInText__not_$reverse { + animation: fadeIn 1s ease-in; +} +.FadeInText { + font-size: 18px; + color: #333; +} +*/ /*#__PURE__*/ styled.p(__styleYak.FadeInText, ({ $reverse })=>$reverse ? /*#__PURE__*/ css(__styleYak.FadeInText__$reverse) : /*#__PURE__*/ css(__styleYak.FadeInText__not_$reverse)); +const fadeIn = /*YAK Extracted CSS: +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +*/ /*#__PURE__*/ keyframes(__styleYak.fadeIn); +const fadeOut = /*YAK Extracted CSS: +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +*/ /*#__PURE__*/ keyframes(__styleYak.fadeOut); diff --git a/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-object/input.tsx b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-object/input.tsx new file mode 100644 index 00000000..fa6f5e46 --- /dev/null +++ b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-object/input.tsx @@ -0,0 +1,34 @@ +import { styled, css, keyframes } from "next-yak"; + +export const FadeInText = styled.p<{ $reverse?: boolean }>` + ${({ $reverse }) => + $reverse + ? css` + animation: ${animations.fadeOut} 1s ease-in; + ` + : css` + animation: ${animations.fadeIn} 1s ease-in; + `} + + font-size: 18px; + color: #333; +`; + +const animations = { + fadeIn: keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`, + fadeOut: keyframes` + from { + opacity: 1; + } + to { + opacity: 0; + } +`, +}; diff --git a/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-object/output.tsx b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-object/output.tsx new file mode 100644 index 00000000..f67ddd6f --- /dev/null +++ b/packages/yak-swc/yak_swc/tests/fixture/keyframe-animations-object/output.tsx @@ -0,0 +1,36 @@ +import { styled, css, keyframes } from "next-yak/internal"; +import __styleYak from "./input.yak.module.css!=!./input?./input.yak.module.css"; +export const FadeInText = /*YAK Extracted CSS: +.FadeInText__$reverse { + animation: animations_fadeOut 1s ease-in; +} +.FadeInText__not_$reverse { + animation: animations_fadeIn 1s ease-in; +} +.FadeInText { + font-size: 18px; + color: #333; +} +*/ /*#__PURE__*/ styled.p(__styleYak.FadeInText, ({ $reverse })=>$reverse ? /*#__PURE__*/ css(__styleYak.FadeInText__$reverse) : /*#__PURE__*/ css(__styleYak.FadeInText__not_$reverse)); +const animations = { + fadeIn: /*YAK Extracted CSS: +@keyframes animations_fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +*/ /*#__PURE__*/ keyframes(__styleYak.animations_fadeIn), + fadeOut: /*YAK Extracted CSS: +@keyframes animations_fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +*/ /*#__PURE__*/ keyframes(__styleYak.animations_fadeOut) +};