diff --git a/apps/oxfmt/src/core/external_formatter.rs b/apps/oxfmt/src/core/external_formatter.rs index fe0bab4eb2bdc..b5480e4470e80 100644 --- a/apps/oxfmt/src/core/external_formatter.rs +++ b/apps/oxfmt/src/core/external_formatter.rs @@ -207,6 +207,8 @@ fn language_to_prettier_parser(language: &str) -> Option<&'static str> { "tagged-graphql" => Some("graphql"), "tagged-html" => Some("html"), "tagged-markdown" => Some("markdown"), + "angular-template" => Some("angular"), + "angular-styles" => Some("scss"), _ => None, } } 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 71cea3baec505..d43a0a2667fd1 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 @@ -144,7 +144,113 @@ var(--color), --------------------" `; -exports[`embedded_languages > css > should format (auto) 1`] = ` +exports[`embedded_languages > angular.ts > should format (auto) 1`] = ` +"--- FILE ----------- +angular.ts +--- BEFORE --------- +// Angular @Component decorator - direct template and styles +// Uses Angular-specific syntax: interpolation, directives, bindings +@Component({ + selector: 'app-root', + template: \` +

{{ title }}

+
+ {{ count }} +
+ + \`, + styles: \`h1 { color: blue }\` +}) +export class AppComponent1 {} + +// Array form styles +@Component({ + selector: 'app-test', + template: \` + \`, + styles: [ \` + + :host { + color: red; + } + div { background: blue + } +\` + +] +}) +class TestComponent {} + +// Computed properties - should NOT be formatted +const styles = "foobar"; +const template = "foobar"; + +@Component({ + selector: 'app-computed', + [template]: \`

{{ hello }}

\`, + [styles]: \`h1 { color: blue }\` +}) +export class AppComponent2 {} + +--- AFTER ---------- +// Angular @Component decorator - direct template and styles +// Uses Angular-specific syntax: interpolation, directives, bindings +@Component({ + selector: "app-root", + template: \` +

{{ title }}

+
+ {{ count }} +
+ + \`, + styles: \` + h1 { + color: blue; + } + \`, +}) +export class AppComponent1 {} + +// Array form styles +@Component({ + selector: "app-test", + template: \` + + \`, + styles: [ + \` + :host { + color: red; + } + div { + background: blue; + } + \`, + ], +}) +class TestComponent {} + +// Computed properties - should NOT be formatted +const styles = "foobar"; +const template = "foobar"; + +@Component({ + selector: "app-computed", + [template]: \`

{{ hello }}

\`, + [styles]: \`h1 { color: blue }\`, +}) +export class AppComponent2 {} + +--------------------" +`; + +exports[`embedded_languages > css.js > should format (auto) 1`] = ` "--- FILE ----------- css.js --- BEFORE --------- @@ -227,7 +333,7 @@ const styledJsx = ( --------------------" `; -exports[`embedded_languages > graphql > should format (auto) 1`] = ` +exports[`embedded_languages > graphql.js > should format (auto) 1`] = ` "--- FILE ----------- graphql.js --- BEFORE --------- @@ -259,7 +365,7 @@ const mutation = graphql\` --------------------" `; -exports[`embedded_languages > html > should format (auto) 1`] = ` +exports[`embedded_languages > html.js > should format (auto) 1`] = ` "--- FILE ----------- html.js --- BEFORE --------- @@ -284,7 +390,7 @@ const component = html\` --------------------" `; -exports[`embedded_languages > markdown > should format (auto) 1`] = ` +exports[`embedded_languages > markdown.js > should format (auto) 1`] = ` "--- FILE ----------- markdown.js --- BEFORE --------- @@ -421,5 +527,101 @@ const readme = markdown\`##Installation npm install package \\\`\\\`\\\`\`; +-------------------- + +--- FILE ----------- +angular.ts +--- BEFORE --------- +// Angular @Component decorator - direct template and styles +// Uses Angular-specific syntax: interpolation, directives, bindings +@Component({ + selector: 'app-root', + template: \` +

{{ title }}

