diff --git a/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap b/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap index 69f5530c5f7e6..8dbd5b5c517b2 100644 --- a/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap +++ b/apps/oxfmt/test/cli/embedded_languages/__snapshots__/embedded_languages.test.ts.snap @@ -12,6 +12,26 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`; +// ============================================================================ +// Member Expression Tags - styled.div, styled.button, etc. +// ============================================================================ + +const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`; + +const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`; + +const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`; + +const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`; + +// ============================================================================ +// Plain template strings within css props or styled-jsx +// ============================================================================ + +const cssProp =
Hello World
; + +const styledJsx = ; + // ============================================================================ // GraphQL - Tagged template literals with gql and graphql tags // ============================================================================ @@ -48,6 +68,8 @@ npm install package const mixedStyles = css\`.button{color:red;}\`; +const mixedStyled = styled.button\`padding:10px;\`; + const mixedQuery = gql\`query{users{name}}\`; const mixedTemplate = html\`

Title

\`; @@ -157,6 +179,62 @@ const styledComponent = styled\` padding: 8px; \`; +// ============================================================================ +// Member Expression Tags - styled.div, styled.button, etc. +// ============================================================================ + +const cssGlobal = css.global\` + .reset { + margin: 0; + padding: 0; + box-sizing: border-box; + } +\`; + +const styledDiv = styled.div\` + width: 100%; + height: 100vh; + background-color: #f0f0f0; +\`; + +const styledLink = styled["a"]\` + text-decoration: none; + color: #007bff; + font-weight: bold; +\`; + +const styledButton = styled(Button)\` + font-size: 16px; + color: #333; + padding: 12px 24px; +\`; + +// ============================================================================ +// Plain template strings within css props or styled-jsx +// ============================================================================ + +const cssProp = ( +
+ Hello World +
+); + +const styledJsx = ( + +); + // ============================================================================ // GraphQL - Tagged template literals with gql and graphql tags // ============================================================================ @@ -225,6 +303,10 @@ const mixedStyles = css\` } \`; +const mixedStyled = styled.button\` + padding: 10px; +\`; + const mixedQuery = gql\` query { users { @@ -348,6 +430,26 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`; +// ============================================================================ +// Member Expression Tags - styled.div, styled.button, etc. +// ============================================================================ + +const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`; + +const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`; + +const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`; + +const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`; + +// ============================================================================ +// Plain template strings within css props or styled-jsx +// ============================================================================ + +const cssProp =
Hello World
; + +const styledJsx = ; + // ============================================================================ // GraphQL - Tagged template literals with gql and graphql tags // ============================================================================ @@ -384,6 +486,8 @@ npm install package const mixedStyles = css\`.button{color:red;}\`; +const mixedStyled = styled.button\`padding:10px;\`; + const mixedQuery = gql\`query{users{name}}\`; const mixedTemplate = html\`

Title

\`; @@ -493,6 +597,62 @@ const styledComponent = styled\` padding: 8px; \`; +// ============================================================================ +// Member Expression Tags - styled.div, styled.button, etc. +// ============================================================================ + +const cssGlobal = css.global\` + .reset { + margin: 0; + padding: 0; + box-sizing: border-box; + } +\`; + +const styledDiv = styled.div\` + width: 100%; + height: 100vh; + background-color: #f0f0f0; +\`; + +const styledLink = styled["a"]\` + text-decoration: none; + color: #007bff; + font-weight: bold; +\`; + +const styledButton = styled(Button)\` + font-size: 16px; + color: #333; + padding: 12px 24px; +\`; + +// ============================================================================ +// Plain template strings within css props or styled-jsx +// ============================================================================ + +const cssProp = ( +
+ Hello World +
+); + +const styledJsx = ( + +); + // ============================================================================ // GraphQL - Tagged template literals with gql and graphql tags // ============================================================================ @@ -561,6 +721,10 @@ const mixedStyles = css\` } \`; +const mixedStyled = styled.button\` + padding: 10px; +\`; + const mixedQuery = gql\` query { users { @@ -684,6 +848,26 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`; +// ============================================================================ +// Member Expression Tags - styled.div, styled.button, etc. +// ============================================================================ + +const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`; + +const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`; + +const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`; + +const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`; + +// ============================================================================ +// Plain template strings within css props or styled-jsx +// ============================================================================ + +const cssProp =
Hello World
; + +const styledJsx = ; + // ============================================================================ // GraphQL - Tagged template literals with gql and graphql tags // ============================================================================ @@ -720,6 +904,8 @@ npm install package const mixedStyles = css\`.button{color:red;}\`; +const mixedStyled = styled.button\`padding:10px;\`; + const mixedQuery = gql\`query{users{name}}\`; const mixedTemplate = html\`

