Which @angular/* package(s) are the source of the bug?
@angular/cli
Is this a regression?
Yes — introduced by feat(@angular/cli): integrate file formatting into update migrations (91b9d28, Jan 2026). Migrations did not run Prettier before this.
Description
After a migration touches many files, the CLI runs Prettier over all changed files in a single spawn. On a large repo (e.g. an Nx monorepo with 1000+ libs / thousands of changed files) the combined argument list exceeds the OS command-line limit and the spawn is rejected:
WARNING: Formatting of files failed with the following error: spawn ENAMETOOLONG
The migration itself succeeds (the error is caught and only logged), but none of the changed files get formatted — so on big repos the new formatting step effectively never runs, which is exactly where it is most useful.
Root cause
packages/angular/cli/src/utilities/prettier.ts — formatFiles spreads the entire files set into one execFile argument list:
await execFileAsync(
process.execPath,
[prettierCliPath, '--write', '--no-error-on-unmatched-pattern', '--ignore-unknown', ...files],
{ cwd, shell: false },
);
When files is large, argv exceeds the platform limit:
- Windows →
ENAMETOOLONG (CreateProcess command line capped at 32,767 chars)
- Linux / macOS →
E2BIG (total argv+envp over ARG_MAX)
shell: false does not help — the limit is on the execve/CreateProcess argument vector itself. Confirmed still present on main.
Please provide a link to a minimal reproduction of the bug
Reproduces exactly what formatFiles builds — no monorepo checkout needed. The spawn fails at the OS level before Prettier even starts, so the paths don't need to exist:
// repro.mjs — run: node repro.mjs
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { createRequire } from 'node:module';
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const prettierCli = require.resolve('prettier/bin/prettier.cjs');
const files = Array.from(
{ length: 25_000 },
(_, i) => `libs/feature-${i % 200}/src/lib/components/very/deeply/nested/widget-${i}.component.ts`,
);
try {
await execFileAsync(process.execPath, [prettierCli, '--write', ...files], { shell: false });
} catch (error) {
console.error(error.code, error.syscall); // E2BIG spawn (linux/mac) | ENAMETOOLONG spawn (windows)
}
Confirmed locally (Node v26, macOS, ARG_MAX 1 MiB): 25,000 paths ≈ 2 MB of argv → spawn E2BIG. The same overflow surfaces as spawn ENAMETOOLONG on Windows, which is what users hit in the wild (see references).
Please provide the exception or error you saw
(or spawn E2BIG on Linux/macOS)
Proposed fix
Batch the file list so each spawn's argument length stays under a conservative cross-platform budget, and run Prettier once per batch:
export async function formatFiles(cwd: string, files: Set<string>): Promise<void> {
if (!files.size) {
return;
}
// ...unchanged prettierCliPath resolution...
if (!prettierCliPath) {
return;
}
const baseArgs = [
prettierCliPath,
'--write',
'--no-error-on-unmatched-pattern',
'--ignore-unknown',
];
// Keep each spawn's argv well under the platform command-line limit
// (~32 KiB on Windows, larger on POSIX) to avoid E2BIG / ENAMETOOLONG when a
// migration touches thousands of files.
const MAX_ARG_LENGTH = 32_000;
const baseLength = baseArgs.reduce((total, arg) => total + arg.length + 1, 0);
let batch: string[] = [];
let batchLength = baseLength;
const flush = async (): Promise<void> => {
if (batch.length) {
await execFileAsync(process.execPath, [...baseArgs, ...batch], { cwd, shell: false });
batch = [];
batchLength = baseLength;
}
};
for (const file of files) {
const fileLength = file.length + 1;
if (batch.length && batchLength + fileLength > MAX_ARG_LENGTH) {
await flush();
}
batch.push(file);
batchLength += fileLength;
}
await flush();
}
32_000 is intentionally conservative (below the Windows 32,767 limit, far below POSIX ARG_MAX), leaving headroom for the environment block. Verified on the repro above: the 25,000-file case runs across ~63 spawns with no error. Happy to send a PR.
Notes
Please provide the environment you discovered this bug in
Angular CLI: 21.x (also reproduced on main)
Node: 26.0.0
OS: darwin (also reported on Windows)
Prettier: 3.x
Which @angular/* package(s) are the source of the bug?
@angular/cli
Is this a regression?
Yes — introduced by
feat(@angular/cli): integrate file formatting into update migrations(91b9d28, Jan 2026). Migrations did not run Prettier before this.Description
After a migration touches many files, the CLI runs Prettier over all changed files in a single spawn. On a large repo (e.g. an Nx monorepo with 1000+ libs / thousands of changed files) the combined argument list exceeds the OS command-line limit and the spawn is rejected:
The migration itself succeeds (the error is caught and only logged), but none of the changed files get formatted — so on big repos the new formatting step effectively never runs, which is exactly where it is most useful.
Root cause
packages/angular/cli/src/utilities/prettier.ts—formatFilesspreads the entirefilesset into oneexecFileargument list:When
filesis large,argvexceeds the platform limit:ENAMETOOLONG(CreateProcesscommand line capped at 32,767 chars)E2BIG(totalargv+envpoverARG_MAX)shell: falsedoes not help — the limit is on theexecve/CreateProcessargument vector itself. Confirmed still present onmain.Please provide a link to a minimal reproduction of the bug
Reproduces exactly what
formatFilesbuilds — no monorepo checkout needed. The spawn fails at the OS level before Prettier even starts, so the paths don't need to exist:Confirmed locally (Node v26, macOS,
ARG_MAX1 MiB): 25,000 paths ≈ 2 MB of argv →spawn E2BIG. The same overflow surfaces asspawn ENAMETOOLONGon Windows, which is what users hit in the wild (see references).Please provide the exception or error you saw
(or
spawn E2BIGon Linux/macOS)Proposed fix
Batch the file list so each spawn's argument length stays under a conservative cross-platform budget, and run Prettier once per batch:
32_000is intentionally conservative (below the Windows 32,767 limit, far below POSIXARG_MAX), leaving headroom for the environment block. Verified on the repro above: the 25,000-file case runs across ~63 spawns with no error. Happy to send a PR.Notes
ng buildspawn ENAMETOOLONGfrom many uncommitted files, incompiler-cli).Please provide the environment you discovered this bug in