From 8fc3c20225a0ef769077f5c3ae157bcec87ef2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Exbrayat?= Date: Wed, 21 Oct 2026 04:23:22 +0000 Subject: [PATCH] feat(@angular/cli): support Oxfmt for automatic file formatting --- .../schematics-command-module.ts | 2 +- .../commands/update/utilities/migration.ts | 2 +- .../utilities/{prettier.ts => formatting.ts} | 43 ++++++++++--------- .../{prettier_spec.ts => formatting_spec.ts} | 2 +- tests/e2e/tests/generate/formatting.ts | 32 ++++++++++++++ 5 files changed, 58 insertions(+), 23 deletions(-) rename packages/angular/cli/src/utilities/{prettier.ts => formatting.ts} (72%) rename packages/angular/cli/src/utilities/{prettier_spec.ts => formatting_spec.ts} (97%) create mode 100644 tests/e2e/tests/generate/formatting.ts diff --git a/packages/angular/cli/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts index e2561fd1c273..d41767942cc3 100644 --- a/packages/angular/cli/src/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -19,8 +19,8 @@ import { isPackageNameSafeForAnalytics } from '../analytics/analytics'; import { EventCustomDimension } from '../analytics/analytics-parameters'; import { getProjectByCwd, getSchematicDefaults } from '../utilities/config'; import { assertIsError } from '../utilities/error'; +import { formatFiles } from '../utilities/formatting'; import { memoize } from '../utilities/memoize'; -import { formatFiles } from '../utilities/prettier'; import { isTTY } from '../utilities/tty'; import { CommandModule, diff --git a/packages/angular/cli/src/commands/update/utilities/migration.ts b/packages/angular/cli/src/commands/update/utilities/migration.ts index 83a1492eaa10..a12eaa3b86e5 100644 --- a/packages/angular/cli/src/commands/update/utilities/migration.ts +++ b/packages/angular/cli/src/commands/update/utilities/migration.ts @@ -19,8 +19,8 @@ import * as semver from 'semver'; import { subscribeToWorkflow } from '../../../command-builder/utilities/schematic-workflow'; import { colors } from '../../../utilities/color'; import { assertIsError } from '../../../utilities/error'; +import { formatFiles } from '../../../utilities/formatting'; import { writeErrorToLogFile } from '../../../utilities/log-file'; -import { formatFiles } from '../../../utilities/prettier'; import { askChoices } from '../../../utilities/prompt'; import { isTTY } from '../../../utilities/tty'; import { coerceVersionNumber } from './cli-version'; diff --git a/packages/angular/cli/src/utilities/prettier.ts b/packages/angular/cli/src/utilities/formatting.ts similarity index 72% rename from packages/angular/cli/src/utilities/prettier.ts rename to packages/angular/cli/src/utilities/formatting.ts index 72c06b299367..f947b6aea00a 100644 --- a/packages/angular/cli/src/utilities/prettier.ts +++ b/packages/angular/cli/src/utilities/formatting.ts @@ -13,7 +13,6 @@ import { dirname, join } from 'node:path'; import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); -let prettierCliPath: string | null | undefined; /** * Conservative upper bound for the length of a single spawned command line. @@ -60,7 +59,7 @@ export function batchFilesByArgumentLength( } /** - * Formats files using Prettier. + * Formats files using the workspace's Oxfmt or Prettier installation. * @param cwd The current working directory. * @param files The files to format. */ @@ -69,40 +68,44 @@ export async function formatFiles(cwd: string, files: Set): Promise; + }; + formatterCliPath = join(dirname(packageJsonPath), typeof bin === 'string' ? bin : bin[name]); + formatter = name; + break; } - if (!prettierCliPath) { + if (!formatterCliPath) { return; } - const baseArgs = [ - prettierCliPath, - '--write', - '--no-error-on-unmatched-pattern', - '--ignore-unknown', - ]; + const baseArgs = [formatterCliPath, '--write', '--no-error-on-unmatched-pattern']; + if (formatter === 'prettier') { + baseArgs.push('--ignore-unknown'); + } // `process.execPath` is spawned as the first argument and also counts toward the - // command-line limit; it and `prettierCliPath` are absolute paths that may contain + // command-line limit; it and `formatterCliPath` are absolute paths that may contain // spaces and therefore be quoted. const baseLength = [process.execPath, ...baseArgs].reduce( (total, arg) => total + arg.length + (arg.includes(' ') ? 3 : 1), 0, ); - // Spawn Prettier once per batch so repositories with many changed files do not + // Spawn the formatter once per batch so repositories with many changed files do not // overflow the OS command-line length limit. A failure in one batch (e.g. a file - // Prettier cannot parse) must not stop the remaining batches, matching the previous + // the formatter cannot parse) must not stop the remaining batches, matching the previous // single-invocation behavior, so errors are collected and reported together. const errors: string[] = []; for (const batch of batchFilesByArgumentLength(files, baseLength, MAX_COMMAND_LINE_LENGTH)) { diff --git a/packages/angular/cli/src/utilities/prettier_spec.ts b/packages/angular/cli/src/utilities/formatting_spec.ts similarity index 97% rename from packages/angular/cli/src/utilities/prettier_spec.ts rename to packages/angular/cli/src/utilities/formatting_spec.ts index 4f4df1e78b03..f94129f43f4c 100644 --- a/packages/angular/cli/src/utilities/prettier_spec.ts +++ b/packages/angular/cli/src/utilities/formatting_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { batchFilesByArgumentLength } from './prettier'; +import { batchFilesByArgumentLength } from './formatting'; describe('batchFilesByArgumentLength', () => { it('returns no batches when there are no files', () => { diff --git a/tests/e2e/tests/generate/formatting.ts b/tests/e2e/tests/generate/formatting.ts new file mode 100644 index 000000000000..d88d5b0c995b --- /dev/null +++ b/tests/e2e/tests/generate/formatting.ts @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import { expectFileToMatch, readFile, writeFile } from '../../utils/fs'; +import { installPackage, uninstallPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; + +export default async function () { + await uninstallPackage('prettier'); + await installPackage('oxfmt'); + + const untouched = 'export const untouched={value:"unchanged"};'; + await writeFile('src/untouched.ts', untouched); + await writeFile('.oxfmtrc.json', JSON.stringify({ singleQuote: false, semi: false })); + + await ng('generate', 'service', 'oxfmt-only', '--skip-tests'); + await expectFileToMatch('src/app/oxfmt-only.ts', 'from "@angular/core"\n'); + + // Distinct configurations make the selected formatter observable when both are installed. + await installPackage('prettier'); + await writeFile('.prettierrc', JSON.stringify({ singleQuote: true, semi: false })); + await ng('generate', 'service', 'both-formatters', '--skip-tests'); + await expectFileToMatch('src/app/both-formatters.ts', 'from "@angular/core"\n'); + + await uninstallPackage('oxfmt'); + await ng('generate', 'service', 'prettier-only', '--skip-tests'); + await expectFileToMatch('src/app/prettier-only.ts', "from '@angular/core'\n"); + + await uninstallPackage('prettier'); + await ng('generate', 'service', 'no-formatter', '--skip-tests'); + await expectFileToMatch('src/app/no-formatter.ts', "from '@angular/core';\n"); + + assert.equal(await readFile('src/untouched.ts'), untouched); +}