Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(styled-components): Implement minification #235

Merged
merged 25 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
494d891
implement minify
ciffelia Nov 21, 2023
4141a1a
enable disabled test fixtures for minify
ciffelia Nov 21, 2023
9131eec
replace .babelrc with config.json
ciffelia Nov 21, 2023
4f96bd4
update test fixtures output
ciffelia Nov 21, 2023
07b438c
Merge branch 'main' into feat-styled-components-minify
kdy1 Nov 22, 2023
ccee7bc
Bump npm package: ./packages/constify
kdy1 Nov 23, 2023
f5ad940
Bump npm package: ./packages/emotion
kdy1 Nov 23, 2023
8e65dca
Bump npm package: ./packages/jest
kdy1 Nov 23, 2023
3ad2e34
Bump npm package: ./packages/loadable-components
kdy1 Nov 23, 2023
2574045
Bump npm package: ./packages/noop
kdy1 Nov 23, 2023
0bbb0f8
Bump npm package: ./packages/react-remove-properties
kdy1 Nov 23, 2023
f69cf46
Bump npm package: ./packages/relay
kdy1 Nov 23, 2023
c2e7aeb
Bump npm package: ./packages/remove-console
kdy1 Nov 23, 2023
1a1fefe
Bump npm package: ./packages/styled-components
kdy1 Nov 23, 2023
8ed7e1c
Bump npm package: ./packages/styled-jsx
kdy1 Nov 23, 2023
2b81976
Bump npm package: ./packages/swc-magic
kdy1 Nov 23, 2023
bc0f2cd
Bump npm package: ./packages/transform-imports
kdy1 Nov 23, 2023
e5e694e
Bump cargo crate: react_remove_properties
kdy1 Nov 23, 2023
168a428
Bump cargo crate: remove_console
kdy1 Nov 23, 2023
7738f5b
Bump cargo crate: styled_components
kdy1 Nov 23, 2023
f053758
Bump cargo crate: styled_jsx
kdy1 Nov 23, 2023
7f18c2d
Bump cargo crate: swc_constify
kdy1 Nov 23, 2023
8770611
Bump cargo crate: swc_emotion
kdy1 Nov 23, 2023
227c297
Bump cargo crate: swc_magic
kdy1 Nov 23, 2023
e412f28
Bump cargo crate: swc_relay
kdy1 Nov 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/styled-components/transform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use swc_ecma_visit::{Fold, VisitMut};
pub use crate::{
utils::{analyze, analyzer, State},
visitors::{
display_name_and_id::display_name_and_id, transpile_css_prop::transpile::transpile_css_prop,
display_name_and_id::display_name_and_id, minify::visitor::minify,
transpile_css_prop::transpile::transpile_css_prop,
},
};

Expand Down Expand Up @@ -71,7 +72,8 @@ impl Config {

/// NOTE: **This is not complete**.
///
/// Only [analyzer] and [display_name_and_id] is implemented.
/// Only [transpile_css_prop], [minify] and [display_name_and_id] is
/// implemented.
pub fn styled_components(
file_name: FileName,
src_file_hash: u128,
Expand All @@ -86,6 +88,10 @@ pub fn styled_components(
enabled: config.css_prop,
visitor: transpile_css_prop(state.clone())
},
Optional {
enabled: config.minify,
visitor: minify(state.clone())
},
display_name_and_id(file_name, src_file_hash, config.clone(), state)
)
}
2 changes: 1 addition & 1 deletion packages/styled-components/transform/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ impl State {
self.imported_local_name = Some(id);
}

fn is_helper(&self, e: &Expr) -> bool {
pub(crate) fn is_helper(&self, e: &Expr) -> bool {
self.is_create_global_style_helper(e)
|| self.is_css_helper(e)
|| self.is_inject_global_helper(e)
Expand Down

This file was deleted.

289 changes: 289 additions & 0 deletions packages/styled-components/transform/src/visitors/minify/css.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
//! Port of https://github.com/styled-components/babel-plugin-styled-components/blob/4e2eb388d9c90f2921c306c760657d059d01a518/src/minify/index.js

use std::collections::HashSet;

use once_cell::sync::Lazy;
use regex::Regex;
use swc_atoms::Atom;

use super::{
css_placeholder::{make_placeholder, split_by_placeholders, PLACEHOLDER_REGEX},
regex_util::split_keep,
};

fn inject_unique_placeholders(str_arr: impl IntoIterator<Item = impl AsRef<str>>) -> String {
let mut result = String::new();

for (i, s) in str_arr.into_iter().enumerate() {
if i > 0 {
result.push_str(&make_placeholder(i - 1));
}
result.push_str(s.as_ref());
}

result
}

static LINEBREAK_REGEX_RAW: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?:\\r|\\n|\r|\n)\s*").unwrap());
static MULTILINE_COMMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?s)/\*[^!].*?\*/").unwrap());
static SYMBOL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s*[;:{},]\s*").unwrap());

