Skip to content

Conversation

@Boshen
Copy link
Member

@Boshen Boshen commented Oct 20, 2025

Summary

Adds support for formatting embedded languages (CSS, GraphQL, HTML, Markdown) in JavaScript/TypeScript template literals using Prettier via NAPI bindings.

Motivation

JavaScript/TypeScript developers often embed CSS, GraphQL queries, HTML, and Markdown in their code using tagged template literals. Without proper formatting support, these embedded languages remain unformatted, leading to inconsistent code style and reduced readability.

This PR enables oxfmt to format these embedded languages using Prettier, providing a seamless formatting experience similar to Prettier's own embedded language support.

Changes

Core Functionality

  • Embedded language detection: Recognizes css, styled, gql, graphql, html, md, and markdown template tags
  • Prettier integration: Uses @prettier/sync for synchronous formatting of embedded code
  • NAPI bridge: Rust-to-JavaScript interop using NAPI ThreadsafeFunction
  • Graceful fallback: Returns original code if formatting fails or tag is unsupported

Implementation Details

Rust side (crates/oxc_formatter/, apps/oxfmt/src/prettier_plugins/):

  • New EmbeddedFormatter trait with callback-based API
  • NAPI bindings using FnArgs pattern for clean multi-argument calls
  • Template formatter enhancement to detect and format tagged templates
  • Proper indentation handling: splits Prettier output into lines, uses hard_line_break() separators, wraps in indent() for context-aware indentation
  • Arena allocator pattern to avoid borrow checker conflicts

JavaScript side (apps/oxfmt/src-js/):

  • TypeScript bindings with clean signatures: formatEmbeddedCode(tagName: string, code: string)
  • Prettier configuration: 80 char width, 2 space indent, semicolons, double quotes
  • Tag-to-parser mapping for supported languages
  • Error handling with console logging and fallback

Testing:

  • E2E test suite using Vitest (8 test cases)
  • Test fixtures for each supported language
  • Snapshot testing with timing normalization for stability
  • All tests passing consistently

Examples

Before (unformatted):

const styles = css`.button { color: red; background: blue; }`;

After (formatted with proper indentation):

const styles = css`
  .button {
    color: red;
    background: blue;
  }
`;

Supported tags:

// CSS
const styles = css`...`;
const component = styled`...`;

// GraphQL
const query = gql`...`;
const schema = graphql`...`;

// HTML
const template = html`...`;

// Markdown
const docs = md`...`;
const readme = markdown`...`;

Testing

# Run oxfmt tests
just oxfmt

# Watch mode for development
just watch-oxfmt

All 8 E2E tests pass:

  • ✓ embedded_css
  • ✓ embedded_graphql
  • ✓ embedded_html
  • ✓ embedded_markdown
  • ✓ mixed_embedded
  • ✓ no_embedded
  • ✓ unsupported_tag
  • ✓ check_mode

Related

Related to #13427

Future Work

Potential enhancements:

  • Support for additional embedded languages (SQL, YAML, etc.)
  • Custom Prettier configuration via oxfmt config
  • Performance optimization for large embedded blocks
  • Integration with language servers for inline diagnostics

@graphite-app
Copy link
Contributor

graphite-app bot commented Oct 20, 2025

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@github-actions github-actions bot added A-cli Area - CLI A-formatter Area - Formatter C-enhancement Category - New feature or request labels Oct 20, 2025
@Boshen Boshen force-pushed the feat/oxfmt-embedded-languages branch from be4e7a4 to 6813cf3 Compare October 20, 2025 15:46
@codspeed-hq
Copy link

codspeed-hq bot commented Oct 20, 2025

CodSpeed Performance Report

Merging #14820 will not alter performance

Comparing feat/oxfmt-embedded-languages (39c90c0) with main (377e904)

Summary

✅ 37 untouched

@ShenQingchuan
Copy link

ShenQingchuan commented Oct 21, 2025

I have a small suggestion, please forgive my abruptness — perhaps I shouldn't bring this up in the discussion section of this Pull Request, but:

Could we consider not strictly binding the relationship between this string tag function and its corresponding language?
For example, in Vue Vine, vine`...` actually contains Vue template inside. Many other frameworks also use similar constructs like template`...` while containing HTML-like syntax.

@KazariEX
Copy link

Would the following mode be supported?

const code = /* ts */`...`;

@Boshen Boshen force-pushed the feat/oxfmt-embedded-languages branch from 75ee9b0 to ff82793 Compare October 21, 2025 10:11
@leaysgur leaysgur self-requested a review October 21, 2025 11:58
Copy link
Member

@leaysgur leaysgur left a comment

