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)
+}