Title

\`; @@ -815,6 +1001,32 @@ const styles = css\`.button{color:red;background:blue;padding:10px 20px;}.contai const styledComponent = styled\`background-color:#ffffff;border-radius:4px;padding:8px;\`; +// ============================================================================ +// Member Expression Tags - styled.div, styled.button, etc. +// ============================================================================ + +const cssGlobal = css.global\`.reset{margin:0;padding:0;box-sizing:border-box;}\`; + +const styledDiv = styled.div\`width:100%;height:100vh;background-color:#f0f0f0;\`; + +const styledLink = styled["a"]\`text-decoration:none;color:#007bff;font-weight:bold;\`; + +const styledButton = styled(Button)\`font-size:16px;color:#333;padding:12px 24px;\`; + +// ============================================================================ +// Plain template strings within css props or styled-jsx +// ============================================================================ + +const cssProp = ( +
+ Hello World +
+); + +const styledJsx = ( + +); + // ============================================================================ // GraphQL - Tagged template literals with gql and graphql tags // ============================================================================ @@ -851,6 +1063,8 @@ npm install package const mixedStyles = css\`.button{color:red;}\`; +const mixedStyled = styled.button\`padding:10px;\`; + const mixedQuery = gql\`query{users{name}}\`; const mixedTemplate = html\`

Title

\`; diff --git a/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js b/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js index 1719c57f9ee06..22a6895552ace 100644 --- a/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js +++ b/apps/oxfmt/test/cli/embedded_languages/fixtures/embedded_languages.js @@ -6,6 +6,26 @@ const styles = css`.button{color:red;background:blue;padding:10px 20px;}.contain const styledComponent = styled`background-color:#ffffff;border-radius:4px;padding:8px;`; +// ============================================================================ +// Member Expression Tags - styled.div, styled.button, etc. +// ============================================================================ + +const cssGlobal = css.global`.reset{margin:0;padding:0;box-sizing:border-box;}`; + +const styledDiv = styled.div`width:100%;height:100vh;background-color:#f0f0f0;`; + +const styledLink = styled["a"]`text-decoration:none;color:#007bff;font-weight:bold;`; + +const styledButton = styled(Button)`font-size:16px;color:#333;padding:12px 24px;`; + +// ============================================================================ +// Plain template strings within css props or styled-jsx +// ============================================================================ + +const cssProp =
Hello World
; + +const styledJsx = ; + // ============================================================================ // GraphQL - Tagged template literals with gql and graphql tags // ============================================================================ @@ -42,6 +62,8 @@ npm install package const mixedStyles = css`.button{color:red;}`; +const mixedStyled = styled.button`padding:10px;`; + const mixedQuery = gql`query{users{name}}`; const mixedTemplate = html`

Title

