Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/styled-components-css-namespace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@swc/plugin-styled-components": minor
---

Add `cssNamespace` to scope styled-components template CSS under a parent selector.
12 changes: 12 additions & 0 deletions packages/styled-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ Then update your `.swcrc` file like below:
}
```

#### Options

`cssNamespace` prefixes generated component CSS with a parent selector. It is independent from `namespace`, which only prefixes the generated `componentId`.

```json
{
"cssNamespace": "myapp"
}
```

With `cssNamespace: "myapp"`, a styled component template is emitted under `.myapp & { ... }`. You can also pass an explicit selector such as `.shell .app`, or a self-reference selector such as `&&` to increase specificity.

# @swc/plugin-styled-components

## 12.7.0
Expand Down
12 changes: 12 additions & 0 deletions packages/styled-components/README.tmpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@ Then update your `.swcrc` file like below:
}
```

#### Options

`cssNamespace` prefixes generated component CSS with a parent selector. It is independent from `namespace`, which only prefixes the generated `componentId`.

```json
{
"cssNamespace": "myapp"
}
```

With `cssNamespace: "myapp"`, a styled component template is emitted under `.myapp & { ... }`. You can also pass an explicit selector such as `.shell .app`, or a self-reference selector such as `&&` to increase specificity.

${CHANGELOG}
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,52 @@ var _StyledDiv = _styled("div").withConfig({
"
`;

exports[`Should load styled-components wasm plugin correctly > Should transform css-namespace correctly 1`] = `
"import styled, { css, createGlobalStyle } from "styled-components";
const Button = styled.button([
\`.myapp &{color:red;&:hover{color:blue;}}\`
]);
const Wrapped = styled(Link)([
\`.myapp &{color:green;}\`
]);
const ObjectStyle = styled.div({
color: "black"
});
const helper = css([
\`color:hotpink;\`
]);
const GlobalStyle = createGlobalStyle([
\`body{margin:0;}\`
]);
"
`;

exports[`Should load styled-components wasm plugin correctly > Should transform css-namespace-selector correctly 1`] = `
"import styled from "styled-components";
const Panel = styled.section([
\`.shell .app &{padding:8px;}\`
]);
"
`;

exports[`Should load styled-components wasm plugin correctly > Should transform css-namespace-self-reference correctly 1`] = `
"import styled from "styled-components";
const Panel = styled.section([
\`&&{padding:8px;}\`
]);
"
`;

exports[`Should load styled-components wasm plugin correctly > Should transform css-namespace-with-component-namespace correctly 1`] = `
"import styled from "styled-components";
const Button = styled.button.withConfig({
componentId: "test-namespace__sc-460d3d61-0"
})([
\`.myapp &{color:red;}\`
]);
"
`;

exports[`Should load styled-components wasm plugin correctly > Should transform does-not-desugar-styled-assignment correctly 1`] = `
"const domElements = [
"div"
Expand Down
39 changes: 36 additions & 3 deletions packages/styled-components/transform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use swc_ecma_ast::{fn_pass, Pass};
pub use crate::{
utils::{analyze, analyzer, State},
visitors::{
display_name_and_id::display_name_and_id, minify::visitor::minify,
pure_annotation::pure_annotation, template_literals::template_literals,
transpile_css_prop::transpile::transpile_css_prop,
css_namespace::css_namespace, display_name_and_id::display_name_and_id,
minify::visitor::minify, pure_annotation::pure_annotation,
template_literals::template_literals, transpile_css_prop::transpile::transpile_css_prop,
},
};

Expand All @@ -36,6 +36,9 @@ pub struct Config {
#[serde(default)]
pub namespace: String,

#[serde(default)]
pub css_namespace: Option<String>,

#[serde(default)]
pub top_level_import_paths: Vec<Wtf8Atom>,

Expand Down Expand Up @@ -67,6 +70,32 @@ impl Config {
}
format!("{}__", self.namespace)
}

pub(crate) fn css_namespace_selector(&self) -> Option<String> {
let css_namespace = self.css_namespace.as_deref()?.trim();

if css_namespace.is_empty() {
return None;
}

if css_namespace.starts_with('&') {
return Some(css_namespace.to_string());
}

let selector = if is_bare_class_name(css_namespace) {
format!(".{css_namespace}")
} else {
css_namespace.to_string()
};

Some(format!("{selector} &"))
}
}

fn is_bare_class_name(value: &str) -> bool {
value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
}

pub fn styled_components<'a, C>(
Expand All @@ -91,6 +120,10 @@ where
return;
}

if let Some(selector) = config.css_namespace_selector() {
program.mutate(css_namespace(&state, selector));
}

program.mutate((
Optional {
enabled: config.minify,
Expand Down
48 changes: 48 additions & 0 deletions packages/styled-components/transform/src/visitors/css_namespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Adds a parent selector wrapper to styled-components template literals.

use swc_ecma_ast::*;
use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};

use crate::utils::State;

pub fn css_namespace(state: &State, selector: String) -> impl '_ + Pass {
visit_mut_pass(CssNamespace { state, selector })
}

#[derive(Debug)]
struct CssNamespace<'a> {
state: &'a State,
selector: String,
}

impl VisitMut for CssNamespace<'_> {
noop_visit_mut_type!(fail);

fn visit_mut_expr(&mut self, expr: &mut Expr) {
expr.visit_mut_children_with(self);

let Expr::TaggedTpl(tagged) = expr else {
return;
};

if !self.state.is_styled(&tagged.tag) {
return;
}

wrap_template(&mut tagged.tpl, &self.selector);
}
}

fn wrap_template(tpl: &mut Tpl, selector: &str) {
if let Some(first) = tpl.quasis.first_mut() {
let raw = first.raw.to_string();
first.raw = format!("{selector} {{{raw}").into();
first.cooked = None;
}

if let Some(last) = tpl.quasis.last_mut() {
let raw = last.raw.to_string();
last.raw = format!("{raw}}}").into();
last.cooked = None;
}
}
1 change: 1 addition & 0 deletions packages/styled-components/transform/src/visitors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod css_namespace;
pub mod display_name_and_id;
pub mod minify;
pub mod pure_annotation;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styled from "styled-components";

const Panel = styled.section`
padding: 8px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cssNamespace": ".shell .app",
"ssr": false,
"displayName": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import styled from "styled-components";
const Panel = styled.section([
`.shell .app &{padding:8px;}`
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styled from "styled-components";

const Panel = styled.section`
padding: 8px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cssNamespace": "&&",
"ssr": false,
"displayName": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import styled from "styled-components";
const Panel = styled.section([
`&&{padding:8px;}`
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styled from "styled-components";

const Button = styled.button`
color: red;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cssNamespace": "myapp",
"namespace": "test-namespace",
"displayName": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import styled from "styled-components";
const Button = styled.button.withConfig({
componentId: "test-namespace__sc-460d3d61-0"
})([
`.myapp &{color:red;}`
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled, { css, createGlobalStyle } from "styled-components";

const Button = styled.button`
color: red;

&:hover {
color: blue;
}
`;

const Wrapped = styled(Link)`
color: green;
`;

const ObjectStyle = styled.div({
color: "black",
});

const helper = css`
color: hotpink;
`;

const GlobalStyle = createGlobalStyle`
body {
margin: 0;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cssNamespace": "myapp",
"ssr": false,
"displayName": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled, { css, createGlobalStyle } from "styled-components";
const Button = styled.button([
`.myapp &{color:red;&:hover{color:blue;}}`
]);
const Wrapped = styled(Link)([
`.myapp &{color:green;}`
]);
const ObjectStyle = styled.div({
color: "black"
});
const helper = css([
`color:hotpink;`
]);
const GlobalStyle = createGlobalStyle([
`body{margin:0;}`
]);
Loading