Skip to content

feat: support formatting embedded CSS in JS#7973

Merged
siketyan merged 13 commits intobiomejs:nextfrom
siketyan:feat/css-in-js
Dec 8, 2025
Merged

feat: support formatting embedded CSS in JS#7973
siketyan merged 13 commits intobiomejs:nextfrom
siketyan:feat/css-in-js

Conversation

@siketyan
Copy link
Member

@siketyan siketyan commented Nov 3, 2025

Summary

#7802

  • Added CssSnippetRoot node to allow parsing CSS snippets without a rule block.
  • Added parse_embedded_nodes and format_embedded handlers for JavaScript.
  • Added support for styled and css template literals to be parsed and formatted as CSS.

Caveats:

  • Only styled and css tag for styled-components or Emotion is supported for now.
  • CSS snippets are always indented.
  • Template expressions with any interpolations (${foo}) are not supported yet.
  • This feature is gated by the javascript.experimentalEmbeddedSnippetsEnabled option.

Test Plan

Added a snapshot test to handle simple cases.

Docs

biomejs/website#3622

@siketyan siketyan self-assigned this Nov 3, 2025
@changeset-bot
Copy link

changeset-bot bot commented Nov 3, 2025

🦋 Changeset detected

Latest commit: a10ddee

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

This PR includes changesets to release 14 packages
Name Type
@biomejs/biome Minor
@biomejs/cli-win32-x64 Minor
@biomejs/cli-win32-arm64 Minor
@biomejs/cli-darwin-x64 Minor
@biomejs/cli-darwin-arm64 Minor
@biomejs/cli-linux-x64 Minor
@biomejs/cli-linux-arm64 Minor
@biomejs/cli-linux-x64-musl Minor
@biomejs/cli-linux-arm64-musl Minor
@biomejs/wasm-web Minor
@biomejs/wasm-bundler Minor
@biomejs/wasm-nodejs Minor
@biomejs/backend-jsonrpc Patch
@biomejs/js-api Major

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

@github-actions github-actions bot added A-Project Area: project A-Linter Area: linter A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools L-JavaScript Language: JavaScript and super languages L-CSS Language: CSS L-Grit Language: GritQL A-Type-Inference Area: type inference labels Nov 3, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Nov 3, 2025

CodSpeed Performance Report

Merging #7973 will not alter performance

Comparing siketyan:feat/css-in-js (6b04f6f) with next (4460388)1

Summary

✅ 58 untouched
⏩ 95 skipped2

Footnotes

  1. No successful run was found on next (50c3513) during the generation of this report, so 4460388 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

  2. 95 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@siketyan siketyan changed the base branch from main to next November 4, 2025 10:09
@siketyan siketyan marked this pull request as ready for review November 4, 2025 10:33
@siketyan siketyan requested review from a team November 4, 2025 10:33
@coderabbitai

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs (1)

123-133: Keep nested-rule validation anchored on }.

When end_kind is EOF (the inline-root case), this guard never sees the closing brace that actually terminates a nested rule, so we fall back to parsing it as a declaration and produce a bogus tree. Please keep matching against T!['}'] (you can also include self.end_kind if you still want the old behaviour) so inline CSS with nested selectors continues to parse correctly.

-                if p.last().is_some_and(|kind| kind == self.end_kind) {
+                if p.last().is_some_and(|kind| kind == T!['}']) {
crates/biome_css_parser/src/lib.rs (1)

164-166: Inline CSS panics in offset parses

parse_css_with_offset can now yield CssInlineRoot, but CssOffsetParse::tree still insists on CssRoot, so the unwrap will panic as soon as we feed it an inline snippet (which the CSS-in-JS path is about to do). Let’s return AnyCssRoot here so the new embedding survives without throwing a tantrum at runtime.

Apply this diff:

-    pub fn tree(&self) -> CssRoot {
-        CssRoot::unwrap_cast(self.root.inner().clone())
+    pub fn tree(&self) -> AnyCssRoot {
+        AnyCssRoot::unwrap_cast(self.root.inner().clone())
     }
🧹 Nitpick comments (3)
crates/biome_css_formatter/src/utils/block_like.rs (1)

49-68: Consider adding a comment explaining the formatting strategy.

The switch to hard_line_break() and block_indent() makes the formatting more rigid and predictable. A brief comment explaining this choice (especially in the context of embedded CSS support) would help future maintainers understand the rationale.

Example:

     // When the list is empty, we still print a hard line to put the
     // closing curly on the next line.
+    // Note: We use hard_line_break() and block_indent() for predictable
+    // formatting, especially important for embedded CSS contexts.
     if self.block.is_empty() || self.block.has_only_empty_declarations() {
crates/biome_css_syntax/src/selector_ext.rs (1)

50-50: Consider using CssFileSource::css() explicitly for consistency.

Whilst Default::default() may be equivalent, other call sites in this PR uniformly use CssFileSource::css(). Explicit is clearer here.

-        let parsed = parse_css(source, Default::default(), CssParserOptions::default());
+        let parsed = parse_css(source, CssFileSource::css(), CssParserOptions::default());
crates/biome_service/src/workspace/server.tests.rs (1)

422-484: Well-structured test for the embedded CSS feature!

The test covers the happy path nicely. Consider adding a test case that verifies the current limitation with template interpolations (${...}), as mentioned in the PR objectives—even if just to document expected behaviour.

@ematipico ematipico added this to the Biome v2.4 milestone Nov 6, 2025
Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great feature! I left some comments but overall the feature looks sound

@arendjr
Copy link
Contributor

arendjr commented Nov 19, 2025

What would it take to get this PR over the finishing line? It's such a promising feature!

siketyan and others added 2 commits December 6, 2025 16:57
# Conflicts:
#	crates/biome_css_syntax/src/file_source.rs
#	crates/biome_css_syntax/src/lib.rs
#	crates/biome_service/src/file_handlers/css.rs
#	crates/biome_service/src/file_handlers/javascript.rs
#	crates/biome_service/src/workspace/server.tests.rs
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
@siketyan
Copy link
Member Author

siketyan commented Dec 6, 2025

@arendjr Nothing! Sorry for my late response, I was busy recently. I'll move this forward.

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users will be happy! What's the plan for the experimental flag? Are we looking for ways to support interpolation?

Let's make sure to have a docs PR ready before merging, so we don't lose it

Awesome work 💯

@siketyan
Copy link
Member Author

siketyan commented Dec 8, 2025

What's the plan for the experimental flag? Are we looking for ways to support interpolation?

As introducing this feature is technically breaking change and some users may not want to format embedded languages, I would gate this behind the option for a while. Full support for HTML-like languages is also gated behind an option, so this feature will follow it. Lacking support for interpolations is also one of the reasons.

# Conflicts:
#	crates/biome_css_analyze/src/lib.rs
#	crates/biome_css_analyze/src/services/semantic.rs
#	crates/biome_css_semantic/src/semantic_model/builder.rs
#	crates/biome_css_semantic/src/semantic_model/model.rs
#	crates/biome_js_formatter/src/syntax_rewriter.rs
#	crates/biome_service/src/file_handlers/css.rs
#	crates/biome_service/src/file_handlers/svelte.rs
#	crates/biome_service/src/file_handlers/vue.rs
#	xtask/rules_check/src/lib.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Formatter Area: formatter A-Linter Area: linter A-Parser Area: parser A-Project Area: project A-Tooling Area: internal tools A-Type-Inference Area: type inference L-CSS Language: CSS L-Grit Language: GritQL L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📎 Expanding embedded language support by handling embedded content in JsTemplateExpression

3 participants