Choose a reason for hiding this comment

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

Although I couldn't identify the cause, it has been confirmed that performance degrades simply because it is the napi version (even without actually formatting).

Benchmark 1: node ./node_modules/oxfmt-current/bin/oxfmt -V
  Time (mean ± σ):      27.4 ms ±   0.8 ms    [User: 18.1 ms, System: 4.3 ms]
  Range (min … max):    26.0 ms …  28.7 ms    10 runs

Benchmark 2: node ./node_modules/oxfmt-napi/dist/cli.js -V
  Time (mean ± σ):      49.5 ms ±   0.5 ms    [User: 41.6 ms, System: 6.5 ms]
  Range (min … max):    48.7 ms …  50.3 ms    10 runs

Summary
  node ./node_modules/oxfmt-current/bin/oxfmt -V ran
    1.81 ± 0.06 times faster than node ./node_modules/oxfmt-napi/dist/cli.js -V

And for full benchmark:

=========================================
Benchmarking parser.ts (single large file)
=========================================
Benchmark 1: prettier
  Time (mean ± σ):     422.1 ms ±   4.2 ms    [User: 1070.0 ms, System: 62.0 ms]
  Range (min … max):   416.2 ms … 430.4 ms    10 runs

Benchmark 2: prettier+oxc-parser
  Time (mean ± σ):     344.9 ms ±   3.7 ms    [User: 759.8 ms, System: 52.0 ms]
  Range (min … max):   340.2 ms … 350.2 ms    10 runs

Benchmark 3: biome
  Time (mean ± σ):      79.8 ms ±   1.4 ms    [User: 60.8 ms, System: 9.5 ms]
  Range (min … max):    78.6 ms …  83.5 ms    10 runs

Benchmark 4: oxfmt
  Time (mean ± σ):      43.5 ms ±   0.5 ms    [User: 30.7 ms, System: 9.0 ms]
  Range (min … max):    42.9 ms …  44.4 ms    10 runs

Benchmark 5: oxfmt_napi
  Time (mean ± σ):     175.8 ms ±   0.6 ms    [User: 164.8 ms, System: 11.9 ms]
  Range (min … max):   174.7 ms … 176.7 ms    10 runs

Summary
  oxfmt ran
    1.84 ± 0.04 times faster than biome
    4.04 ± 0.05 times faster than oxfmt_napi
    7.93 ± 0.12 times faster than prettier+oxc-parser
    9.70 ± 0.15 times faster than prettier

=========================================
Benchmarking Outline repository
=========================================
Benchmark 1: prettier
  Time (mean ± σ):      4.229 s ±  0.169 s    [User: 21.219 s, System: 2.269 s]
  Range (min … max):    3.874 s …  4.447 s    10 runs

Benchmark 2: prettier+oxc-parser
  Time (mean ± σ):      3.340 s ±  0.044 s    [User: 12.569 s, System: 1.464 s]
  Range (min … max):    3.285 s …  3.406 s    10 runs

Benchmark 3: biome
  Time (mean ± σ):     275.8 ms ±   7.4 ms    [User: 1098.2 ms, System: 317.4 ms]
  Range (min … max):   268.2 ms … 290.4 ms    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 4: oxfmt
  Time (mean ± σ):     102.7 ms ±   2.0 ms    [User: 264.8 ms, System: 154.0 ms]
  Range (min … max):   100.7 ms … 106.9 ms    10 runs

Benchmark 5: oxfmt_napi
  Time (mean ± σ):     578.1 ms ±  17.1 ms    [User: 3246.6 ms, System: 205.7 ms]
  Range (min … max):   560.6 ms … 620.0 ms    10 runs

Summary
  oxfmt ran
    2.69 ± 0.09 times faster than biome
    5.63 ± 0.20 times faster than oxfmt_napi
   32.52 ± 0.76 times faster than prettier+oxc-parser
   41.17 ± 1.83 times faster than prettier

outline repo contain 26 files which have css`...` calls.

The results are not improved so much after I manually deleted that 26 files.


