nx 23.2.0-beta.8 → 23.2.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/src/ai/set-up-ai-agents/set-up-ai-agents.js +2 -2
  2. package/dist/src/command-line/format/format.js +84 -101
  3. package/dist/src/command-line/import/import.js +12 -0
  4. package/dist/src/command-line/init/configure-plugins.js +4 -1
  5. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +9 -0
  6. package/dist/src/command-line/init/implementation/add-nx-to-turborepo.js +3 -17
  7. package/dist/src/command-line/init/implementation/angular/index.js +4 -0
  8. package/dist/src/command-line/init/implementation/angular/legacy-angular-versions.js +2 -0
  9. package/dist/src/command-line/init/implementation/angular/standalone-workspace.js +8 -2
  10. package/dist/src/command-line/init/implementation/check-compatible-with-plugins.js +2 -0
  11. package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +5 -0
  12. package/dist/src/command-line/init/implementation/format.d.ts +18 -0
  13. package/dist/src/command-line/init/implementation/format.js +114 -0
  14. package/dist/src/command-line/init/implementation/utils.js +8 -0
  15. package/dist/src/command-line/init/init-v1.js +7 -0
  16. package/dist/src/command-line/init/init-v2.js +11 -0
  17. package/dist/src/command-line/migrate/migrate.d.ts +4 -0
  18. package/dist/src/command-line/migrate/migrate.js +64 -24
  19. package/dist/src/command-line/release/version.js +2 -2
  20. package/dist/src/daemon/client/client.js +11 -2
  21. package/dist/src/devkit-internals.d.ts +5 -2
  22. package/dist/src/devkit-internals.js +14 -6
  23. package/dist/src/generators/internal-utils/{format-changed-files-with-prettier-if-available.d.ts → format-changed-files.d.ts} +6 -6
  24. package/dist/src/generators/internal-utils/format-changed-files.js +163 -0
  25. package/dist/src/generators/testing-utils/create-tree-with-empty-workspace.d.ts +15 -0
  26. package/dist/src/generators/testing-utils/create-tree-with-empty-workspace.js +21 -3
  27. package/dist/src/migrations/update-15-0-0/prefix-outputs.js +2 -2
  28. package/dist/src/migrations/update-16-0-0/remove-nrwl-cli.js +2 -2
  29. package/dist/src/migrations/update-16-0-0/update-depends-on-to-tokens.js +2 -2
  30. package/dist/src/migrations/update-16-0-0/update-nx-cloud-runner.js +2 -2
  31. package/dist/src/migrations/update-16-2-0/remove-run-commands-output-path.js +2 -2
  32. package/dist/src/migrations/update-17-0-0/rm-default-collection-npm-scope.js +2 -2
  33. package/dist/src/migrations/update-17-0-0/use-minimal-config-for-tasks-runner-options.js +2 -2
  34. package/dist/src/migrations/update-17-2-0/move-default-base.js +2 -2
  35. package/dist/src/migrations/update-18-0-0/disable-crystal-for-existing-workspaces.js +2 -2
  36. package/dist/src/migrations/update-19-2-4/set-project-name.js +2 -2
  37. package/dist/src/migrations/update-20-0-0/move-use-daemon-process.js +2 -2
  38. package/dist/src/migrations/update-20-0-1/use-legacy-cache.js +2 -2
  39. package/dist/src/migrations/update-21-0-0/release-changelog-config-changes.js +2 -2
  40. package/dist/src/migrations/update-21-0-0/release-version-config-changes.js +2 -2
  41. package/dist/src/migrations/update-21-0-0/remove-custom-tasks-runner.js +2 -2
  42. package/dist/src/migrations/update-21-0-0/remove-legacy-cache.js +2 -2
  43. package/dist/src/migrations/update-22-0-0/consolidate-release-tag-config.js +2 -2
  44. package/dist/src/migrations/update-22-0-0/release-version-config-changes.js +2 -2
  45. package/dist/src/migrations/update-22-2-0/add-self-healing-to-gitignore.js +2 -2
  46. package/dist/src/migrations/update-22-6-0/add-claude-settings-local-to-git-ignore.js +2 -2
  47. package/dist/src/migrations/update-22-6-0/add-claude-worktrees-to-git-ignore.js +2 -2
  48. package/dist/src/migrations/update-22-7-0/add-polygraph-to-git-ignore.js +2 -2
  49. package/dist/src/migrations/update-23-0-0/add-migrate-runs-to-git-ignore.js +2 -2
  50. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +2 -2
  51. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  52. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  53. package/dist/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.js +3 -3
  54. package/dist/src/utils/chunkify.d.ts +7 -1
  55. package/dist/src/utils/chunkify.js +10 -3
  56. package/dist/src/utils/formatters/index.d.ts +13 -0
  57. package/dist/src/utils/formatters/index.js +74 -0
  58. package/dist/src/utils/formatters/oxfmt.d.ts +32 -0
  59. package/dist/src/utils/formatters/oxfmt.js +764 -0
  60. package/dist/src/utils/formatters/prettier.d.ts +26 -0
  61. package/dist/src/utils/formatters/prettier.js +201 -0
  62. package/dist/src/utils/formatters/shared.d.ts +5 -0
  63. package/dist/src/utils/formatters/shared.js +8 -0
  64. package/dist/src/utils/ignore.d.ts +47 -5
  65. package/dist/src/utils/ignore.js +93 -11
  66. package/dist/src/utils/write-formatted-json-file.d.ts +3 -2
  67. package/dist/src/utils/write-formatted-json-file.js +5 -4
  68. package/package.json +11 -11
  69. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.js +0 -88
  70. package/dist/src/utils/is-using-prettier.d.ts +0 -3
  71. package/dist/src/utils/is-using-prettier.js +0 -62