/// Counts occurrences of a character inside string
fn count_occurrences(s: impl AsRef<str>, c: char) -> usize {
s.as_ref().split(c).count() - 1
}

/// Joins substrings until predicate returns true
fn reduce_substr(
substrs: impl IntoIterator<Item = impl AsRef<str>>,
join: impl AsRef<str>,
predicate: impl Fn(&str) -> bool,
) -> String {
let mut res = "".to_string();

for (i, substr) in substrs.into_iter().enumerate() {
if i == 0 {
res.push_str(substr.as_ref());
continue;
}
if predicate(&res) {
break;
}
res.push_str(join.as_ref());
res.push_str(substr.as_ref());
}

res
}

/// Joins at comment starts when it's inside a string or parentheses
/// effectively removing line comments
fn strip_line_comment(line: impl AsRef<str>) -> String {
reduce_substr(line.as_ref().split("//"), "//", |str| {
!str.ends_with(':') // NOTE: This is another guard against urls, if they're not inside strings or parantheses.
&& count_occurrences(str, '\'') % 2 == 0
&& count_occurrences(str, '"') % 2 == 0
&& count_occurrences(str, '(') == count_occurrences(str, ')')
})
}

fn compress_symbols(code: impl AsRef<str>) -> String {
split_keep(&SYMBOL_REGEX, code.as_ref())
.into_iter()
.enumerate()
.fold("".to_string(), |str, (index, fragment)| {
// Even-indices are non-symbol fragments
if index % 2 == 0 {
return str + fragment;
}

// Only manipulate symbols outside of strings
if count_occurrences(&str, '\'') % 2 != 0 || count_occurrences(&str, '"') % 2 != 0 {
return str + fragment;
}

// Preserve whitespace preceding colon, to avoid joining selectors.
if !fragment.starts_with(':') && fragment.trim_start().starts_with(':') {
return str + " " + fragment.trim();
}

str + fragment.trim()
})
}

/// Detects lines that are exclusively line comments
fn is_line_comment(s: impl AsRef<str>) -> bool {
s.as_ref().trim_start().starts_with("//")
}

