Skip to content

Commit

Permalink
show an error if semicolon after mixin is missing
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon committed Aug 26, 2024
1 parent c5721dc commit dbcc824
Show file tree
Hide file tree
Showing 18 changed files with 276 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
@@ -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};
}
`;
Original file line number Diff line number Diff line change
@@ -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);
*/ /*#__PURE__*/ css();
Original file line number Diff line number Diff line change
Expand Up @@ -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);
*/ /*#__PURE__*/ styled(Button)(__styleYak.PrimaryButton);
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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"]);
});
});
*/ /*#__PURE__*/ css();
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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);
27 changes: 19 additions & 8 deletions packages/next-yak/loaders/lib/resolveCrossFileSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,14 +38,15 @@ export async function resolveCrossFileConstant(
): Promise<string> {
// 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));
return {
encodedArguments,
moduleSpecifier,
specifier,
importKind,
position: match.index!,
size: fullMatch.length,
};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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 "${
Expand Down Expand Up @@ -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 };
148 changes: 148 additions & 0 deletions packages/yak-swc/css_in_js_parser/src/find_char.rs
Original file line number Diff line number Diff line change
@@ -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<char> = None;
let mut current_comment_state = CommentStateType::None;
let mut back_slashes = 0;

let chars: Vec<char> = 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(&current_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)));
}
}
3 changes: 3 additions & 0 deletions packages/yak-swc/css_in_js_parser/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit dbcc824

Please sign in to comment.