@@ -0,0 +1,764 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oxfmtConfigFiles = void 0;
4
+ exports.isUsingOxfmt = isUsingOxfmt;
5
+ exports.isUsingOxfmtInTree = isUsingOxfmtInTree;
6
+ exports.getOxfmtBinPath = getOxfmtBinPath;
7
+ exports.writeWithOxfmt = writeWithOxfmt;
8
+ exports.checkWithOxfmt = checkWithOxfmt;
9
+ exports.formatFilesWithOxfmt = formatFilesWithOxfmt;
10
+ const tslib_1 = require("tslib");
11
+ const ignore = require("ignore");
12
+ const minimatch_1 = require("minimatch");
13
+ const node_child_process_1 = require("node:child_process");
14
+ const node_fs_1 = require("node:fs");
15
+ const path = tslib_1.__importStar(require("node:path"));
16
+ const node_url_1 = require("node:url");
17
+ const ignore_1 = require("../ignore");
18
+ const json_1 = require("../json");
19
+ const package_json_1 = require("../package-json");
20
+ const shared_1 = require("./shared");
21
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
22
+ /**
23
+ * oxfmt errors on a file it has no parser for rather than skipping it, and
24
+ * nx hands it every changed file.
25
+ */
26
+ const UNSUPPORTED_FILE_TYPE = 'Unsupported file type';
27
+ /**
28
+ * Config filenames oxfmt *discovers* - narrower than the set `-c` accepts.
29
+ * Measured against 0.60.0: `oxfmt.config.{js,cjs,mjs,cts}` load when named
30
+ * but are never searched for, so treating one as config would format on
31
+ * options oxfmt ignores. No precedence order - oxfmt fails with
32
+ * "Both '<a>' and '<b>' found in <dir>" when a directory holds two.
33
+ */
34
+ exports.oxfmtConfigFiles = [
35
+ '.oxfmtrc.json',
36
+ '.oxfmtrc.jsonc',
37
+ 'oxfmt.config.ts',
38
+ 'oxfmt.config.mts',
39
+ ];
40
+ function isUsingOxfmt(root) {
41
+ for (const file of exports.oxfmtConfigFiles) {
42
+ if ((0, node_fs_1.existsSync)(path.join(root, file))) {
43
+ return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+ function isUsingOxfmtInTree(tree) {
49
+ for (const file of exports.oxfmtConfigFiles) {
50
+ if (tree.exists(file)) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+ /**
57
+ * oxfmt exits 2 when *every* path was skipped. Nx routinely passes mixed
58
+ * file lists, so an empty match is success, not failure.
59
+ */
60
+ const OXFMT_BASE_ARGS = ['--no-error-on-unmatched-pattern'];
61
+ let cachedOxfmtBin;
62
+ function getOxfmtBinPath() {
63
+ if (cachedOxfmtBin) {
64
+ return cachedOxfmtBin;
65
+ }
66
+ const { packageJson, path: packageJsonPath } = (0, package_json_1.readModulePackageJson)('oxfmt');
67
+ const bin = typeof packageJson.bin === 'string'
68
+ ? packageJson.bin
69
+ : packageJson.bin?.['oxfmt'];
70
+ if (!bin) {
71
+ throw new Error(`Could not find the oxfmt binary in ${packageJsonPath}`);
72
+ }
73
+ cachedOxfmtBin = path.resolve(path.dirname(packageJsonPath), bin);
74
+ return cachedOxfmtBin;
75
+ }
76
+ function writeWithOxfmt(patterns,
77
+ // Defaults to the caller's cwd. `nx init` sets it so it can pass paths
78
+ // relative to the repo root rather than absolute ones the user has to read.
79
+ cwd) {
80
+ const oxfmtPath = getOxfmtBinPath();
81
+ (0, node_child_process_1.execFileSync)('node', [oxfmtPath, ...OXFMT_BASE_ARGS, '--write', '--', ...patterns], {
82
+ cwd,
83
+ stdio: [0, 1, 2],
84
+ windowsHide: true,
85
+ });
86
+ }
87
+ function checkWithOxfmt(patterns) {
88
+ const oxfmtPath = getOxfmtBinPath();
89
+ return new Promise((resolve, reject) => {
90
+ (0, node_child_process_1.execFile)('node', [oxfmtPath, ...OXFMT_BASE_ARGS, '--list-different', '--', ...patterns], {
91
+ encoding: 'utf-8',
92
+ windowsHide: true,
93
+ maxBuffer: shared_1.FORMATTER_MAX_BUFFER,
94
+ }, (error, stdout, stderr) => {
95
+ // A spawn failure, kill, or maxBuffer overrun reports a string `code` or
96
+ // none. Treating those as exit 0 would let `nx format:check` pass on a
97
+ // formatter that never ran, so they reject before the code is read.
98
+ if (error && typeof error['code'] !== 'number') {
99
+ reject(new Error(`oxfmt could not be run to completion (${error['code'] ?? error.signal ?? 'unknown'}): ${error.message}`));
100
+ return;
101
+ }
102
+ // oxfmt writes the differing paths to stdout *before* it reports any
103
+ // error, so a non-empty stdout does not mean the run succeeded. The
104
+ // exit code is the only reliable signal.
105
+ const code = error ? error['code'] : 0 /* OxfmtExitCode.Success */;
106
+ if (code === 0 /* OxfmtExitCode.Success */) {
107
+ resolve([]);
108
+ }
109
+ else if (code === 1 /* OxfmtExitCode.Mismatch */ &&
110
+ stdout.trim().length > 0) {
111
+ resolve(stdout.trim().split('\n'));
112
+ }
113
+ else {
114
+ // Exit 1 with no stdout means an invalid config; exit 2 means oxfmt
115
+ // failed outright (parse error, unreadable file).
116
+ reject(new Error(stderr?.trim() ||
117
+ error?.message ||
118
+ `oxfmt exited with code ${code}`));
119
+ }
120
+ });
121
+ });
122
+ }
123
+ let cachedOxfmtModule;
124
+ /**
125
+ * `require` first: Node resolves ESM-only through it (20.19+/22.12+), and the
126
+ * bare specifier is what lets jest swap in a CommonJS mock. The fallback reaches
127
+ * `import()` via `new Function` so TypeScript cannot downlevel it to `require`.
128
+ *
129
+ * The two resolve from different places on purpose - `require` from nx's own
130
+ * chain, the fallback from the workspace's install, since nx does not depend
131
+ * on oxfmt.
132
+ */
133
+ function loadOxfmtModule() {
134
+ if (!cachedOxfmtModule) {
135
+ cachedOxfmtModule = (async () => {
136
+ try {
137
+ // Node resolves an ESM-only package through `require` on its own, and
138
+ // going through the package name keeps the module mockable.
139
+ const required = require('oxfmt');
140
+ return required.format ? required : required.default;
141
+ }
142
+ catch {
143
+ // Older runtimes cannot `require` an ESM package, so import the entry
144
+ // point the same way the binary is resolved - from the workspace's own
145
+ // install, because nx does not depend on oxfmt itself.
146
+ const { packageJson, path: packageJsonPath } = (0, package_json_1.readModulePackageJson)('oxfmt');
147
+ const entryPoint = path.resolve(path.dirname(packageJsonPath), packageJson.main ?? 'dist/index.js');
148
+ const imported = await dynamicImport((0, node_url_1.pathToFileURL)(entryPoint).href);
149
+ return imported.format ? imported : imported.default;
150
+ }
151
+ })().catch((error) => {
152
+ // Do not hold on to the failure - the next call gets to try again.
153
+ cachedOxfmtModule = undefined;
154
+ throw error;
155
+ });
156
+ }
157
+ return cachedOxfmtModule;
158
+ }
159
+ function isJsonOxfmtConfig(name) {
160
+ return name.endsWith('.json') || name.endsWith('.jsonc');
161
+ }
162
+ /**
163
+ * `register` is required lazily so a JSON config does not pull in the
164
+ * transpiler. `loadTsFile` bubbles the ESM-redispatch codes for a caller like
165
+ * this one to dispatch to `import()`.
166
+ *
167
+ * The retry is deliberately not gated on those codes: the same config surfaces
168
+ * as `ERR_REQUIRE_ASYNC_MODULE` or as `exports is not defined` depending on
169
+ * whether swc/ts-node registered, and both mean "this is ESM, import it".
170
+ * Only `import()` ever evaluates the config, so its error is thrown with the
171
+ * `require` one as its cause rather than either being chosen.
172
+ *
173
+ * Covered by `create-nx-workspace-formatter.test.ts`; unreachable from jest.
174
+ */
175
+ async function loadTsOxfmtConfig(configPath) {
176
+ try {
177
+ return require('../../plugins/js/utils/register').loadTsFile(configPath);
178
+ }
179
+ catch (loadError) {
180
+ try {
181
+ return await dynamicImport((0, node_url_1.pathToFileURL)(configPath).href);
182
+ }
183
+ catch (importError) {
184
+ throw Object.assign(importError, { cause: loadError });
185
+ }
186
+ }
187
+ }
188
+ /**
189
+ * True when an ignore file along the chain covers the file, or the resolved
190
+ * config's own `ignorePatterns` do.
191
+ *
192
+ * `ignorePatterns` is not an ignore file: oxfmt roots it at that config's
193
+ * directory, so it is matched separately from the chain.
194
+ */
195
+ function isIgnored(isIgnoredFile, relativePath, config, absoluteFilePath) {
196
+ if (isIgnoredFile(relativePath)) {
197
+ return true;
198
+ }
199
+ if (config.ignoreMatcher) {
200
+ const relative = toRelativeWithin(config.dir, absoluteFilePath);
201
+ if (relative !== undefined && config.ignoreMatcher.ignores(relative)) {
202
+ return true;
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+ function readEditorConfigInDir(dir) {
208
+ let contents;
209
+ try {
210
+ contents = (0, node_fs_1.readFileSync)(path.join(dir, '.editorconfig'), 'utf-8');
211
+ }
212
+ catch (e) {
213
+ // Not having one is the common case. Anything else - unreadable, a
214
+ // directory, a broken mount - would otherwise look identical to that and
215
+ // silently format to different widths than `nx format` produces.
216
+ if (e.code !== 'ENOENT') {
217
+ throw new Error(`Could not read .editorconfig: ${e.message}`);
218
+ }
219
+ return undefined;
220
+ }
221
+ return parseEditorConfig(contents);
222
+ }
223
+ /**
224
+ * Compiles each section's glob once per batch, not per file: the globs are
225
+ * invariant, so this is O(sections) rather than O(files x sections).
226
+ */
227
+ function parseEditorConfig(contents) {
228
+ const sections = [];
229
+ let current;
230
+ let isRoot = false;
231
+ for (const line of contents.split(/\r?\n/)) {
232
+ const trimmed = line.trim();
233
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
234
+ continue;
235
+ }
236
+ const header = /^\[(.*)\]$/.exec(trimmed);
237
+ if (header) {
238
+ current = { matches: compileEditorConfigGlob(header[1]), properties: {} };
239
+ sections.push(current);
240
+ continue;
241
+ }
242
+ const separator = trimmed.indexOf('=');
243
+ if (separator === -1) {
244
+ continue;
245
+ }
246
+ const key = trimmed.slice(0, separator).trim().toLowerCase();
247
+ const value = trimmed
248
+ .slice(separator + 1)
249
+ .trim()
250
+ .toLowerCase();
251
+ // Anything before the first section is preamble, where `root = true` stops
252
+ // the walk-up.
253
+ if (!current) {
254
+ if (key === 'root' && value === 'true') {
255
+ isRoot = true;
256
+ }
257
+ continue;
258
+ }
259
+ current.properties[key] = value;
260
+ }
261
+ return { sections, isRoot };
262
+ }
263
+ /**
264
+ * The `.editorconfig` files applying to a directory, farthest first, walking
265
+ * up until one declares `root = true` - the spec's termination rule, which
266
+ * the oxfmt CLI follows. Deliberately continues above the workspace root,
267
+ * where a repo nested in a larger checkout keeps shared settings.
268
+ *
269
+ * `read` takes workspace-relative paths and the walk starts at the workspace
270
+ * root, so it can only serve the starting directory; every directory above
271
+ * sits outside the workspace and is read from disk.
272
+ *
273
+ * Returned in application order, so nearer overwrites farther.
274
+ */
275
+ function editorConfigChainFor(dir, read) {
276
+ const found = [];
277
+ const start = path.resolve(dir);
278
+ let current = start;
279
+ while (true) {
280
+ let parsed;
281
+ if (read && current === start) {
282
+ const contents = read('.editorconfig');
283
+ parsed = contents == null ? undefined : parseEditorConfig(contents);
284
+ }
285
+ else {
286
+ parsed = readEditorConfigInDir(current);
287
+ }
288
+ if (parsed) {
289
+ found.push({ ...parsed, dir: current });
290
+ if (parsed.isRoot) {
291
+ break;
292
+ }
293
+ }
294
+ const parent = path.dirname(current);
295
+ if (parent === current) {
296
+ break;
297
+ }
298
+ current = parent;
299
+ }
300
+ return found.reverse();
301
+ }
302
+ /**
303
+ * Compiles one `.editorconfig` section header into a matcher.
304
+ *
305
+ * Per the spec a pattern containing `/` is relative to the `.editorconfig`'s
306
+ * directory (a leading `/` is only an anchor and is stripped); one with no
307
+ * separator applies at any depth.
308
+ */
309
+ function compileEditorConfigGlob(glob) {
310
+ const pattern = glob.startsWith('/')
311
+ ? glob.slice(1)
312
+ : glob.includes('/')
313
+ ? glob
314
+ : `**/${glob}`;
315
+ const matcher = new minimatch_1.Minimatch(pattern, { dot: true });
316
+ return (filePath) => matcher.match(filePath);
317
+ }
318
+ /**
319
+ * Translates the `.editorconfig` properties with an oxfmt equivalent. Values
320
+ * oxfmt has no meaning for - including the spec's `unset` - are left out so
321
+ * oxfmt's own default applies.
322
+ */
323
+ function editorConfigOptionsForFile(files, absoluteFilePath) {
324
+ const properties = {};
325
+ for (const file of files) {
326
+ // Globs are relative to the directory holding the `.editorconfig`.
327
+ const relative = toRelativeWithin(file.dir, absoluteFilePath);
328
+ if (relative === undefined) {
329
+ continue;
330
+ }
331
+ for (const section of file.sections) {
332
+ if (section.matches(relative)) {
333
+ // Later sections win, matching how editorconfig resolves a property.
334
+ Object.assign(properties, section.properties);
335
+ }
336
+ }
337
+ }
338
+ const options = {};
339
+ const indentStyle = properties['indent_style'];
340
+ if (indentStyle === 'tab' || indentStyle === 'space') {
341
+ options.useTabs = indentStyle === 'tab';
342
+ }
343
+ // Measured divergence, left deliberately: with no `indent_style` the CLI
344
+ // ignores `indent_size` and uses `tab_width` (default 2), while this follows
345
+ // the spec and honours `indent_size`. Matching the quirk would drop a width
346
+ // the user asked for. With `indent_style` set - nearly every file - the two
347
+ // agree.
348
+ const indentSize = properties['indent_size'] ?? properties['tab_width'];
349
+ if (indentSize === 'tab') {
350
+ options.useTabs = true;
351
+ }
352
+ else if (indentSize && /^\d+$/.test(indentSize)) {
353
+ options.tabWidth = Number(indentSize);
354
+ }
355
+ const maxLineLength = properties['max_line_length'];
356
+ if (maxLineLength && /^\d+$/.test(maxLineLength)) {
357
+ options.printWidth = Number(maxLineLength);
358
+ }
359
+ const quoteType = properties['quote_type'];
360
+ if (quoteType === 'single' || quoteType === 'double') {
361
+ options.singleQuote = quoteType === 'single';
362
+ }
363
+ const endOfLine = properties['end_of_line'];
364
+ if (endOfLine === 'lf' || endOfLine === 'crlf' || endOfLine === 'cr') {
365
+ options.endOfLine = endOfLine;
366
+ }
367
+ const insertFinalNewline = properties['insert_final_newline'];
368
+ if (insertFinalNewline === 'true' || insertFinalNewline === 'false') {
369
+ options.insertFinalNewline = insertFinalNewline === 'true';
370
+ }
371
+ return options;
372
+ }
373
+ /**
374
+ * `overrides` and `ignorePatterns` are config-file schema, not the
375
+ * `FormatConfig` the programmatic API takes - `format()` would silently drop
376
+ * them, so a generator would diverge from `nx format`. Split out here and
377
+ * applied per file by `formatFilesWithOxfmt`.
378
+ */
379
+ function splitOxfmtConfig(config) {
380
+ if (config === undefined) {
381
+ return {};
382
+ }
383
+ // Measured: `123` / `"x"` / `[]` / `null` / `true` each exit 1 with
384
+ // "invalid type: ... expected struct Oxfmtrc". Returning `{}` would instead
385
+ // format on bare defaults - the divergence the `error` arm prevents. `[]`
386
+ // needs its own check since `typeof [] === 'object'`. `config` stays
387
+ // `unknown` so the guard does not look dead.
388
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
389
+ return { error: 'the config must be an object' };
390
+ }
391
+ const { overrides: rawOverrides, ignorePatterns: rawIgnorePatterns, ...options } = config;
392
+ // oxfmt reads an explicit `null` as an absent key and formats without
393
+ // complaint, so it is normalised rather than rejected - erroring here would
394
+ // skip the whole batch over a config `nx format` accepts.
395
+ const overrides = rawOverrides ?? undefined;
396
+ const ignorePatterns = rawIgnorePatterns ?? undefined;
397
+ // Shapes oxfmt refuses to load are reported rather than dropped. Quietly
398
+ // ignoring them would leave the batch formatting past exclusions the config
399
+ // asked for, while `nx format` on the same workspace fails outright.
400
+ if (ignorePatterns !== undefined &&
401
+ (!Array.isArray(ignorePatterns) ||
402
+ ignorePatterns.some((pattern) => typeof pattern !== 'string'))) {
403
+ return { error: '"ignorePatterns" must be an array of strings' };
404
+ }
405
+ if (overrides !== undefined &&
406
+ (!Array.isArray(overrides) ||
407
+ overrides.some((override) => override === null ||
408
+ typeof override !== 'object' ||
409
+ // `files` is required by oxfmt, not merely typed: an override that
410
+ // omits it fails the whole config with "missing field `files`".
411
+ override.files === undefined ||
412
+ !isGlobSet(override.files) ||
413
+ !isGlobSet(override.excludeFiles)))) {
414
+ return {
415
+ error: '"overrides" must be an array of { files, excludeFiles } string arrays, each with "files"',
416
+ };
417
+ }
418
+ return {
419
+ options,
420
+ ignorePatterns,
421
+ overrides: Array.isArray(overrides)
422
+ ? overrides.map((override) => {
423
+ // oxfmt matches these against paths relative to the config file,
424
+ // which for a workspace batch is the workspace root.
425
+ const include = compileGlobSet(override?.files);
426
+ const exclude = compileGlobSet(override?.excludeFiles);
427
+ return {
428
+ matches: (filePath) => include(filePath) && !exclude(filePath),
429
+ options: (override?.options ?? {}),
430
+ };
431
+ })
432
+ : undefined,
433
+ };
434
+ }
435
+ /**
436
+ * oxfmt's `GlobSet` is `string[]`. Absent passes: it is legal for
437
+ * `excludeFiles`, and `files` is checked separately by its caller.
438
+ */
439
+ function isGlobSet(globs) {
440
+ return (globs === undefined ||
441
+ (Array.isArray(globs) && globs.every((g) => typeof g === 'string')));
442
+ }
443
+ function compileGlobSet(globs) {
444
+ // Not lenient about a bare string, though prettier allows one: oxfmt's type
445
+ // is `GlobSet = string[]` and its CLI rejects the config with "invalid type:
446
+ // string, expected a sequence". Accepting it would apply an override that
447
+ // `nx format` refuses to run.
448
+ if (!Array.isArray(globs) || globs.length === 0) {
449
+ return () => false;
450
+ }
451
+ const matchers = globs.map((glob) => {
452
+ // oxfmt lifts a separator-less pattern to any depth and reads `./` as
453
+ // anchored to the config's directory; minimatch does neither, so `*.md`
454
+ // would match only at the root - the shape `oxfmt migrate-prettier` emits.
455
+ // See GlobSet::new in crates/oxc_config/src/glob_set.rs.
456
+ const lifted = glob.startsWith('./')
457
+ ? glob.slice(2)
458
+ : glob.includes('/')
459
+ ? glob
460
+ : `**/${glob}`;
461
+ // Under negation oxfmt collapses a *single* leading globstar to one segment:
462
+ // `!**/t.ts` selects as `!*/t.ts`, where minimatch's zero-or-more `**` would
463
+ // exclude every `t.ts`. Interior and doubled leading globstars already agree
464
+ // - measured against 0.60.0, so rewriting those would introduce divergence.
465
+ const pattern = lifted.startsWith('!**/') && !lifted.startsWith('!**/**/')
466
+ ? `!*/${lifted.slice(4)}`
467
+ : lifted;
468
+ // minimatch's `!` handling is left on: oxfmt normalizes then matches with
469
+ // fast-glob, which also treats leading `!` as inversion. A separator-less
470
+ // `!*.ts` is lifted above, so its `!` stops being leading and neither
471
+ // inverts.
472
+ return new minimatch_1.Minimatch(pattern, { dot: true });
473
+ });
474
+ return (filePath) => matchers.some((matcher) => matcher.match(filePath));
475
+ }
476
+ /**
477
+ * Options from every override matching this file; later ones win. Globs are
478
+ * rooted at the workspace, so a path outside it matches nothing.
479
+ */
480
+ function overrideOptionsForFile(overrides, filePath) {
481
+ if (!overrides?.length || filePath === undefined) {
482
+ return undefined;
483
+ }
484
+ let options;
485
+ for (const override of overrides) {
486
+ if (override.matches(filePath)) {
487
+ options = { ...options, ...override.options };
488
+ }
489
+ }
490
+ return options;
491
+ }
492
+ /**
493
+ * Reimplements oxfmt's own config resolution because `format()` discovers
494
+ * nothing. Tracked at https://github.com/oxc-project/oxc/issues/19922.
495
+ *
496
+ * The nearest config at or above the file's directory wins and *replaces* the
497
+ * one above rather than merging - measured against oxfmt 0.60.0, so merging
498
+ * here would format differently from `nx format:write`.
499
+ *
500
+ * `seedConfig` is a root config that exists only in the tree, so it outranks
501
+ * disk. JSON only - a TypeScript form is reported rather than formatted against
502
+ * whatever it is replacing.
503
+ * There is no JavaScript branch - oxfmt does not discover `oxfmt.config.js`.
504
+ */
505
+ async function resolveOxfmtConfigInDir(dir, workspaceRoot, seedConfig, rootConfigNames) {
506
+ const isRoot = path.resolve(dir) === path.resolve(workspaceRoot);
507
+ // oxfmt defines no precedence between two config files in one directory and
508
+ // refuses to run, so choosing one here would format against a config the next
509
+ // `nx format:write` rejects outright. A seed shares the flushed directory
510
+ // with whatever is on disk, and replaces only the name it matches.
511
+ //
512
+ // `rootConfigNames` is the root's post-flush state, which disk cannot see: a
513
+ // staged config is not there yet, and a deleted one still is.
514
+ const candidates = isRoot && rootConfigNames
515
+ ? rootConfigNames
516
+ : exports.oxfmtConfigFiles.filter((name) => (0, node_fs_1.existsSync)(path.join(dir, name)) ||
517
+ (isRoot && seedConfig?.name === name));
518
+ if (candidates.length > 1) {
519
+ return {
520
+ error: `Both '${candidates[0]}' and '${candidates[1]}' found in ${path.relative(workspaceRoot, dir) || '.'} - oxfmt does not define which one wins.`,
521
+ };
522
+ }
523
+ // The seed previews JSON only. Nx generates `.oxfmtrc.json` and never a
524
+ // TypeScript config, so a TS name here means some other generator staged one
525
+ // - and evaluating it means importing it, which tree content cannot stand in
526
+ // for. An older copy on disk is not a fallback: it would format against the
527
+ // options being replaced.
528
+ if (seedConfig && isRoot && !isJsonOxfmtConfig(seedConfig.name)) {
529
+ return {
530
+ error: `Cannot read ${seedConfig.name} before it is written to disk. Use a JSON oxfmt config, or format after the generator has finished.`,
531
+ };
532
+ }
533
+ // The seed is the root config the generator just created, so it only stands
534
+ // in for a config at the root itself.
535
+ if (seedConfig && isJsonOxfmtConfig(seedConfig.name) && isRoot) {
536
+ try {
537
+ return splitOxfmtConfig((0, json_1.parseJson)(seedConfig.content));
538
+ }
539
+ catch (e) {
540
+ return { error: `Could not read ${seedConfig.name}: ${e.message}` };
541
+ }
542
+ }
543
+ // `candidates`, not every supported name: at the root a config the tree
544
+ // deletes is still on disk, and reading it would format against a file the
545
+ // flush is about to remove.
546
+ for (const name of candidates) {
547
+ const configPath = path.join(dir, name);
548
+ if (!(0, node_fs_1.existsSync)(configPath)) {
549
+ continue;
550
+ }
551
+ try {
552
+ if (isJsonOxfmtConfig(name)) {
553
+ return splitOxfmtConfig((0, json_1.parseJson)((0, node_fs_1.readFileSync)(configPath, 'utf-8')));
554
+ }
555
+ // Every discovered name is JSON above or TypeScript here - there is no
556
+ // JavaScript branch, because oxfmt does not discover `oxfmt.config.js`.
557
+ const loaded = (await loadTsOxfmtConfig(configPath));
558
+ return splitOxfmtConfig(loaded?.default ?? loaded);
559
+ }
560
+ catch (e) {
561
+ // Unlike the CLI, oxfmt never sees this file - it is handed options in
562
+ // memory - so nothing else will report that the config is unusable.
563
+ return {
564
+ error: `Could not read ${path.relative(workspaceRoot, configPath)}: ${e.message}`,
565
+ };
566
+ }
567
+ }
568
+ return undefined;
569
+ }
570
+ function createOxfmtConfigResolver(workspaceRoot, seedConfig, rootConfigNames) {
571
+ const cache = new Map();
572
+ const resolve = async (fileDir) => {
573
+ for (const dir of ancestorsWithin(workspaceRoot, fileDir)) {
574
+ const config = await resolveOxfmtConfigInDir(dir, workspaceRoot, seedConfig, rootConfigNames);
575
+ if (config) {
576
+ return {
577
+ ...config,
578
+ dir,
579
+ ignoreMatcher: config.ignorePatterns?.length
580
+ ? ignore().add(config.ignorePatterns)
581
+ : undefined,
582
+ };
583
+ }
584
+ }
585
+ return { dir: workspaceRoot };
586
+ };
587
+ return (fileDir) => {
588
+ const key = path.resolve(fileDir);
589
+ let pending = cache.get(key);
590
+ if (!pending) {
591
+ pending = resolve(key);
592
+ cache.set(key, pending);
593
+ }
594
+ return pending;
595
+ };
596
+ }
597
+ /**
598
+ * `dir` and every directory between it and `workspaceRoot`, nearest first.
599
+ * Yields nothing when `dir` is outside the workspace.
600
+ */
601
+ function* ancestorsWithin(workspaceRoot, dir) {
602
+ const root = path.resolve(workspaceRoot);
603
+ let current = path.resolve(dir);
604
+ if (current !== root && !current.startsWith(root + path.sep)) {
605
+ return;
606
+ }
607
+ while (true) {
608
+ yield current;
609
+ if (current === root) {
610
+ return;
611
+ }
612
+ const parent = path.dirname(current);
613
+ // `dirname` of a filesystem root returns itself; without this a path that
614
+ // somehow escaped the check above would spin forever.
615
+ if (parent === current) {
616
+ return;
617
+ }
618
+ current = parent;
619
+ }
620
+ }
621
+ /**
622
+ * `ignore` rejects anything not already relative, and callers pass both
623
+ * workspace-relative and absolute paths.
624
+ *
625
+ * Undefined for a path outside the workspace root, including the root
626
+ * itself; the caller reads that as "no ignore rules apply". On Windows a
627
+ * path on another drive comes back looking relative (`D:/...`) - wrong
628
+ * rather than undefined, but no shipped caller does that.
629
+ */
630
+ function toRelativeWithin(baseDir, filePath) {
631
+ const relative = path
632
+ .relative(baseDir, path.resolve(baseDir, filePath))
633
+ .split(path.sep)
634
+ .join('/');
635
+ return relative && !relative.startsWith('../') ? relative : undefined;
636
+ }
637
+ /**
638
+ * The batch's own oxfmt config, by discovery order.
639
+ *
640
+ * Any discovered name is returned; only the JSON form is honoured *as a seed*,
641
+ * and `resolveOxfmtConfigInDir` is what gates that. A `.ts` config still loads
642
+ * normally when it is found on disk.
643
+ */
644
+ function findOxfmtConfigInBatch(files) {
645
+ for (const name of exports.oxfmtConfigFiles) {
646
+ const match = files.find((file) => file.path === name);
647
+ if (match) {
648
+ return { name, content: match.content };
649
+ }
650
+ }
651
+ return undefined;
652
+ }
653
+ /**
654
+ * Nothing is written to disk: staging files inside the workspace would race the
655
+ * daemon's watcher and the project graph mid-generator.
656
+ *
657
+ * A path is absent from the result when oxfmt has no parser for it, an ignore
658
+ * file covers it, or it is already formatted. One unparseable file fails only
659
+ * itself; the rest of the batch still applies.
660
+ */
661
+ async function formatFilesWithOxfmt(files, workspaceRoot, seedConfig,
662
+ // Every supported config name the root holds once the tree is flushed. Only a
663
+ // Tree-holding caller knows this; without it the root is resolved from disk.
664
+ rootConfigNames,
665
+ // Reads a workspace-relative file, `null` when it does not exist. A
666
+ // tree-holding caller passes a tree-backed one so ignore files and the root
667
+ // `.editorconfig` are read as they will be after the flush; everyone else
668
+ // gets disk.
669
+ read) {
670
+ const formatted = new Map();
671
+ if (files.length === 0) {
672
+ return { formatted };
673
+ }
674
+ const { format } = await loadOxfmtModule();
675
+ // Resolved from the file's own directory upwards, as the CLI does, cached
676
+ // per directory. A config in the batch is the freshest there is, and for a
677
+ // tree-holding caller may be the only copy. Callers that know which file
678
+ // that is still pass it; this is the fallback.
679
+ const resolveConfig = createOxfmtConfigResolver(workspaceRoot, seedConfig ?? findOxfmtConfigInBatch(files), rootConfigNames);
680
+ // The filenames and the merge rule come from the shared constant so the two
681
+ // consumers cannot drift; the cascade axis is the ancestor-aware checker
682
+ // itself, which both go through. oxfmt honours `.prettierignore` as well as
683
+ // `.gitignore` (measured against its CLI), keeping one matcher per file so a
684
+ // `!` in one cannot re-include what the other excluded.
685
+ // `readFileIfExisting` is not usable here: it returns '' for a missing file,
686
+ // which an empty ignore file also returns.
687
+ const resolveIgnores = (0, ignore_1.createIgnoreChainResolver)(read ??
688
+ ((relativePath) => {
689
+ const absolute = path.join(workspaceRoot, relativePath);
690
+ return (0, node_fs_1.existsSync)(absolute) ? (0, node_fs_1.readFileSync)(absolute, 'utf-8') : null;
691
+ }), ignore_1.OXFMT_IGNORE_OPTIONS.filenames, ignore_1.OXFMT_IGNORE_OPTIONS.merge);
692
+ const { isIgnoredFile } = (0, ignore_1.createAncestorAwareIgnoreChecker)(resolveIgnores);
693
+ // Measured against the CLI: it resolves `.editorconfig` from the directory it
694
+ // runs in and never reads a nested one, unlike the per-file walk it does for
695
+ // `.oxfmtrc.json`. Honouring a nearer file here would be undone by the next
696
+ // `nx format:write`. The root's own file goes through `read`, so one a
697
+ // generator staged (or deleted) is honoured as it will be after the flush.
698
+ let editorConfigChain;
699
+ try {
700
+ editorConfigChain = editorConfigChainFor(workspaceRoot, read);
701
+ }
702
+ catch (e) {
703
+ // Resolved once for the batch, so this sits outside the per-file catch.
704
+ // Bare defaults would format to widths `nx format` does not, so an
705
+ // unreadable file fails every file rather than being formatted without it.
706
+ return { formatted, errors: files.map((f) => `${f.path}: ${e.message}`) };
707
+ }
708
+ const errors = [];
709
+ await Promise.all(files.map(async (file) => {
710
+ try {
711
+ const absolutePath = path.resolve(workspaceRoot, file.path);
712
+ const fileDir = path.dirname(absolutePath);
713
+ const config = await resolveConfig(fileDir);
714
+ if (config.error) {
715
+ // An unreadable config costs the workspace's style *and* its
716
+ // `ignorePatterns`, so formatting on bare defaults would rewrite files the
717
+ // config asks to skip - and `tree.write` is not undone by a warning. Only
718
+ // files under that config are skipped.
719
+ errors.push(config.error);
720
+ return;
721
+ }
722
+ // Inside the try: `ignores()` throws on a path it considers non-relative,
723
+ // and an unhandled rejection would discard the whole batch's formatting. A
724
+ // path outside the workspace cannot be covered by its ignore files.
725
+ const relativePath = toRelativeWithin(workspaceRoot, file.path);
726
+ if (relativePath !== undefined &&
727
+ isIgnored(isIgnoredFile, relativePath, config, absolutePath)) {
728
+ return;
729
+ }
730
+ // Overrides are globs in the config's own file, so they match relative
731
+ // to wherever that config was found rather than to the workspace root.
732
+ const relativeToConfig = toRelativeWithin(config.dir, absolutePath);
733
+ const result = await format(absolutePath, file.content, {
734
+ // Precedence runs .editorconfig < the config's own options < a
735
+ // matching override, which is the order the CLI resolves them in.
736
+ ...editorConfigOptionsForFile(editorConfigChain, absolutePath),
737
+ ...config.options,
738
+ ...overrideOptionsForFile(config.overrides, relativeToConfig),
739
+ });
740
+ if (result.errors?.length) {
741
+ // Most changed files have no oxfmt parser; those are skipped rather than
742
+ // reported, matching the CLI's --no-error-on-unmatched-pattern. Every
743
+ // diagnostic is read, not just [0]: an `Unsupported file type` entry can
744
+ // precede a real one.
745
+ for (const failure of result.errors) {
746
+ if (failure.message.startsWith(UNSUPPORTED_FILE_TYPE)) {
747
+ continue;
748
+ }
749
+ // `message` alone is context-free ("Unexpected token"); the path
750
+ // and line live in the codeframe.
751
+ errors.push(`${file.path}: ${failure.codeframe?.trim() || failure.message}`);
752
+ }
753
+ return;
754
+ }
755
+ if (result.code !== file.content) {
756
+ formatted.set(file.path, result.code);
757
+ }
758
+ }
759
+ catch (e) {
760
+ errors.push(`${file.path}: ${e.message}`);
761
+ }
762
+ }));
763
+ return { formatted, errors: errors.length > 0 ? errors : undefined };
764
+ }