/// Minifies a string of CSS code
fn minify(code: impl AsRef<str>, linebreak_regex: &Regex) -> String {
// Remove multiline comments
let code = MULTILINE_COMMENT_REGEX.replace_all(code.as_ref(), "\n");

let code = linebreak_regex
.split(&code) // Split at newlines
.filter(|line| !line.is_empty() && !is_line_comment(line)) // Removes lines containing only line comments
.map(strip_line_comment) // Remove line comments inside text
.collect::<Vec<_>>()
.join(" "); // Rejoin all lines

compress_symbols(code)
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MinifyResult {
pub values: Vec<Atom>,

/// Indices of expressions that are not eliminated (i.e. not in comments).
pub retained_expression_indices: HashSet<usize>,
}

/// Minifies template literal quasis
fn minify_values(
values: impl IntoIterator<Item = impl AsRef<str>>,
linebreak_regex: &Regex,
) -> MinifyResult {
let code = inject_unique_placeholders(values);
let minified_code = minify(code, linebreak_regex);

let minified_values = split_by_placeholders(&minified_code)
.into_iter()
.map(Atom::from)
.collect();

let retained_expression_indices: HashSet<usize> = PLACEHOLDER_REGEX
.captures_iter(&minified_code)
.map(|captures| captures[1].parse().unwrap())
.collect();

MinifyResult {
values: minified_values,
retained_expression_indices,
}
}

pub fn minify_raw_values(values: impl IntoIterator<Item = Atom>) -> MinifyResult {
minify_values(values, &LINEBREAK_REGEX_RAW)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_inject_unique_placeholders() {
assert_eq!(
inject_unique_placeholders(vec!["a", "b", "c"]),
"a__PLACEHOLDER_0__b__PLACEHOLDER_1__c"
);
}

#[test]
fn test_count_occurrences() {
assert_eq!(count_occurrences("abbbcc", 'a'), 1);
assert_eq!(count_occurrences("abbbcc", 'b'), 3);
assert_eq!(count_occurrences("abbbcc", 'c'), 2);
assert_eq!(count_occurrences("abbbcc", 'd'), 0);
}

#[test]
fn test_strip_line_comment() {
// splits a line by potential comment starts and joins until one is an actual
// comment
assert_eq!(strip_line_comment("abc def//ghi//jkl"), "abc def");

// ignores comment markers that are inside strings
assert_eq!(
strip_line_comment(r#"abc def"//"ghi'//'jkl//the end"#),
r#"abc def"//"ghi'//'jkl"#
);

// ignores comment markers that are inside parantheses
assert_eq!(
strip_line_comment(r#"bla (//) bla//the end"#),
r#"bla (//) bla"#
);

// ignores even unescaped URLs
assert_eq!(
strip_line_comment(r#"https://test.com// comment//"#),
r#"https://test.com"#
);
}

#[test]
fn test_compress_symbols() {
// removes spaces around symbols
// The whitespace preceding the colon is removed here as part of the
// trailing whitespace on the semi-colon. Contrast to the "preserves"
// test below.
assert_eq!(compress_symbols("; : { } , ; "), ";:{},;");

// ignores symbols inside strings
assert_eq!(compress_symbols(r#"; " : " ' : ' ;"#), r#";" : " ' : ';"#);

// preserves whitespace preceding colons
assert_eq!(
compress_symbols(r#"& :last-child { color: blue; }"#),
r#"& :last-child{color:blue;}"#
);
}

#[test]
fn test_minify() {
fn test(description: &str, code: &str, expected: &str) {
// test minify()
assert_eq!(
minify(code, &LINEBREAK_REGEX_RAW),
expected,
"{}: minify",
description
);

// test minify_values()
assert_eq!(
minify_values(vec![code], &LINEBREAK_REGEX_RAW),
MinifyResult {
values: vec![expected.into()],
retained_expression_indices: HashSet::new(),
},
"{}: minify_css_quasis",
description
);
}

test(
"Removes multi-line comments",
"this is a/* ignore me please */test",
"this is a test",
);

test(
"Joins all lines of code",
"this\nis\na/* ignore me \n please */\ntest",
"this is a test",
);

test(
"Removes line comments filling an entire line",
"line one\n// remove this comment\nline two",
"line one line two",
);

test(
"Removes line comments at the end of lines of code",
"valid line with // a comment\nout comments",
"valid line with out comments",
);

test(
"Preserves multi-line comments starting with /*!",
"this is a /*! dont ignore me please */ test/* but you can ignore me */",
"this is a /*! dont ignore me please */ test",
);

test(
"works with raw escape codes",
"this\\nis\\na/* ignore me \\n please */\\ntest",
"this is a test",
);
}

#[test]
fn test_minify_values() {
// Returns the indices of retained placeholders (expressions)
assert_eq!(
minify_values(
vec!["this is some\ninput with ", " and // ignored ", ""],
&LINEBREAK_REGEX_RAW
),
MinifyResult {
values: vec!["this is some input with ".into(), " and ".into()],
retained_expression_indices: vec![0].into_iter().collect(),
}
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Port of https://github.com/styled-components/babel-plugin-styled-components/blob/main/src/css/placeholderUtils.js

use once_cell::sync::Lazy;
use regex::Regex;

pub static PLACEHOLDER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"__PLACEHOLDER_(\d+)__").unwrap());

pub fn make_placeholder(index: usize) -> String {
format!("__PLACEHOLDER_{}__", index)
}

pub fn split_by_placeholders(input: &str) -> Vec<&str> {
PLACEHOLDER_REGEX.split(input).collect()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod css;
mod css_placeholder;
mod regex_util;
pub mod visitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use regex::Regex;

/// Split string by regex, keeping the delimiters.
pub fn split_keep<'a>(r: &Regex, text: &'a str) -> Vec<&'a str> {
let mut result = Vec::new();
let mut last = 0;
for m in r.find_iter(text) {
result.push(&text[last..m.start()]);
result.push(m.as_str());
last = m.start() + m.len();
}
result.push(&text[last..]);
result
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_split_keep() {
assert_eq!(
split_keep(&Regex::new("[ ,.]+").unwrap(), "this... is a, test"),
vec!["this", "... ", "is", " ", "a", ", ", "test"]
);

// Produces empty string when there are consecutive delimiters.
assert_eq!(
split_keep(&Regex::new("[.,]").unwrap(), ",.ab,."),
vec!["", ",", "", ".", "ab", ",", "", ".", ""]
);
}
}