+
+ {{ count }} +
+ + \`, + styles: \`h1 { color: blue }\` +}) +export class AppComponent1 {} + +// Array form styles +@Component({ + selector: 'app-test', + template: \` + \`, + styles: [ \` + + :host { + color: red; + } + div { background: blue + } +\` + +] +}) +class TestComponent {} + +// Computed properties - should NOT be formatted +const styles = "foobar"; +const template = "foobar"; + +@Component({ + selector: 'app-computed', + [template]: \`

{{ hello }}

\`, + [styles]: \`h1 { color: blue }\` +}) +export class AppComponent2 {} + +--- AFTER ---------- +// Angular @Component decorator - direct template and styles +// Uses Angular-specific syntax: interpolation, directives, bindings +@Component({ + selector: "app-root", + template: \` +

{{ title }}

+
+ {{ count }} +
+ + \`, + styles: \`h1 { color: blue }\`, +}) +export class AppComponent1 {} + +// Array form styles +@Component({ + selector: "app-test", + template: \` + \`, + styles: [ + \` + + :host { + color: red; + } + div { background: blue + } +\`, + ], +}) +class TestComponent {} + +// Computed properties - should NOT be formatted +const styles = "foobar"; +const template = "foobar"; + +@Component({ + selector: "app-computed", + [template]: \`

{{ hello }}

\`, + [styles]: \`h1 { color: blue }\`, +}) +export class AppComponent2 {} + --------------------" `; diff --git a/apps/oxfmt/test/cli/embedded_languages/embedded_languages.test.ts b/apps/oxfmt/test/cli/embedded_languages/embedded_languages.test.ts index 797f68a637072..dbe9dd9d2e630 100644 --- a/apps/oxfmt/test/cli/embedded_languages/embedded_languages.test.ts +++ b/apps/oxfmt/test/cli/embedded_languages/embedded_languages.test.ts @@ -3,22 +3,21 @@ import { join } from "node:path"; import { runWriteModeAndSnapshot } from "../utils"; const fixturesDir = join(import.meta.dirname, "fixtures"); -const languages = ["css", "graphql", "html", "markdown"] as const; +const languages = ["css.js", "graphql.js", "html.js", "markdown.js", "angular.ts"]; describe("embedded_languages", () => { describe.each(languages)("%s", (lang) => { it("should format (auto)", async () => { - const snapshot = await runWriteModeAndSnapshot(fixturesDir, [`${lang}.js`]); + const snapshot = await runWriteModeAndSnapshot(fixturesDir, [lang]); expect(snapshot).toMatchSnapshot(); }); }); it("should not format any language (off)", async () => { - const snapshot = await runWriteModeAndSnapshot( - fixturesDir, - languages.map((lang) => `${lang}.js`), - ["--config", "off_embedded.json"], - ); + const snapshot = await runWriteModeAndSnapshot(fixturesDir, languages, [ + "--config", + "off_embedded.json", + ]); expect(snapshot).toMatchSnapshot(); }); diff --git a/apps/oxfmt/test/cli/embedded_languages/fixtures/angular.ts b/apps/oxfmt/test/cli/embedded_languages/fixtures/angular.ts new file mode 100644 index 0000000000000..775918d411ae8 --- /dev/null +++ b/apps/oxfmt/test/cli/embedded_languages/fixtures/angular.ts @@ -0,0 +1,44 @@ +// Angular @Component decorator - direct template and styles +// Uses Angular-specific syntax: interpolation, directives, bindings +@Component({ + selector: 'app-root', + template: ` +

{{ title }}

+
+ {{ count }} +
+ + `, + styles: `h1 { color: blue }` +}) +export class AppComponent1 {} + +// Array form styles +@Component({ + selector: 'app-test', + template: ` + `, + styles: [ ` + + :host { + color: red; + } + div { background: blue + } +` + +] +}) +class TestComponent {} + +// Computed properties - should NOT be formatted +const styles = "foobar"; +const template = "foobar"; + +@Component({ + selector: 'app-computed', + [template]: `

{{ hello }}

`, + [styles]: `h1 { color: blue }` +}) +export class AppComponent2 {} diff --git a/crates/oxc_formatter/src/print/template.rs b/crates/oxc_formatter/src/print/template.rs index decb9806fd8f3..f7f3f55fd7475 100644 --- a/crates/oxc_formatter/src/print/template.rs +++ b/crates/oxc_formatter/src/print/template.rs @@ -29,6 +29,11 @@ use super::FormatWrite; impl<'a> FormatWrite<'a> for AstNode<'a, TemplateLiteral<'a>> { fn write(&self, f: &mut Formatter<'_, 'a>) { + // Angular `@Component({ template, styles })` + if try_format_angular_component(self, f) { + return; + } + // styled-jsx: or
if try_format_css_template(self, f) { return; } @@ -880,3 +885,78 @@ fn try_format_css_template<'a>( format_embedded_template(f, "styled-jsx", template_content) } + +/// Try to format a template literal inside Angular @Component's template/styles property. +/// Returns `true` if formatting was performed, `false` if not applicable. +fn try_format_angular_component<'a>( + template_literal: &AstNode<'a, TemplateLiteral<'a>>, + f: &mut Formatter<'_, 'a>, +) -> bool { + // TODO: Support expressions in the template + if !template_literal.is_no_substitution_template() { + return false; + } + + // Check if inside `@Component` decorator's `template/styles` property + let Some(language) = get_angular_component_language(template_literal) else { + return false; + }; + + let quasi = template_literal.quasis(); + let template_content = quasi[0].value.raw.as_str(); + + format_embedded_template(f, language, template_content) +} + +/// Check if this template literal is one of: +/// ```ts +/// @Component({ +/// template: `...`, +/// styles: `...`, +/// // or styles: [`...`] +/// }) +/// ``` +fn get_angular_component_language(node: &AstNode<'_, TemplateLiteral<'_>>) -> Option<&'static str> { + let prop = match node.parent { + AstNodes::ObjectProperty(prop) => prop, + AstNodes::ArrayExpression(arr) => { + let AstNodes::ObjectProperty(prop) = arr.parent else { + return None; + }; + prop + } + _ => return None, + }; + + // Skip computed properties + if prop.computed { + return None; + } + let PropertyKey::StaticIdentifier(key) = &prop.key else { + return None; + }; + + // Check parent chain: ObjectExpression -> CallExpression(Component) -> Decorator + let AstNodes::ObjectExpression(obj) = prop.parent else { + return None; + }; + let AstNodes::CallExpression(call) = obj.parent else { + return None; + }; + let Expression::Identifier(ident) = &call.callee else { + return None; + }; + if ident.name.as_str() != "Component" { + return None; + } + if !matches!(call.parent, AstNodes::Decorator(_)) { + return None; + } + + let language = match key.name.as_str() { + "template" => "angular-template", + "styles" => "angular-styles", + _ => return None, + }; + Some(language) +}