diff --git a/.changeset/strong-falcons-grab.md b/.changeset/strong-falcons-grab.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/strong-falcons-grab.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/yak-swc/css_in_js_parser/src/parse_css.rs b/packages/yak-swc/css_in_js_parser/src/parse_css.rs index 374c7658..a135310f 100644 --- a/packages/yak-swc/css_in_js_parser/src/parse_css.rs +++ b/packages/yak-swc/css_in_js_parser/src/parse_css.rs @@ -504,7 +504,7 @@ mod tests { ); let all_declarations = declarations .into_iter() - .chain(declarations2.into_iter()) + .chain(declarations2) .collect::>(); assert_debug_snapshot!((state1, state2, all_declarations)); } diff --git a/packages/yak-swc/yak_swc/benches/extension_detection.rs b/packages/yak-swc/yak_swc/benches/extension_detection.rs index 4187670e..4b62e586 100644 --- a/packages/yak-swc/yak_swc/benches/extension_detection.rs +++ b/packages/yak-swc/yak_swc/benches/extension_detection.rs @@ -3,23 +3,23 @@ use regex::Regex; fn main() { // These are a quick and dirty replacement for tests just to be sure - assert_eq!(true, ends_with_impl("file.yak.ts")); - assert_eq!(false, ends_with_impl("file.yak")); - assert_eq!(false, ends_with_impl("file.yak.tsx.nope")); - - assert_eq!(true, regex_impl("file.yak.ts")); - assert_eq!(false, regex_impl("file.yak")); - assert_eq!(false, regex_impl("file.yak.tsx.nope")); - - assert_eq!(true, last_chars_impl("file.yak.ts")); - assert_eq!(false, last_chars_impl("file.yak")); - assert_eq!(false, last_chars_impl("file.yak.tsx.nope")); - assert_eq!(true, last_chars_impl("file.yak.tsx")); - - assert_eq!(true, last_chars_impl_macro("file.yak.ts")); - assert_eq!(false, last_chars_impl_macro("file.yak")); - assert_eq!(false, last_chars_impl_macro("file.yak.tsx.nope")); - assert_eq!(true, last_chars_impl_macro("file.yak.tsx")); + assert!(ends_with_impl("file.yak.ts")); + assert!(!ends_with_impl("file.yak")); + assert!(!ends_with_impl("file.yak.tsx.nope")); + + assert!(regex_impl("file.yak.ts")); + assert!(!regex_impl("file.yak")); + assert!(!regex_impl("file.yak.tsx.nope")); + + assert!(last_chars_impl("file.yak.ts")); + assert!(!last_chars_impl("file.yak")); + assert!(!last_chars_impl("file.yak.tsx.nope")); + assert!(last_chars_impl("file.yak.tsx")); + + assert!(last_chars_impl_macro("file.yak.ts")); + assert!(!last_chars_impl_macro("file.yak")); + assert!(!last_chars_impl_macro("file.yak.tsx.nope")); + assert!(last_chars_impl_macro("file.yak.tsx")); // and that's the actual benchmark divan::main(); diff --git a/packages/yak-swc/yak_swc/src/lib.rs b/packages/yak-swc/yak_swc/src/lib.rs index 5440d012..d62e352c 100644 --- a/packages/yak-swc/yak_swc/src/lib.rs +++ b/packages/yak-swc/yak_swc/src/lib.rs @@ -23,7 +23,7 @@ use utils::encode_module_import::{encode_module_import, ImportKind}; mod variable_visitor; use variable_visitor::{ScopedVariableReference, VariableVisitor}; mod yak_imports; -use yak_imports::YakImportVisitor; +use yak_imports::{visit_module_imports, YakImports}; mod yak_file_visitor; use yak_file_visitor::YakFileVisitor; mod math_evaluate; @@ -86,7 +86,7 @@ where /// Visitor to gather all imports from the current program /// Used to check if the current program is using next-yak /// to idenftify css-in-js expressions - yak_library_imports: YakImportVisitor, + yak_library_imports: Option, /// Variable Name to Unique CSS Identifier Mapping\ /// e.g. const Rotation = keyframes`...` -> Rotation\ /// e.g. const Button = styled.button`...` -> Button\ @@ -119,7 +119,7 @@ where current_condition: vec![], current_exported: false, variables: VariableVisitor::new(), - yak_library_imports: YakImportVisitor::new(), + yak_library_imports: None, naming_convention: NamingConvention::new(filename.clone(), dev_mode, prefix), variable_name_selector_mapping: FxHashMap::default(), expression_replacement: None, @@ -134,6 +134,13 @@ where self.current_css_state.is_some() } + fn yak_imports(&self) -> &YakImports { + self + .yak_library_imports + .as_ref() + .expect("Internal error: yak_library_imports is None - this should be impossible as imports are parsed in the initial program visit before any other processing") + } + /// 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) -> ScopedVariableReference { @@ -290,7 +297,7 @@ where // constant mixins e.g. // const highlight = css`color: red;` // const Button = styled.button`&:hover { ${highlight}; }` - if is_valid_tagged_tpl(&tagged_tpl, &self.yak_library_imports.yak_css_idents) { + if is_valid_tagged_tpl(&tagged_tpl, self.yak_imports().yak_css_idents()) { let (inline_runtime_exprs, inline_runtime_css_vars) = self.process_yak_literal(&mut tagged_tpl.clone(), css_state.clone()); runtime_expressions.extend(inline_runtime_exprs); @@ -299,10 +306,8 @@ where // 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, - ) { + else if is_valid_tagged_tpl(&tagged_tpl, self.yak_imports().yak_keyframes_idents()) + { // Create a unique name for the keyframe let keyframe_name = self .naming_convention @@ -376,7 +381,7 @@ ${{() => {var}}};\n", // Handle inline css literals // e.g. styled.button`${css`color: red;`};` else if let Expr::TaggedTpl(tpl) = &mut **expr { - if is_valid_tagged_tpl(tpl, &self.yak_library_imports.yak_css_idents) { + if is_valid_tagged_tpl(tpl, self.yak_imports().yak_css_idents()) { let (inline_runtime_exprs, inline_runtime_css_vars) = self.process_yak_literal(tpl, css_state.clone()); runtime_expressions.extend(inline_runtime_exprs); @@ -442,6 +447,8 @@ ${{() => {var}}};\n", *expr.clone(), self .yak_library_imports + .as_mut() + .unwrap() .get_yak_utility_ident("unitPostFix".to_string()), unit.to_string(), ) @@ -481,12 +488,14 @@ where GenericComments: Comments, { fn visit_mut_program(&mut self, program: &mut Program) { - let mut yak_import_visitor = YakImportVisitor::new(); - program.visit_mut_children_with(&mut yak_import_visitor); - self.yak_library_imports = yak_import_visitor; + if let Program::Module(module) = program { + self.yak_library_imports = Some(visit_module_imports(module)); + } else { + return; + } // Skip this program only if yak is not used at all - if !self.yak_library_imports.is_using_next_yak() { + if !self.yak_imports().is_using_next_yak() { return; } @@ -517,6 +526,8 @@ where import_declaration.specifiers.extend( self .yak_library_imports + .as_ref() + .unwrap() .get_yak_utility_import_declaration(), ); break; @@ -568,7 +579,7 @@ where /// To store the current name which can be used for class names /// e.g. Button for const Button = styled.button`color: red;` fn visit_mut_var_decl(&mut self, n: &mut VarDecl) { - if !self.yak_library_imports.is_using_next_yak() { + if !self.yak_imports().is_using_next_yak() { return; } for decl in &mut n.decls { @@ -588,7 +599,7 @@ where /// 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() { + if !self.yak_imports().is_using_next_yak() { return; } if self.current_variable_name.is_none() { @@ -627,7 +638,7 @@ where // Visit JSX expressions for css prop support fn visit_mut_jsx_opening_element(&mut self, n: &mut JSXOpeningElement) { - if !self.yak_library_imports.is_using_next_yak() { + if !self.yak_imports().is_using_next_yak() { return; } let css_prop = n.has_css_prop(); @@ -640,6 +651,8 @@ where n, &self .yak_library_imports + .as_mut() + .unwrap() .get_yak_utility_ident("mergeCssProp".into()), ); } @@ -655,7 +668,7 @@ where 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) { + if is_valid_tagged_tpl(&tpl, self.yak_imports().yak_css_idents()) { let replacement_before = self.expression_replacement.clone(); let tpl = &mut tpl.clone(); tpl.span = n.span(); @@ -730,7 +743,11 @@ where /// Visit tagged template literals /// This is where the css-in-js expressions are fn visit_mut_tagged_tpl(&mut self, n: &mut TaggedTpl) { - let yak_library_function_name = self.yak_library_imports.get_yak_library_function_name(n); + let yak_library_function_name = self + .yak_library_imports + .as_mut() + .unwrap() + .get_yak_library_function_name(n); if yak_library_function_name.is_none() { n.visit_mut_children_with(self); return; @@ -841,6 +858,8 @@ where if let Expr::Ident(ident) = &**callee { if self .yak_library_imports + .as_mut() + .unwrap() .get_yak_library_name_for_ident(&ident.to_id()) == Some(atom!("atoms")) { diff --git a/packages/yak-swc/yak_swc/src/yak_file_visitor.rs b/packages/yak-swc/yak_swc/src/yak_file_visitor.rs index 97e1bf4e..49ead9ee 100644 --- a/packages/yak-swc/yak_swc/src/yak_file_visitor.rs +++ b/packages/yak-swc/yak_swc/src/yak_file_visitor.rs @@ -1,12 +1,13 @@ -use crate::yak_imports::YakImportVisitor; use swc_core::atoms::atom; use swc_core::common::Spanned; use swc_core::ecma::ast::*; use swc_core::ecma::visit::{Fold, VisitMut, VisitMutWith}; use swc_core::plugin::errors::HANDLER; +use crate::yak_imports::{visit_module_imports, YakImports}; + pub struct YakFileVisitor { - yak_imports: YakImportVisitor, + yak_imports: Option, is_inside_css_tpl: bool, } @@ -16,7 +17,7 @@ pub struct YakFileVisitor { impl YakFileVisitor { pub fn new() -> Self { Self { - yak_imports: YakImportVisitor::new(), + yak_imports: None, is_inside_css_tpl: false, } } @@ -33,12 +34,19 @@ impl YakFileVisitor { true }); } + + fn yak_imports(&self) -> &YakImports { + self + .yak_imports + .as_ref() + .expect("Internal error: yak_library_imports is None - this should be impossible as imports are parsed in the initial program visit before any other processing") + } } impl VisitMut for YakFileVisitor { fn visit_mut_module(&mut self, module: &mut Module) { - module.visit_mut_children_with(&mut self.yak_imports); - if self.yak_imports.is_using_next_yak() { + self.yak_imports = Some(visit_module_imports(module)); + if self.yak_imports().is_using_next_yak() { self.remove_next_yak_imports(module); module.visit_mut_children_with(self); } @@ -51,7 +59,12 @@ impl VisitMut for YakFileVisitor { // This is necessary as the mixin is also imported at runtime and a string would be // interpreted as a class name if let Expr::TaggedTpl(n) = expr { - if let Some(name) = self.yak_imports.get_yak_library_function_name(n) { + if let Some(name) = self + .yak_imports + .as_mut() + .unwrap() + .get_yak_library_function_name(n) + { if name == atom!("css") { *expr = ObjectLit { span: n.span, @@ -73,7 +86,12 @@ impl VisitMut for YakFileVisitor { } fn visit_mut_tagged_tpl(&mut self, n: &mut TaggedTpl) { - if let Some(name) = self.yak_imports.get_yak_library_function_name(n) { + if let Some(name) = self + .yak_imports + .as_mut() + .unwrap() + .get_yak_library_function_name(n) + { // Right now only css template literals are allowed if name != atom!("css") { HANDLER.with(|handler| { diff --git a/packages/yak-swc/yak_swc/src/yak_imports.rs b/packages/yak-swc/yak_swc/src/yak_imports.rs index 419fe11c..cab6f443 100644 --- a/packages/yak-swc/yak_swc/src/yak_imports.rs +++ b/packages/yak-swc/yak_swc/src/yak_imports.rs @@ -1,43 +1,86 @@ use rustc_hash::{FxHashMap, FxHashSet}; use swc_core::atoms::Atom; use swc_core::ecma::visit::Fold; +use swc_core::ecma::visit::VisitMutWith; use swc_core::{ common::DUMMY_SP, ecma::{ast::*, visit::VisitMut}, }; #[derive(Debug)] -/// Visitor implementation to gather all names imported from "next-yak" -/// Side effect: converts the import source from "next-yak" to "next-yak/internal" -pub struct YakImportVisitor { - /// Imports from "next-yak" - /// Local to Imported mapping - yak_library_imports: FxHashMap, + +pub struct YakImports { /// Utilities used from "next-yak/internal" /// e.g. unitPostFix, mergeCssProp yak_utilities: FxHashMap, + /// Imports from "next-yak" + /// Local to Imported mapping + yak_library_imports: FxHashMap, /// Local Identifiers for the next-yak css function \ /// Most of the time it is just `css#0` for `import { css } from "next-yak"` \ /// but it might also contain renamings like `import { css as css_ } from "next-yak"` - pub yak_css_idents: FxHashSet, + yak_css_idents: FxHashSet, /// Local Identifiers for the next-yak keyframes function \ /// Most of the time it is just `keyframes#0` for `import { keyframes } from "next-yak"` \ /// but it might also contain renamings like `import { keyframes as keyframes_ } from "next-yak"` - pub yak_keyframes_idents: FxHashSet, + yak_keyframes_idents: FxHashSet, +} + +/// Scans a JavaScript/TypeScript module for yak library usage and collects import information. +/// +/// This function analyzes the entire module AST to: +/// - Detect imports from the "next-yak" library +/// - Track CSS-in-JS template literal identifiers +/// - Monitor renamed imports and utility functions +/// - Convert "next-yak" imports to "next-yak/internal" +/// +/// # Returns +/// +/// Returns a `YakImports` struct containing: +/// - Mapped imports from next-yak +/// - CSS function identifiers +/// - Keyframe function identifiers +/// - Utility function references +pub fn visit_module_imports(module: &mut Module) -> YakImports { + let mut yak_import_visitor = YakImportVisitor::new(); + module.visit_mut_children_with(&mut yak_import_visitor); + yak_import_visitor.into() } const UTILITIES: &[&str] = &["unitPostFix", "mergeCssProp"]; -impl YakImportVisitor { - pub fn new() -> Self { +impl From for YakImports { + fn from(value: YakImportVisitor) -> Self { + YakImports::new( + value.yak_library_imports, + value.yak_css_idents, + value.yak_keyframes_idents, + ) + } +} + +impl YakImports { + fn new( + yak_library_imports: FxHashMap, + yak_css_idents: FxHashSet, + yak_keyframes_idents: FxHashSet, + ) -> Self { Self { - yak_library_imports: FxHashMap::default(), yak_utilities: FxHashMap::default(), - yak_css_idents: FxHashSet::default(), - yak_keyframes_idents: FxHashSet::default(), + yak_library_imports, + yak_css_idents, + yak_keyframes_idents, } } + pub fn yak_css_idents(&self) -> &FxHashSet { + &self.yak_css_idents + } + + pub fn yak_keyframes_idents(&self) -> &FxHashSet { + &self.yak_keyframes_idents + } + /// Check if the current AST has imports to the next-yak library pub fn is_using_next_yak(&self) -> bool { !self.yak_library_imports.is_empty() @@ -77,7 +120,7 @@ impl YakImportVisitor { if !self.is_using_next_yak() { return None; } - return self.yak_library_imports.get(id).map(|id| id.0.clone()); + self.yak_library_imports.get(id).map(|id| id.0.clone()) } /// Returns the utility function identifier @@ -112,6 +155,30 @@ impl YakImportVisitor { } } +struct YakImportVisitor { + /// Imports from "next-yak" + /// Local to Imported mapping + pub yak_library_imports: FxHashMap, + /// Local Identifiers for the next-yak css function \ + /// Most of the time it is just `css#0` for `import { css } from "next-yak"` \ + /// but it might also contain renamings like `import { css as css_ } from "next-yak"` + pub yak_css_idents: FxHashSet, + /// Local Identifiers for the next-yak keyframes function \ + /// Most of the time it is just `keyframes#0` for `import { keyframes } from "next-yak"` \ + /// but it might also contain renamings like `import { keyframes as keyframes_ } from "next-yak"` + pub yak_keyframes_idents: FxHashSet, +} + +impl YakImportVisitor { + pub fn new() -> Self { + Self { + yak_library_imports: FxHashMap::default(), + yak_css_idents: FxHashSet::default(), + yak_keyframes_idents: FxHashSet::default(), + } + } +} + impl VisitMut for YakImportVisitor { /// Visit the import declaration and store the imported names /// That way we know if `styled`, `css` is imported from "next-yak" @@ -123,6 +190,7 @@ impl VisitMut for YakImportVisitor { // and how the library is called internally import_decl.src.value = "next-yak/internal".into(); import_decl.src.raw = None; + // Store the local name of the imported function for specifier in &import_decl.specifiers { if let ImportSpecifier::Named(named) = specifier { @@ -171,7 +239,8 @@ mod tests { code, code, ); - assert_eq!(visitor.is_using_next_yak(), false); + let imports: YakImports = visitor.into(); + assert!(!imports.is_using_next_yak()); } #[test] @@ -200,7 +269,8 @@ mod tests { } "#, ); - assert_eq!(visitor.is_using_next_yak(), true); + let imports: YakImports = visitor.into(); + assert!(imports.is_using_next_yak()); } #[test] @@ -289,10 +359,11 @@ mod tests { #[test] fn test_yak_import_visitor_utility_ident() { - let mut visitor = YakImportVisitor::new(); - let ident = visitor.get_yak_utility_ident("unitPostFix".to_string()); + let visitor = YakImportVisitor::new(); + let mut imports: YakImports = visitor.into(); + let ident = imports.get_yak_utility_ident("unitPostFix".to_string()); assert_eq!(ident.sym, "__yak_unitPostFix"); - let ident = visitor.get_yak_utility_ident("mergeCssProp".to_string()); + let ident = imports.get_yak_utility_ident("mergeCssProp".to_string()); assert_eq!(ident.sym, "__yak_mergeCssProp"); } }