Skip to content

feat(@angular/cli): support Oxfmt for automatic file formatting - #34460

Open
cexbrayat wants to merge 1 commit into
angular:mainfrom
cexbrayat:feat/oxfmt-autoformatting
Open

cexbrayat wants to merge 1 commit into
angular:mainfrom
cexbrayat:feat/oxfmt-autoformatting

Conversation

@cexbrayat

Copy link
Copy Markdown
Member

PR Checklist

Please check to confirm your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

The only auto formatting supported out if the box is prettier

What is the new behavior?

If the project has oxfmt installed, then oxfmt is preferred. Prettier is then used as a fallback to preserve the existing behavior.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

@angular-robot angular-robot Bot added detected: feature PR contains a feature commit area: @angular/cli labels Oct 21, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the Angular CLI formatting utility to support both oxfmt and prettier as formatters, renaming the utility file and updating imports and tests accordingly. The review feedback suggests improving robustness and performance by caching the resolved formatter path at the module level to avoid redundant disk I/O. Additionally, it points out that the --no-error-on-unmatched-pattern flag should only be passed to prettier, as oxfmt uses strict argument parsing and will fail if an unknown option is provided.

Comment on lines +71 to 88
const workspaceRequire = createRequire(cwd + '/');
let formatterCliPath: string | undefined;
let formatter: string | undefined;
for (const name of ['oxfmt', 'prettier']) {
let packageJsonPath: string;
try {
const prettierPath = createRequire(cwd + '/').resolve('prettier/package.json');
const prettierPackageJson = JSON.parse(await readFile(prettierPath, 'utf-8')) as {
bin: string;
};
prettierCliPath = join(dirname(prettierPath), prettierPackageJson.bin);
packageJsonPath = workspaceRequire.resolve(`${name}/package.json`);
} catch {
// Prettier is not installed.
prettierCliPath = null;
continue;
}

const { bin } = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as {
bin: string | Record<string, string>;
};
formatterCliPath = join(dirname(packageJsonPath), typeof bin === 'string' ? bin : bin[name]);
formatter = name;
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Improve robustness and implement caching when resolving the formatter:

  1. Wrap the entire resolution, file reading, and JSON parsing inside the try-catch block to prevent any unexpected errors (e.g., missing/malformed package.json or missing bin field) from crashing the CLI.
  2. Safely check if bin is a string or an object containing the formatter name before calling path.join to avoid a TypeError if bin or bin[name] is undefined.
  3. Use the module-level formatterCache to cache the resolved formatter details.
  let cacheEntry = formatterCache.get(cwd);
  if (!cacheEntry) {
    const workspaceRequire = createRequire(cwd + '/');
    let formatterCliPath: string | null = null;
    let formatter: string | undefined;

    for (const name of ['oxfmt', 'prettier']) {
      try {
        const packageJsonPath = workspaceRequire.resolve(`${name}/package.json`);
        const { bin } = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as {
          bin?: string | Record<string, string>;
        };
        const binPath = typeof bin === 'string' ? bin : bin?.[name];
        if (binPath) {
          formatterCliPath = join(dirname(packageJsonPath), binPath);
          formatter = name;
          break;
        }
      } catch {
        continue;
      }
    }

    cacheEntry = { cliPath: formatterCliPath, formatter };
    formatterCache.set(cwd, cacheEntry);
  }

  const { cliPath: formatterCliPath, formatter } = cacheEntry;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let me know if this should be done

Comment on lines +94 to +97
const baseArgs = [formatterCliPath, '--write', '--no-error-on-unmatched-pattern'];
if (formatter === 'prettier') {
baseArgs.push('--ignore-unknown');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Only pass --no-error-on-unmatched-pattern when the formatter is prettier. oxfmt (and other oxc-based tools) use strict argument parsing (via Rust's clap library) and will fail with an error if an unknown option like --no-error-on-unmatched-pattern is passed, preventing any files from being formatted.

Suggested change
const baseArgs = [formatterCliPath, '--write', '--no-error-on-unmatched-pattern'];
if (formatter === 'prettier') {
baseArgs.push('--ignore-unknown');
}
const baseArgs = [formatterCliPath, '--write'];
if (formatter === 'prettier') {
baseArgs.push('--no-error-on-unmatched-pattern', '--ignore-unknown');
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oxfmt 0.74.0 supports --no-error-on-unmatched-pattern https://oxc.rs/docs/guide/usage/formatter/cli.html#runtime-options

Comment on lines 15 to 16
const execFileAsync = promisify(execFile);
let prettierCliPath: string | null | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To avoid redundant disk I/O and package resolution when formatFiles is called multiple times (e.g., during multiple migrations in a loop), we should cache the resolved formatter path and name. We can define a cache Map at the module level.

Suggested change
const execFileAsync = promisify(execFile);
let prettierCliPath: string | null | undefined;
const execFileAsync = promisify(execFile);
const formatterCache = new Map<string, { cliPath: string | null; formatter?: string }>();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let me know if that should be done

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

Labels

area: @angular/cli detected: feature PR contains a feature commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant