Skip to content

Commit 7d6550e

Browse files
cexbrayatalan-agius4
authored andcommitted
feat(@angular/cli): support Oxfmt for automatic file formatting
1 parent 19de4c3 commit 7d6550e

5 files changed

Lines changed: 64 additions & 25 deletions

File tree

‎packages/angular/cli/src/command-builder/schematics-command-module.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { isPackageNameSafeForAnalytics } from '../analytics/analytics';
1919
import { EventCustomDimension } from '../analytics/analytics-parameters';
2020
import { getProjectByCwd, getSchematicDefaults } from '../utilities/config';
2121
import { assertIsError } from '../utilities/error';
22+
import { formatFiles } from '../utilities/formatting';
2223
import { memoize } from '../utilities/memoize';
23-
import { formatFiles } from '../utilities/prettier';
2424
import { isTTY } from '../utilities/tty';
2525
import {
2626
CommandModule,

‎packages/angular/cli/src/commands/update/utilities/migration.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import * as semver from 'semver';
1919
import { subscribeToWorkflow } from '../../../command-builder/utilities/schematic-workflow';
2020
import { colors } from '../../../utilities/color';
2121
import { assertIsError } from '../../../utilities/error';
22+
import { formatFiles } from '../../../utilities/formatting';
2223
import { writeErrorToLogFile } from '../../../utilities/log-file';
23-
import { formatFiles } from '../../../utilities/prettier';
2424
import { askChoices } from '../../../utilities/prompt';
2525
import { isTTY } from '../../../utilities/tty';
2626
import { coerceVersionNumber } from './cli-version';

packages/angular/cli/src/utilities/prettier.ts renamed to packages/angular/cli/src/utilities/formatting.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { dirname, join } from 'node:path';
1313
import { promisify } from 'node:util';
1414

1515
const execFileAsync = promisify(execFile);
16-
let prettierCliPath: string | null | undefined;
16+
let formatter: { name: string; cliPath: string } | null | undefined;
1717

1818
/**
1919
* Conservative upper bound for the length of a single spawned command line.
@@ -60,7 +60,7 @@ export function batchFilesByArgumentLength(
6060
}
6161

6262
/**
63-
* Formats files using Prettier.
63+
* Formats files using the workspace's Prettier installation, falling back to Oxfmt.
6464
* @param cwd The current working directory.
6565
* @param files The files to format.
6666
*/
@@ -69,40 +69,46 @@ export async function formatFiles(cwd: string, files: Set<string>): Promise<void
6969
return;
7070
}
7171

72-
if (prettierCliPath === undefined) {
73-
try {
74-
const prettierPath = createRequire(cwd + '/').resolve('prettier/package.json');
75-
const prettierPackageJson = JSON.parse(await readFile(prettierPath, 'utf-8')) as {
76-
bin: string;
77-
};
78-
prettierCliPath = join(dirname(prettierPath), prettierPackageJson.bin);
79-
} catch {
80-
// Prettier is not installed.
81-
prettierCliPath = null;
72+
if (formatter === undefined) {
73+
const workspaceRequire = createRequire(cwd + '/');
74+
formatter = null;
75+
for (const name of ['prettier', 'oxfmt']) {
76+
try {
77+
const packageJsonPath = workspaceRequire.resolve(`${name}/package.json`);
78+
const { bin } = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as {
79+
bin: string | Record<string, string>;
80+
};
81+
formatter = {
82+
name,
83+
cliPath: join(dirname(packageJsonPath), typeof bin === 'string' ? bin : bin[name]),
84+
};
85+
break;
86+
} catch {
87+
continue;
88+
}
8289
}
8390
}
8491

85-
if (!prettierCliPath) {
92+
if (!formatter) {
8693
return;
8794
}
8895

89-
const baseArgs = [
90-
prettierCliPath,
91-
'--write',
92-
'--no-error-on-unmatched-pattern',
93-
'--ignore-unknown',
94-
];
96+
const { cliPath: formatterCliPath } = formatter;
97+
const baseArgs = [formatterCliPath, '--write', '--no-error-on-unmatched-pattern'];
98+
if (formatter.name === 'prettier') {
99+
baseArgs.push('--ignore-unknown');
100+
}
95101
// `process.execPath` is spawned as the first argument and also counts toward the
96-
// command-line limit; it and `prettierCliPath` are absolute paths that may contain
102+
// command-line limit; it and `formatterCliPath` are absolute paths that may contain
97103
// spaces and therefore be quoted.
98104
const baseLength = [process.execPath, ...baseArgs].reduce(
99105
(total, arg) => total + arg.length + (arg.includes(' ') ? 3 : 1),
100106
0,
101107
);
102108

103-
// Spawn Prettier once per batch so repositories with many changed files do not
109+
// Spawn the formatter once per batch so repositories with many changed files do not
104110
// overflow the OS command-line length limit. A failure in one batch (e.g. a file
105-
// Prettier cannot parse) must not stop the remaining batches, matching the previous
111+
// the formatter cannot parse) must not stop the remaining batches, matching the previous
106112
// single-invocation behavior, so errors are collected and reported together.
107113
const errors: string[] = [];
108114
for (const batch of batchFilesByArgumentLength(files, baseLength, MAX_COMMAND_LINE_LENGTH)) {

packages/angular/cli/src/utilities/prettier_spec.ts renamed to packages/angular/cli/src/utilities/formatting_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { batchFilesByArgumentLength } from './prettier';
9+
import { batchFilesByArgumentLength } from './formatting';
1010

1111
describe('batchFilesByArgumentLength', () => {
1212
it('returns no batches when there are no files', () => {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import assert from 'node:assert/strict';
2+
import { expectFileToMatch, readFile, writeFile } from '../../utils/fs';
3+
import { installPackage, uninstallPackage } from '../../utils/packages';
4+
import { ng } from '../../utils/process';
5+
6+
export default async function () {
7+
const untouched = 'export const untouched={value:"unchanged"};';
8+
try {
9+
await writeFile('src/untouched.ts', untouched);
10+
await writeFile('.prettierrc', JSON.stringify({ singleQuote: true, semi: false }));
11+
await ng('generate', 'service', 'prettier-only', '--skip-tests');
12+
await expectFileToMatch('src/app/prettier-only.ts', "from '@angular/core'\n");
13+
14+
// Distinct configurations make the selected formatter observable when both are installed.
15+
await installPackage('oxfmt');
16+
await writeFile('.oxfmtrc.json', JSON.stringify({ singleQuote: false, semi: false }));
17+
await ng('generate', 'service', 'both-formatters', '--skip-tests');
18+
await expectFileToMatch('src/app/both-formatters.ts', "from '@angular/core'\n");
19+
20+
await uninstallPackage('prettier');
21+
await ng('generate', 'service', 'oxfmt-only', '--skip-tests');
22+
await expectFileToMatch('src/app/oxfmt-only.ts', 'from "@angular/core"\n');
23+
24+
await uninstallPackage('oxfmt');
25+
await ng('generate', 'service', 'no-formatter', '--skip-tests');
26+
await expectFileToMatch('src/app/no-formatter.ts', "from '@angular/core';\n");
27+
28+
assert.equal(await readFile('src/untouched.ts'), untouched);
29+
} finally {
30+
await uninstallPackage('oxfmt');
31+
await installPackage('prettier');
32+
}
33+
}

0 commit comments

Comments
 (0)