(I hope that it might be my end's problem?)

  • Run pnpm build in oxc repo apps/oxfmt dir
  • Then update package.json in bench-formatter repo
    "oxfmt-current": "npm:oxfmt@0.8.0",
    "oxfmt-napi": "file:../oxc/apps/oxfmt"

@Boshen Boshen force-pushed the feat/oxfmt-embedded-languages branch from 07e3108 to 9023bea Compare October 28, 2025 07:55
@Boshen Boshen marked this pull request as ready for review October 28, 2025 07:55
@Boshen Boshen requested a review from Dunqing as a code owner October 28, 2025 07:55
Copilot AI review requested due to automatic review settings October 28, 2025 07:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds support for formatting embedded languages (CSS, GraphQL, HTML, Markdown) within JavaScript/TypeScript template literals by integrating Prettier through NAPI bindings. The implementation allows oxfmt to detect and format code in tagged template literals (e.g., css\...`, gql`...``) using Prettier's parsers, providing consistent formatting for embedded language blocks.

Key Changes

  • Added EmbeddedFormatter trait with callback-based API for formatting embedded code via Prettier
  • Implemented NAPI bridge to enable Rust-to-JavaScript interop for external formatting
  • Enhanced template literal formatting to detect supported tags and delegate to Prettier when appropriate

Reviewed Changes

Copilot reviewed 28 out of 30 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/oxc_formatter/src/embedded_formatter.rs Defines the EmbeddedFormatter trait and callback interface
crates/oxc_formatter/src/write/template.rs Added logic to detect and format embedded templates with proper indentation
crates/oxc_formatter/src/formatter/context.rs Added embedded formatter support to format context
apps/oxfmt/src/prettier_plugins/external_formatter.rs NAPI integration layer wrapping JS callbacks for embedded formatting
apps/oxfmt/src-js/embedded.ts TypeScript implementation using Prettier for formatting embedded code
apps/oxfmt/test/e2e.test.ts E2E tests for embedded language formatting
apps/oxfmt/package.json Added Prettier dependency and build scripts
justfile Added oxfmt build commands
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Boshen Boshen force-pushed the feat/oxfmt-embedded-languages branch from 09e24fd to d6f5cd0 Compare October 28, 2025 09:52
Copy link
Member

Dunqing commented Oct 30, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Member

@Dunqing Dunqing left a comment

Choose a reason for hiding this comment

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

LGTM! I made some improvements on the Rust side. It is a good first step for embedded language formatting support; there is a lot of work that needs to be done.

@fisker
Copy link

fisker commented Oct 31, 2025

@prettier/sync don't have a prettier dependency, should we add prettier to dependencies?

@leaysgur
Copy link
Member

leaysgur commented Nov 2, 2025

Thanks! Yes, we should.
(For now, we are using pnpm which automatically install peerDeps though)


Also I think we need to follow outcome of #15187.

@graphite-app graphite-app bot added the 0-merge Merge with Graphite Merge Queue label Nov 3, 2025
@graphite-app
Copy link
Contributor

graphite-app bot commented Nov 3, 2025

Merge activity

…on (#14820)

## Summary

Adds support for formatting embedded languages (CSS, GraphQL, HTML, Markdown) in JavaScript/TypeScript template literals using Prettier via NAPI bindings.

## Motivation

JavaScript/TypeScript developers often embed CSS, GraphQL queries, HTML, and Markdown in their code using tagged template literals. Without proper formatting support, these embedded languages remain unformatted, leading to inconsistent code style and reduced readability.

This PR enables oxfmt to format these embedded languages using Prettier, providing a seamless formatting experience similar to Prettier's own embedded language support.

## Changes

### Core Functionality
- **Embedded language detection**: Recognizes `css`, `styled`, `gql`, `graphql`, `html`, `md`, and `markdown` template tags
- **Prettier integration**: Uses `@prettier/sync` for synchronous formatting of embedded code
- **NAPI bridge**: Rust-to-JavaScript interop using NAPI ThreadsafeFunction
- **Graceful fallback**: Returns original code if formatting fails or tag is unsupported

### Implementation Details

**Rust side** (`crates/oxc_formatter/`, `apps/oxfmt/src/prettier_plugins/`):
- New `EmbeddedFormatter` trait with callback-based API
- NAPI bindings using `FnArgs` pattern for clean multi-argument calls
- Template formatter enhancement to detect and format tagged templates
- Proper indentation handling: splits Prettier output into lines, uses `hard_line_break()` separators, wraps in `indent()` for context-aware indentation
- Arena allocator pattern to avoid borrow checker conflicts

**JavaScript side** (`apps/oxfmt/src-js/`):
- TypeScript bindings with clean signatures: `formatEmbeddedCode(tagName: string, code: string)`
- Prettier configuration: 80 char width, 2 space indent, semicolons, double quotes
- Tag-to-parser mapping for supported languages
- Error handling with console logging and fallback

**Testing**:
- E2E test suite using Vitest (8 test cases)
- Test fixtures for each supported language
- Snapshot testing with timing normalization for stability
- All tests passing consistently

## Examples

**Before** (unformatted):
```js
const styles = css`.button { color: red; background: blue; }`;
```

**After** (formatted with proper indentation):
```js
const styles = css`
  .button {
    color: red;
    background: blue;
  }
`;
```

**Supported tags:**
```js
// CSS
const styles = css`...`;
const component = styled`...`;

// GraphQL
const query = gql`...`;
const schema = graphql`...`;

// HTML
const template = html`...`;

// Markdown
const docs = md`...`;
const readme = markdown`...`;
```

## Testing

```bash
# Run oxfmt tests
just oxfmt

# Watch mode for development
just watch-oxfmt
```

All 8 E2E tests pass:
- ✓ embedded_css
- ✓ embedded_graphql
- ✓ embedded_html
- ✓ embedded_markdown
- ✓ mixed_embedded
- ✓ no_embedded
- ✓ unsupported_tag
- ✓ check_mode

## Related

Related to #13427

## Future Work

Potential enhancements:
- Support for additional embedded languages (SQL, YAML, etc.)
- Custom Prettier configuration via oxfmt config
- Performance optimization for large embedded blocks
- Integration with language servers for inline diagnostics
@graphite-app graphite-app bot force-pushed the feat/oxfmt-embedded-languages branch from 39c90c0 to 898d6fe Compare November 3, 2025 02:05
@graphite-app graphite-app bot merged commit 898d6fe into main Nov 3, 2025
24 checks passed
@graphite-app graphite-app bot deleted the feat/oxfmt-embedded-languages branch November 3, 2025 02:12
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Nov 3, 2025
@Boshen Boshen mentioned this pull request Nov 4, 2025
leaysgur added a commit that referenced this pull request Nov 4, 2025
## [0.10.0] - 2025-11-04

### 🚀 Features

- 505252c formatter: Wrap parenthesis for AssignmentExpression that is a
key of `PropertyDefinition` (#15243) (Dunqing)
- 880b259 formatter: Align import-like formatting the same as Prettier
(#15238) (Dunqing)
- b77f254 oxfmt,formatter: Support `embeddedLanguageFormatting` option
(#15216) (leaysgur)
- 898d6fe oxfmt: Add embedded language formatting with Prettier
integration (#14820) (Boshen)
- e77a48e formatter: Detect code removal feature (#15059) (leaysgur)

### 🐛 Bug Fixes

- daacf85 oxfmt: Release build fails (#15262) (Dunqing)
- f5d0348 oxfmt: Sync `dependencies` with `npm/oxfmt` and `apps/oxfmt`
(#15261) (leaysgur)
- 46793d7 formatter: Correct printing comments for `LabeledStatement`
(#15260) (Dunqing)
- 831ae99 formatter: Multiple comments in `LogicalExpression` and
`TSIntersectionType` (#15253) (Dunqing)
- 5fa9b1e formatter: Should not indent `BinaryLikeExpression` when it is
an argument of `Boolean` (#15250) (Dunqing)
- 99e520f formatter: Handle chain expression for
`JSXExpressionContainer` (#15242) (Dunqing)
- a600bf5 formatter: Correct printing comments for
`TaggedTemplateExpression` (#15241) (Dunqing)
- a7289e7 formatter: Handle member chain for the call's parent is a
chain expression (#15237) (Dunqing)

### 🚜 Refactor

- 36ae721 formatter: Simplify the use of `indent` with
`soft_line_break_or_space` (#15254) (Dunqing)
- cdd8e2f formatter/sort-imports: Split sort_imports modules (#15189)
(leaysgur)
- 27b4f36 diagnostic: Remove `path` from sender (#15130) (camc314)
- 85fb8e8 formatter/sort-imports: Pass options to is_ignored() (#15181)
(leaysgur)

### 🧪 Testing

- 9d5b34b formatter/sort-imports: Refactor sort_imports tests (#15188)
(leaysgur)

Co-authored-by: leaysgur <6259812+leaysgur@users.noreply.github.com>
leaysgur added a commit that referenced this pull request Nov 4, 2025
leaysgur added a commit that referenced this pull request Nov 4, 2025
leaysgur added a commit that referenced this pull request Nov 4, 2025
leaysgur added a commit that referenced this pull request Nov 4, 2025
leaysgur added a commit that referenced this pull request Nov 4, 2025
@KieranP
Copy link

KieranP commented Nov 6, 2025

Do you think something similar could be done for whole files? e.g. Svelte files. If I ran:
oxfmt **/*.{js,ts,html,svelte,json}

could the html, svelte, and json files be offloaded to prettier? That way I wouldn't need to run two format commands:
yarn run prettier **/*.{html,svelte,json}
yarn run oxfmt **/*.{js,ts}

Delegating unsupported files to other formatters would be most helpful.

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

Labels

A-cli Area - CLI A-formatter Area - Formatter C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants