Skip to content

[codex] Add cssNamespace to styled-components plugin#617

Draft
kdy1 wants to merge 1 commit into
mainfrom
kdy1/styled-components-css-namespace
Draft

[codex] Add cssNamespace to styled-components plugin#617
kdy1 wants to merge 1 commit into
mainfrom
kdy1/styled-components-css-namespace

Conversation

@kdy1
Copy link
Copy Markdown
Member

@kdy1 kdy1 commented May 11, 2026

Summary

Adds a cssNamespace option to @swc/plugin-styled-components so styled template CSS can be scoped under a parent selector, addressing #616.

Changes

  • Adds cssNamespace config parsing and selector normalization.
  • Adds a styled-components visitor that wraps tagged template CSS before minify/template literal transpilation.
  • Leaves object styles and styled-components helpers such as css, keyframes, and createGlobalStyle un-namespaced.
  • Adds fixtures, WASM snapshots, docs, and a changeset.

Validation

  • cargo test -p styled_components
  • cargo test -p styled_components --test fixture -- --include-ignored css_namespace
  • cargo test -p styled_components --test fixture -- --include-ignored --skip transformed_imports_with_jsx_member_expressions
  • pnpm -F @swc/plugin-styled-components run test
  • git diff --check

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: 3128a61

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@swc/plugin-styled-components Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@claude
Copy link
Copy Markdown

claude Bot commented May 11, 2026

Review: cssNamespace for styled-components

Nice, focused addition. Implementation reuses State::is_styled, runs before minify/template_literals, and leans on CSS nesting to do the work. Snapshot + fixture pair is clear. A few things worth considering before merge:

Correctness / edge cases

1. is_bare_class_name accepts strings that don't form valid CSS class names (packages/styled-components/transform/src/lib.rs:95-99).

Allowing any string of [a-zA-Z0-9_-] is too permissive:

  • cssNamespace: \"123\".123 & (invalid — class selectors can't start with a digit).
  • cssNamespace: \"-\" or \"--\" → also invalid identifiers.
  • Empty after trim is already filtered, but a leading hyphen/digit slips through.

Consider rejecting (return None / log) or at least ensuring the first char is alpha/underscore. Alternatively, document that this branch is for "a single valid CSS identifier" and recommend users pass an explicit selector (.123) when in doubt.

2. No validation of arbitrary selector values — easy footgun.

cssNamespace is concatenated raw into every styled template (css_namespace.rs:38-39). A stray } or unterminated /* in the config silently corrupts every styled component in the project, with the broken CSS only visible at runtime. Since this is developer-supplied config, it's not a security issue, but a defensive check for balanced {}/comments (or just rejecting {, }, ;) would surface mistakes at build time rather than at render time.

3. wrap_template on the last quasis can collide with a trailing unterminated CSS comment.

If a styled template ends with /* trailing */ and no newline, appending } is fine. If it ends inside an unterminated block comment (already invalid CSS, but stylis tolerates some shapes), the } becomes part of the comment. Niche, but worth a comment in the visitor noting the assumption that input is well-formed CSS.

4. cooked = None is consistent with minify/visitor.rs:42-46 — good. Worth a one-line comment here mirroring the explanation there, so future readers know it's intentional rather than an oversight.

Test coverage gaps

  • No fixture for dynamic interpolation (styled.div\\\color: ${color};\``). This is the dominant real-world case; worth a fixture even though the wrap should be transparent to interpolations.
  • No fixture for **nested css\\\\`insidestyled\`\`**, which is a common shared-styles pattern. The current snapshot test asserts css` is not wrapped, but not that nested usage stays correct.
  • No fixture for **styled.div.attrs(...)\\\\`/withConfigchains** —State::is_styled` handles these via recursion, but a regression fixture would lock that in.
  • No fixture for the invalid/edge config values discussed above (empty string is implicitly covered by the early return; whitespace-only, leading digit, etc. are not).

Minor

  • CssNamespace.state is only used inside visit_mut_expr for the is_styled check — fine, just noting that this visitor walks every Expr in the program, same as minify/template_literals. Performance parity is acceptable.
  • README example is good. Consider explicitly calling out that object styles are not namespaced (the fixture verifies this but the docs only mention css/keyframes/createGlobalStyle). Users with mixed object + template usage will be surprised otherwise.
  • Snapshot uses the JS output post-template_literals transpile, so the CSS that ships to stylis is the raw inside the array. Worth a quick manual check that .myapp &{...} survives styled-components' own preprocessing in a real app — the unit tests don't exercise that path.

Ordering note

css_namespace runs after transpile_css_prop, which means css={...} props are also namespaced. That's probably what users want, but it's an undocumented side-effect — worth either confirming intentionally or mentioning in the README.

Overall: solid PR, the wrapper-via-nesting approach is clean. The main asks are config validation and a couple of additional fixtures.

@norbertsuski
Copy link
Copy Markdown

hi @kdy1 - where is it expected to be merged? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants