From dbcc82487383f258737fa3a7750e19b9217e1860 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Mon, 26 Aug 2024 16:47:42 +0200 Subject: [PATCH] show an error if semicolon after mixin is missing --- .../fixtures/constant/output/index.tsx | 2 +- .../__tests__/fixtures/nestedMixin/mixin.tsx | 13 +- .../output/helper/anotherMixin.tsx | 4 +- .../fixtures/nestedMixin/output/index.tsx | 8 +- .../nestedMixin/output/index.yak.module.css | 4 - .../fixtures/nestedMixin/output/mixin.tsx | 20 +-- .../fixtures/staticMixin/output/index.tsx | 4 +- .../fixtures/yak-file/output/index.tsx | 2 +- .../loaders/lib/resolveCrossFileSelectors.ts | 27 +++- .../yak-swc/css_in_js_parser/src/find_char.rs | 148 ++++++++++++++++++ packages/yak-swc/css_in_js_parser/src/lib.rs | 3 + packages/yak-swc/yak_swc/src/lib.rs | 62 ++++---- .../yak-swc/yak_swc/src/utils/ast_helper.rs | 9 +- .../yak_swc/src/utils/encode_module_import.rs | 57 ++++--- .../fixture/cross-file-constants/output.tsx | 8 +- .../cross-file-mixin-export/output.tsx | 2 +- .../cross-file-mixin-import/output.tsx | 18 +-- .../fixture/cross-file-selectors/output.tsx | 4 +- 18 files changed, 276 insertions(+), 119 deletions(-) create mode 100644 packages/yak-swc/css_in_js_parser/src/find_char.rs diff --git a/packages/cross-file-tests/__tests__/fixtures/constant/output/index.tsx b/packages/cross-file-tests/__tests__/fixtures/constant/output/index.tsx index abf7e5ba..ee45a2c5 100644 --- a/packages/cross-file-tests/__tests__/fixtures/constant/output/index.tsx +++ b/packages/cross-file-tests/__tests__/fixtures/constant/output/index.tsx @@ -4,6 +4,6 @@ import { siteMaxWidth } from "./constants"; export var Button = /*YAK Extracted CSS: .Button { color: red; - height: --yak-css-import: url("./constants:siteMaxWidth")px; + height: --yak-css-import: url("./constants:siteMaxWidth",mixin)px; } */ /*#__PURE__*/ styled.button(__styleYak.Button); \ No newline at end of file diff --git a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/mixin.tsx b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/mixin.tsx index 65e6acae..b1978ac4 100644 --- a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/mixin.tsx +++ b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/mixin.tsx @@ -1,16 +1,13 @@ import { css } from 'next-yak'; import { Icon } from './icon'; -const buttonTextMixin = css<{ $disabled: boolean }>` +const buttonTextMixin = css` color: black; - ${({ $disabled }) => $disabled && css`opacity: 0.5;`} `; -export const buttonMixin = css<{ $hasIcon: boolean; $disabled: boolean }>` +export const buttonMixin = css` ${buttonTextMixin}; - ${({ $hasIcon }) => $hasIcon && css` - ${Icon} { - ${buttonTextMixin}; - } - `} + ${Icon} { + ${buttonTextMixin}; + } `; \ No newline at end of file diff --git a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/helper/anotherMixin.tsx b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/helper/anotherMixin.tsx index 77d06c6c..3c338ee0 100644 --- a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/helper/anotherMixin.tsx +++ b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/helper/anotherMixin.tsx @@ -1,6 +1,6 @@ import { css } from "next-yak/internal"; import { buttonMixin } from '../mixin'; export var primaryButtonMixin = /*YAK EXPORTED MIXIN:primaryButtonMixin ---yak-css-import: url("../mixin:buttonMixin"); +--yak-css-import: url("../mixin:buttonMixin",mixin); color: green; -*/ /*#__PURE__*/ css(buttonMixin); \ No newline at end of file +*/ /*#__PURE__*/ css(); \ No newline at end of file diff --git a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.tsx b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.tsx index da4cc945..49303a48 100644 --- a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.tsx +++ b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.tsx @@ -4,11 +4,11 @@ import { buttonMixin } from './mixin'; import { primaryButtonMixin } from './helper/anotherMixin'; export var Button = /*YAK Extracted CSS: .Button { - --yak-css-import: url("./mixin:buttonMixin"); + --yak-css-import: url("./mixin:buttonMixin",mixin); } -*/ /*#__PURE__*/ styled.button(__styleYak.Button, buttonMixin); +*/ /*#__PURE__*/ styled.button(__styleYak.Button); export var PrimaryButton = /*YAK Extracted CSS: .PrimaryButton { - --yak-css-import: url("./helper/anotherMixin:primaryButtonMixin"); + --yak-css-import: url("./helper/anotherMixin:primaryButtonMixin",mixin); } -*/ /*#__PURE__*/ styled(Button)(__styleYak.PrimaryButton, primaryButtonMixin); \ No newline at end of file +*/ /*#__PURE__*/ styled(Button)(__styleYak.PrimaryButton); \ No newline at end of file diff --git a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.yak.module.css b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.yak.module.css index 787b79dd..0b2d0504 100644 --- a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.yak.module.css +++ b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/index.yak.module.css @@ -1,18 +1,14 @@ .Button { color: black; -opacity: 0.5; :global(.icon_Icon__vdEsc) { color: black; - opacity: 0.5; } } .PrimaryButton { color: black; -opacity: 0.5; :global(.icon_Icon__vdEsc) { color: black; - opacity: 0.5; } color: green; diff --git a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/mixin.tsx b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/mixin.tsx index 89c804a7..087f9ae0 100644 --- a/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/mixin.tsx +++ b/packages/cross-file-tests/__tests__/fixtures/nestedMixin/output/mixin.tsx @@ -1,23 +1,9 @@ import { css } from "next-yak/internal"; import { Icon } from './icon'; -var buttonTextMixin = /*#__PURE__*/ css(function(param) { - var $disabled = param.$disabled; - return $disabled && /*#__PURE__*/ css(__styleYak.buttonTextMixin__$disabled); -}); +var buttonTextMixin = /*#__PURE__*/ css(); export var buttonMixin = /*YAK EXPORTED MIXIN:buttonMixin color: black; -opacity: 0.5; ---yak-css-import: url("./icon:Icon") { +--yak-css-import: url("./icon:Icon",mixin) { color: black; - opacity: 0.5; } -*/ /*#__PURE__*/ css(function(param) { - var $disabled = param.$disabled; - return $disabled && /*#__PURE__*/ css(__styleYak.buttonMixin__$disabled); -}, function(param) { - var $hasIcon = param.$hasIcon; - return $hasIcon && /*#__PURE__*/ css(__styleYak.buttonMixin__$hasIcon, function(param) { - var $disabled = param.$disabled; - return $disabled && /*#__PURE__*/ css(__styleYak["buttonMixin__$hasIcon-and-$disabled"]); - }); -}); \ No newline at end of file +*/ /*#__PURE__*/ css(); \ No newline at end of file diff --git a/packages/cross-file-tests/__tests__/fixtures/staticMixin/output/index.tsx b/packages/cross-file-tests/__tests__/fixtures/staticMixin/output/index.tsx index 78399a84..a4bd44b9 100644 --- a/packages/cross-file-tests/__tests__/fixtures/staticMixin/output/index.tsx +++ b/packages/cross-file-tests/__tests__/fixtures/staticMixin/output/index.tsx @@ -5,8 +5,8 @@ var ListItem = /*YAK Extracted CSS: .ListItem { margin-bottom: 10px; &:hover { - --yak-css-import: url("./mixin:lastChildMixin"); + --yak-css-import: url("./mixin:lastChildMixin",mixin); } } -*/ /*#__PURE__*/ styled.li(__styleYak.ListItem, lastChildMixin); +*/ /*#__PURE__*/ styled.li(__styleYak.ListItem); export default ListItem; \ No newline at end of file diff --git a/packages/cross-file-tests/__tests__/fixtures/yak-file/output/index.tsx b/packages/cross-file-tests/__tests__/fixtures/yak-file/output/index.tsx index bf43764b..4cb0f071 100644 --- a/packages/cross-file-tests/__tests__/fixtures/yak-file/output/index.tsx +++ b/packages/cross-file-tests/__tests__/fixtures/yak-file/output/index.tsx @@ -4,6 +4,6 @@ import { siteMaxWidth } from "./constants.yak"; export var Button = /*YAK Extracted CSS: .Button { color: red; - height: --yak-css-import: url("./constants.yak:siteMaxWidth")px; + height: --yak-css-import: url("./constants.yak:siteMaxWidth",mixin)px; } */ /*#__PURE__*/ styled.button(__styleYak.Button); \ No newline at end of file diff --git a/packages/next-yak/loaders/lib/resolveCrossFileSelectors.ts b/packages/next-yak/loaders/lib/resolveCrossFileSelectors.ts index b1b4a21a..71ed4fa8 100644 --- a/packages/next-yak/loaders/lib/resolveCrossFileSelectors.ts +++ b/packages/next-yak/loaders/lib/resolveCrossFileSelectors.ts @@ -5,7 +5,7 @@ import babelPlugin from "@babel/plugin-syntax-typescript"; import type { Compilation, LoaderContext } from "webpack"; import { getCssModuleLocalIdent } from "next/dist/build/webpack/config/blocks/css/loaders/getCssModuleLocalIdent.js"; -const yakCssImportRegex = /--yak-css-import\:\s*url\("([^"]+)"\);?/g; +const yakCssImportRegex = /--yak-css-import\:\s*url\("([^"]+)",(mixin|selector)\);?/g; const compilationCache = new WeakMap< Compilation, @@ -38,7 +38,7 @@ export async function resolveCrossFileConstant( ): Promise { // Search for --yak-css-import: url("path/to/module") in the css const matches = [...css.matchAll(yakCssImportRegex)].map((match) => { - const [fullMatch, encodedArguments] = match; + const [fullMatch, encodedArguments, importKind] = match; const [moduleSpecifier, ...specifier] = encodedArguments .split(":") .map((entry) => decodeURIComponent(entry)); @@ -46,6 +46,7 @@ export async function resolveCrossFileConstant( encodedArguments, moduleSpecifier, specifier, + importKind, position: match.index!, size: fullMatch.length, }; @@ -84,9 +85,17 @@ export async function resolveCrossFileConstant( // Replace the imports with the resolved values let result = css; for (let i = matches.length - 1; i >= 0; i--) { - const { position, size } = matches[i]; + const { position, size, importKind, moduleSpecifier, specifier } = matches[i]; const resolved = resolvedValues[i]; + if (importKind === "selector") { + if (resolved.type === "mixin") { + throw new Error( + `Found mixin but expected a selector - did you forget a semicolon after \`${specifier.join(".")}\`?`, + ); + } + } + const replacement = resolved.type === "styled-component" ? `:global(.${getCssModuleLocalIdent( @@ -465,13 +474,14 @@ async function resolveModuleSpecifierRecursively( ); } depth++; - // mixins in .yak files ware wrapped inside an object with a __yak key - if (depth === specifier.length) { - current = current["__yak"]; - } else { + // mixins in .yak files are wrapped inside an object with a __yak key + if (depth !== specifier.length) { current = current[specifier[depth]]; } } while (current); + if (current && current["__yak"]) { + return { type: "mixin", value: current["__yak"] }; + } if (specifier[depth] === undefined) { throw new Error( `Error unpacking Record/Array - could not extract \`${specifier @@ -485,7 +495,7 @@ async function resolveModuleSpecifierRecursively( }\` from \`${specifier.slice(0, depth).join(".")}\``, ); } else if (exportValue.type === "mixin") { - return { type: "constant", value: exportValue.value }; + return { type: "mixin", value: exportValue.value }; } throw new Error( `Error unpacking Record/Array - unexpected exportValue "${ @@ -516,4 +526,5 @@ type ParsedExport = type ResolvedExport = | { type: "styled-component"; from: string; name: string } + | { type: "mixin"; value: string | number } | { type: "constant"; value: string | number }; diff --git a/packages/yak-swc/css_in_js_parser/src/find_char.rs b/packages/yak-swc/css_in_js_parser/src/find_char.rs new file mode 100644 index 00000000..5cca83c6 --- /dev/null +++ b/packages/yak-swc/css_in_js_parser/src/find_char.rs @@ -0,0 +1,148 @@ +/// Finds the next occurrence of any specified character in a CSS-like text, +/// taking into account string literals and comments. +/// +/// This function is useful for parsing CSS-like syntax where you need to find +/// specific delimiters or markers while ignoring those same characters when they +/// appear within string literals or comments. +/// +/// # Arguments +/// +/// * `text` - A string slice that contains the text to search. +/// * `search_chars` - A slice of characters to search for. +/// +/// # Returns +/// +/// Returns `Some((char, usize))` if a character from `search_chars` is found, +/// where `char` is the found character and `usize` is its position in the input text. +/// Returns `None` if no character from `search_chars` is found before the end of the text. +/// +pub fn find_char(text: &str, search_chars: &[char]) -> Option<(char, usize)> { + let mut is_inside_string: Option = None; + let mut current_comment_state = CommentStateType::None; + let mut back_slashes = 0; + + let chars: Vec = text.chars().collect(); + let mut char_position = 0; + + while char_position < chars.len() { + let previous_back_slashes = back_slashes; + let current_character = chars[char_position]; + + if current_character == '\\' { + back_slashes += 1; + } else { + back_slashes = 0; + } + + match current_comment_state { + CommentStateType::MultiLine => { + if current_character == '*' + && char_position + 1 < chars.len() + && chars[char_position + 1] == '/' + { + current_comment_state = CommentStateType::None; + char_position += 2; + continue; + } + } + CommentStateType::SingleLine => { + if current_character == '\n' { + current_comment_state = CommentStateType::None; + char_position += 1; + continue; + } + } + CommentStateType::None => { + if previous_back_slashes % 2 == 0 && (current_character == '"' || current_character == '\'') + { + if is_inside_string == Some(current_character) { + is_inside_string = None; + } else if is_inside_string.is_none() { + is_inside_string = Some(current_character); + } + } else if is_inside_string.is_none() { + if current_character == '/' && char_position + 1 < chars.len() { + if chars[char_position + 1] == '*' { + current_comment_state = CommentStateType::MultiLine; + char_position += 2; + continue; + } else if chars[char_position + 1] == '/' { + current_comment_state = CommentStateType::SingleLine; + char_position += 2; + continue; + } + } + + if search_chars.contains(¤t_character) { + return Some((current_character, char_position)); + } + } + } + } + + char_position += 1; + } + + None +} + +#[derive(PartialEq, Debug, Clone)] +enum CommentStateType { + None, + SingleLine, + MultiLine, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_search() { + let css = "color: red; background: blue;"; + let search_chars = &[';', '{', '}']; + assert_eq!(find_char(css, search_chars), Some((';', 10))); + } + + #[test] + fn test_ignore_string_content() { + let css = "content: ';'; color: red;"; + let search_chars = &[';']; + assert_eq!(find_char(css, search_chars), Some((';', 12))); + } + + #[test] + fn test_ignore_single_line_comment() { + let css = "// ; this is a comment\nbackground: blue;"; + let search_chars = &[';']; + assert_eq!(find_char(css, search_chars), Some((';', 39))); + } + + #[test] + fn test_ignore_multi_line_comment() { + let css = "color: /* ; this is a\n multi-line comment */ blue;"; + let search_chars = &[';']; + assert_eq!(find_char(css, search_chars), Some((';', 49))); + } + + #[test] + fn test_escaped_characters() { + let css = "content: '\\\";'; color: red;"; + let search_chars = &[';']; + assert_eq!(find_char(css, search_chars), Some((';', 14))); + } + + #[test] + fn test_no_match() { + let css = "color: red background: blue"; + let search_chars = &[';', '{', '}']; + assert_eq!(find_char(css, search_chars), None); + } + + #[test] + fn test_multiple_search_chars() { + let css = "a { color: red; }"; + let search_chars = &[';', '{', '}']; + assert_eq!(find_char(css, search_chars), Some(('{', 2))); + } +} diff --git a/packages/yak-swc/css_in_js_parser/src/lib.rs b/packages/yak-swc/css_in_js_parser/src/lib.rs index 12e1f6d8..ca017893 100644 --- a/packages/yak-swc/css_in_js_parser/src/lib.rs +++ b/packages/yak-swc/css_in_js_parser/src/lib.rs @@ -1,5 +1,8 @@ +mod find_char; mod parse_css; mod to_css; pub use parse_css::*; pub use to_css::*; + +pub use find_char::find_char; diff --git a/packages/yak-swc/yak_swc/src/lib.rs b/packages/yak-swc/yak_swc/src/lib.rs index 2b07e124..02ec95fd 100644 --- a/packages/yak-swc/yak_swc/src/lib.rs +++ b/packages/yak-swc/yak_swc/src/lib.rs @@ -1,4 +1,4 @@ -use css_in_js_parser::{parse_css, to_css, CommentStateType}; +use css_in_js_parser::{find_char, parse_css, to_css, CommentStateType}; use css_in_js_parser::{Declaration, ParserState}; use rustc_hash::FxHashMap; use serde::Deserialize; @@ -18,10 +18,10 @@ use swc_core::plugin::metadata::TransformPluginMetadataContextKind; use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}; use utils::add_suffix_to_expr::add_suffix_to_expr; use utils::ast_helper::{extract_ident_and_parts, is_valid_tagged_tpl, TemplateIterator}; -use utils::encode_module_import::{encode_module_import, is_mixin_expression}; +use utils::encode_module_import::{encode_module_import, ImportKind}; mod variable_visitor; -use variable_visitor::{ImportSourceType, VariableVisitor}; +use variable_visitor::VariableVisitor; mod yak_imports; use yak_imports::YakImportVisitor; mod yak_file_visitor; @@ -163,6 +163,8 @@ where // e.g. styled.button`left: ${({$x}) => $x}px;` -> `left: var(--left);` let mut css_code_offset: usize = 0; + let quasis = n.tpl.quasis.clone(); + let mut template_iter = TemplateIterator::new(&mut n.tpl); // Javascript Quasi (TplElement) and Expressions (Exprs) are interleaved // e.g. styled.button`color: ${primary};` => [TplElement, Expr, TplElement] @@ -216,34 +218,40 @@ where } // Cross-file references // e.g.: - // import { primary } from "./theme"; + // import { colors } from "./theme"; // styled.button`color: ${colors.primary};` - else if let Some((import_source_type, module_path)) = + else if let Some((_import_source_type, module_path)) = self.variables.get_imported_variable(&scoped_name) { - let next_css_code = pair.next_quasi.map(|next_quasi| next_quasi.raw.to_string()); - // TODO: We can probably remove ImportSourceType::Normal and - // stop handling .yak imports differently from normal imports + let import_kind: ImportKind = match find_char( + &quasis[pair.index..] + .iter() + .map(|quasi| quasi.raw.as_str()) + .collect::(), + &[';', '{', '}', '@'], + ) { + Some((char, _)) => + // e.g. styled.button`${Icon} { ... }` + { + if char == '{' { + ImportKind::Selector + } + // e.g. styled.button`${colors.primary} @media { ... }` + // e.g. styled.button`.foo { ${colors.primary} }` + // e.g. styled.button`${colors.primary};` + else { + ImportKind::Mixin + } + } + // e.g. styled.button`${colors.primary}` + None => ImportKind::Mixin, + }; let cross_file_import_token = - encode_module_import(module_path.as_str(), member_expr_parts); - - // Cross file mixin imports - if import_source_type == ImportSourceType::Normal - && is_mixin_expression( - css_state.clone(), - cross_file_import_token.clone(), - next_css_code, - ) - { - let (new_state, _) = parse_css(&cross_file_import_token, css_state); - css_state = Some(new_state); - // TODO Track Dynamic Mixins as runtime dependency to pass props: `runtime_expressions.push(*expr.clone());` - } - // An imported constant or a mixin import from a .yak file - else { - let (new_state, _) = parse_css(&cross_file_import_token, css_state); - css_state = Some(new_state); - } + encode_module_import(module_path.as_str(), member_expr_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 diff --git a/packages/yak-swc/yak_swc/src/utils/ast_helper.rs b/packages/yak-swc/yak_swc/src/utils/ast_helper.rs index 513e920f..b48d1e26 100644 --- a/packages/yak-swc/yak_swc/src/utils/ast_helper.rs +++ b/packages/yak-swc/yak_swc/src/utils/ast_helper.rs @@ -116,7 +116,7 @@ pub fn is_valid_tagged_tpl(tagged_tpl: &TaggedTpl, literal_name: FxHashSet) pub struct TemplateIterator<'a> { tpl: &'a mut Tpl, - tpl_clone: Tpl, + quasis: Vec, current: usize, } @@ -125,12 +125,13 @@ pub struct TemplatePart<'a> { pub expr: Option<&'a mut Box>, pub next_quasi: Option<&'a TplElement>, pub is_last: bool, + pub index: usize, } impl<'a> TemplateIterator<'a> { pub fn new(tpl: &'a mut Tpl) -> Self { Self { - tpl_clone: tpl.clone(), + quasis: tpl.quasis.clone(), tpl, current: 0, } @@ -143,13 +144,14 @@ impl<'a> TemplateIterator<'a> { let quasi_count = self.tpl.quasis.len(); let is_last = self.current == quasi_count - 1; let next_quasi = if !is_last { - Some(&self.tpl_clone.quasis[self.current + 1]) + Some(&self.quasis[self.current + 1]) } else { None }; let quasi = &mut self.tpl.quasis[self.current]; let expr = self.tpl.exprs.get_mut(self.current); + let index = self.current; self.current += 1; Some(TemplatePart { @@ -157,6 +159,7 @@ impl<'a> TemplateIterator<'a> { expr, next_quasi, is_last, + index, }) } } diff --git a/packages/yak-swc/yak_swc/src/utils/encode_module_import.rs b/packages/yak-swc/yak_swc/src/utils/encode_module_import.rs index 80ff5ac3..e8a9f0c9 100644 --- a/packages/yak-swc/yak_swc/src/utils/encode_module_import.rs +++ b/packages/yak-swc/yak_swc/src/utils/encode_module_import.rs @@ -1,7 +1,12 @@ -use css_in_js_parser::{parse_css, ParserState}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use swc_core::atoms::Atom; +#[derive(PartialEq)] +pub enum ImportKind { + Selector, + Mixin, +} + /// This function generates a special CSS selector that represents an import from another module, /// encoding the module path and imported properties to ensure CSS parser compatibility /// @@ -25,35 +30,28 @@ use swc_core::atoms::Atom; /// - The function uses URL encoding for the import chain to handle special characters /// - The resulting string is meant to be processed and resolved by the yak css loader /// - The kind gives a hint how the import can be used - to provide an error message if the import is not supported -pub fn encode_module_import(module_path: &str, import_chain: Vec) -> String { +pub fn encode_module_import( + module_path: &str, + import_chain: Vec, + kind: ImportKind, +) -> String { let encoded_chain = import_chain .into_iter() .map(|part| utf8_percent_encode(&part, NON_ALPHANUMERIC).to_string()) .collect::>() .join(":"); format!( - "--yak-css-import: url(\"{}:{}\")", - module_path, encoded_chain + "--yak-css-import: url(\"{}:{}\",{})", + module_path, + encoded_chain, + if kind == ImportKind::Selector { + "selector" + } else { + "mixin" + } ) } -pub fn is_mixin_expression( - css_state: Option, - css_code: String, - next_css_code: Option, -) -> bool { - let next_css = next_css_code.unwrap_or("".to_string()); - let next_css = if next_css.is_empty() { - ";".to_string() - } else { - next_css - }; - let (_, next_declaration) = parse_css(format!("{css_code}{next_css}").as_str(), css_state); - // If the next declaration is a import declaration, it is a mixin expression as the css parser expected a - // css property value pair - !next_declaration.is_empty() && next_declaration.first().unwrap().property == "--yak-css-import" -} - #[cfg(test)] mod tests { use super::*; @@ -67,19 +65,24 @@ mod tests { Atom::from("")], + ImportKind::Selector, ); assert_eq!( selector, - "--yak-css-import: url(\"./styles/media:breakpoints:%3C%3A%22%3E\")" + "--yak-css-import: url(\"./styles/media:breakpoints:%3C%3A%22%3E\",selector)" ); } } diff --git a/packages/yak-swc/yak_swc/tests/fixture/cross-file-constants/output.tsx b/packages/yak-swc/yak_swc/tests/fixture/cross-file-constants/output.tsx index 0d671fe3..3e1cc8e5 100644 --- a/packages/yak-swc/yak_swc/tests/fixture/cross-file-constants/output.tsx +++ b/packages/yak-swc/yak_swc/tests/fixture/cross-file-constants/output.tsx @@ -8,9 +8,9 @@ import { fonts } from "./fonts"; import { sizes } from "./sizes"; export const Button = /*YAK Extracted CSS: .Button { - font-size: --yak-css-import: url("./fonts:fonts:sm"); - color: --yak-css-import: url("./colors:colors:colors:dark:primary"); - background-color: --yak-css-import: url("./colors:colors:colors:light:full%20opacity"); - height: --yak-css-import: url("./sizes:sizes:0"); + font-size: --yak-css-import: url("./fonts:fonts:sm",mixin); + color: --yak-css-import: url("./colors:colors:colors:dark:primary",mixin); + background-color: --yak-css-import: url("./colors:colors:colors:light:full%20opacity",mixin); + height: --yak-css-import: url("./sizes:sizes:0",mixin); } */ /*#__PURE__*/ styled.button(__styleYak.Button); diff --git a/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-export/output.tsx b/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-export/output.tsx index c1c255c2..e9d98802 100644 --- a/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-export/output.tsx +++ b/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-export/output.tsx @@ -10,7 +10,7 @@ border-radius: 5px; cursor: pointer; font-size: 16px; color: black; ---yak-css-import: url("./typography:typogaphyMixin"); +--yak-css-import: url("./typography:typogaphyMixin",mixin); */ /*#__PURE__*/ css(); export const Button = /*YAK Extracted CSS: .Button { diff --git a/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-import/output.tsx b/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-import/output.tsx index c3aaa2da..d2f3be6a 100644 --- a/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-import/output.tsx +++ b/packages/yak-swc/yak_swc/tests/fixture/cross-file-mixin-import/output.tsx @@ -8,40 +8,40 @@ import { fancy } from "./fancy"; import { yakMixin } from "./constants.yak"; export const Button = /*YAK Extracted CSS: .Button { - --yak-css-import: url("./fonts:fonts:h1"); + --yak-css-import: url("./fonts:fonts:h1",mixin); } */ /*#__PURE__*/ styled.button(__styleYak.Button); export const Button2 = /*YAK Extracted CSS: .Button2 { - --yak-css-import: url("./fonts:fonts:h1"); + --yak-css-import: url("./fonts:fonts:h1",mixin); } */ /*#__PURE__*/ styled.button(__styleYak.Button2); export const Button3 = /*YAK Extracted CSS: .Button3 { - --yak-css-import: url("./fonts:fonts:h1"); + --yak-css-import: url("./fonts:fonts:h1",mixin); color: green; } */ /*#__PURE__*/ styled.button(__styleYak.Button3); export const Button4 = /*YAK Extracted CSS: .Button4 { - --yak-css-import: url("./fonts:fonts:h1") ---yak-css-import: url("./fonts:fonts:underline"); + --yak-css-import: url("./fonts:fonts:h1",mixin) +--yak-css-import: url("./fonts:fonts:underline",mixin); color: green; } */ /*#__PURE__*/ styled.button(__styleYak.Button4); export const Button5 = /*YAK Extracted CSS: .Button5 { - --yak-css-import: url("./fonts:fonts:h1"); - --yak-css-import: url("./fancy:fancy:fancy:mixins:specialEffect"); + --yak-css-import: url("./fonts:fonts:h1",mixin); + --yak-css-import: url("./fancy:fancy:fancy:mixins:specialEffect",mixin); color: green; } */ /*#__PURE__*/ styled.button(__styleYak.Button5); export const Button6 = /*YAK Extracted CSS: .Button6 { &:hover { - --yak-css-import: url("./constants.yak:yakMixin"); + --yak-css-import: url("./constants.yak:yakMixin",selector); } - --yak-css-import: url("./fancy:fancy:fancy:mixins:specialEffect") + --yak-css-import: url("./fancy:fancy:fancy:mixins:specialEffect",mixin) ; color: green; } diff --git a/packages/yak-swc/yak_swc/tests/fixture/cross-file-selectors/output.tsx b/packages/yak-swc/yak_swc/tests/fixture/cross-file-selectors/output.tsx index dd8e28f3..12a3a82f 100644 --- a/packages/yak-swc/yak_swc/tests/fixture/cross-file-selectors/output.tsx +++ b/packages/yak-swc/yak_swc/tests/fixture/cross-file-selectors/output.tsx @@ -7,10 +7,10 @@ export const Button = /*YAK Extracted CSS: .Button { font-size: 1rem; color: green; - --yak-css-import: url("./Icon:Icon") { + --yak-css-import: url("./Icon:Icon",mixin) { color: red; } - --yak-css-import: url("./Icon:Icon") --yak-css-import: url("./Icon:Icon") { + --yak-css-import: url("./Icon:Icon",selector) --yak-css-import: url("./Icon:Icon",selector) { color: blue; } }