`; diff --git a/crates/oxc_formatter/src/print/template.rs b/crates/oxc_formatter/src/print/template.rs index 027fe93f81562..0427ac38c992b 100644 --- a/crates/oxc_formatter/src/print/template.rs +++ b/crates/oxc_formatter/src/print/template.rs @@ -8,7 +8,7 @@ use oxc_span::{GetSpan, Span}; use crate::{ ExternalCallbacks, IndentWidth, - ast_nodes::{AstNode, AstNodeIterator}, + ast_nodes::{AstNode, AstNodeIterator, AstNodes}, format_args, formatter::{ Format, FormatElement, Formatter, TailwindContextEntry, VecBuffer, @@ -29,6 +29,9 @@ use super::FormatWrite; impl<'a> FormatWrite<'a> for AstNode<'a, TemplateLiteral<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) { + if try_format_css_template(self, f) { + return; + } let template = TemplateLike::TemplateLiteral(self); write!(f, template); } @@ -758,6 +761,46 @@ impl<'a> Format<'a> for EachTemplateTable<'a> { } } +fn get_tag_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> { + let expr = expr.get_inner_expression(); + match expr { + Expression::Identifier(ident) => Some(ident.name.as_str()), + Expression::StaticMemberExpression(member) => get_tag_name(&member.object), + Expression::ComputedMemberExpression(exp) => get_tag_name(&exp.object), + Expression::CallExpression(call) => get_tag_name(&call.callee), + _ => None, + } +} + +fn format_embedded_template<'a>( + f: &mut Formatter<'_, 'a>, + tag_name: &str, + template_content: &str, +) -> bool { + let Some(Ok(formatted)) = + f.context().external_callbacks().format_embedded(tag_name, template_content) + else { + return false; + }; + + // Format with proper template literal structure: + // - Opening backtick + // - Hard line break (newline after backtick) + // - Indented content (each line will be indented) + // - Hard line break (newline before closing backtick) + // - Closing backtick + let format_content = format_with(|f: &mut Formatter<'_, 'a>| { + let content = f.context().allocator().alloc_str(&formatted); + for line in content.split('\n') { + write!(f, [text(line), hard_line_break()]); + } + }); + + write!(f, ["`", block_indent(&format_content), "`"]); + + true +} + /// Try to format a tagged template with the embedded formatter if supported. /// Returns `Some(result)` if formatting was attempted, `None` if not applicable. fn try_format_embedded_template<'a>( @@ -769,11 +812,10 @@ fn try_format_embedded_template<'a>( return false; } - let Expression::Identifier(ident) = &tagged.tag else { + let Some(tag_name) = get_tag_name(&tagged.tag) else { return false; }; - let tag_name = ident.name.as_str(); // Check if the tag is supported by the embedded formatter if !ExternalCallbacks::is_supported_tag(tag_name) { return false; @@ -782,26 +824,59 @@ fn try_format_embedded_template<'a>( // Get the external callbacks from the context let template_content = quasi.quasis[0].value.raw.as_str(); - let Some(Ok(formatted)) = - f.context().external_callbacks().format_embedded(tag_name, template_content) - else { + format_embedded_template(f, tag_name, template_content) +} + +/// Check if the template literal is inside a `css` prop or ` +/// ``` +fn is_in_css_jsx<'a>(node: &AstNode<'a, TemplateLiteral<'a>>) -> bool { + let AstNodes::JSXExpressionContainer(container) = node.parent else { return false; }; - // Format with proper template literal structure: - // - Opening backtick - // - Hard line break (newline after backtick) - // - Indented content (each line will be indented) - // - Hard line break (newline before closing backtick) - // - Closing backtick - let format_content = format_with(|f: &mut Formatter<'_, 'a>| { - let content = f.context().allocator().alloc_str(&formatted); - for line in content.split('\n') { - write!(f, [text(line), hard_line_break()]); + match container.parent { + AstNodes::JSXAttribute(attribute) => { + if let JSXAttributeName::Identifier(ident) = &attribute.name + && ident.name == "css" + { + return true; + } } - }); + AstNodes::JSXElement(element) => { + if let JSXElementName::Identifier(ident) = &element.opening_element.name + && ident.name == "style" + && element.opening_element.attributes.iter().any(|attr| { + matches!(attr.as_attribute().and_then(|a| a.name.as_identifier()), Some(name) if name.name == "jsx") + }) + { + return true; + } + } + _ => {} + } + false +} - write!(f, ["`", block_indent(&format_content), "`"]); +/// Try to format a template literal inside css prop or styled-jsx with the embedded formatter. +/// Returns `true` if formatting was attempted, `false` if not applicable. +fn try_format_css_template<'a>( + template_literal: &AstNode<'a, TemplateLiteral<'a>>, + f: &mut Formatter<'_, 'a>, +) -> bool { + if !template_literal.is_no_substitution_template() { + return false; + } - true + if !is_in_css_jsx(template_literal) { + return false; + } + + let quasi = template_literal.quasis(); + let template_content = quasi[0].value.raw.as_str(); + + format_embedded_template(f, "css", template_content) }