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.
- package/dist/src/ai/set-up-ai-agents/set-up-ai-agents.js +2 -2
- package/dist/src/command-line/format/format.js +84 -101
- package/dist/src/command-line/import/import.js +12 -0
- package/dist/src/command-line/init/configure-plugins.js +4 -1
- package/dist/src/command-line/init/implementation/add-nx-to-nest.js +9 -0
- package/dist/src/command-line/init/implementation/add-nx-to-turborepo.js +3 -17
- package/dist/src/command-line/init/implementation/angular/index.js +4 -0
- package/dist/src/command-line/init/implementation/angular/legacy-angular-versions.js +2 -0
- package/dist/src/command-line/init/implementation/angular/standalone-workspace.js +8 -2
- package/dist/src/command-line/init/implementation/check-compatible-with-plugins.js +2 -0
- package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +5 -0
- package/dist/src/command-line/init/implementation/format.d.ts +18 -0
- package/dist/src/command-line/init/implementation/format.js +114 -0
- package/dist/src/command-line/init/implementation/utils.js +8 -0
- package/dist/src/command-line/init/init-v1.js +7 -0
- package/dist/src/command-line/init/init-v2.js +11 -0
- package/dist/src/command-line/migrate/migrate.d.ts +4 -0
- package/dist/src/command-line/migrate/migrate.js +64 -24
- package/dist/src/command-line/release/version.js +2 -2
- package/dist/src/daemon/client/client.js +11 -2
- package/dist/src/devkit-internals.d.ts +5 -2
- package/dist/src/devkit-internals.js +14 -6
- package/dist/src/generators/internal-utils/{format-changed-files-with-prettier-if-available.d.ts → format-changed-files.d.ts} +6 -6
- package/dist/src/generators/internal-utils/format-changed-files.js +163 -0
- package/dist/src/generators/testing-utils/create-tree-with-empty-workspace.d.ts +15 -0
- package/dist/src/generators/testing-utils/create-tree-with-empty-workspace.js +21 -3
- package/dist/src/migrations/update-15-0-0/prefix-outputs.js +2 -2
- package/dist/src/migrations/update-16-0-0/remove-nrwl-cli.js +2 -2
- package/dist/src/migrations/update-16-0-0/update-depends-on-to-tokens.js +2 -2
- package/dist/src/migrations/update-16-0-0/update-nx-cloud-runner.js +2 -2
- package/dist/src/migrations/update-16-2-0/remove-run-commands-output-path.js +2 -2
- package/dist/src/migrations/update-17-0-0/rm-default-collection-npm-scope.js +2 -2
- package/dist/src/migrations/update-17-0-0/use-minimal-config-for-tasks-runner-options.js +2 -2
- package/dist/src/migrations/update-17-2-0/move-default-base.js +2 -2
- package/dist/src/migrations/update-18-0-0/disable-crystal-for-existing-workspaces.js +2 -2
- package/dist/src/migrations/update-19-2-4/set-project-name.js +2 -2
- package/dist/src/migrations/update-20-0-0/move-use-daemon-process.js +2 -2
- package/dist/src/migrations/update-20-0-1/use-legacy-cache.js +2 -2
- package/dist/src/migrations/update-21-0-0/release-changelog-config-changes.js +2 -2
- package/dist/src/migrations/update-21-0-0/release-version-config-changes.js +2 -2
- package/dist/src/migrations/update-21-0-0/remove-custom-tasks-runner.js +2 -2
- package/dist/src/migrations/update-21-0-0/remove-legacy-cache.js +2 -2
- package/dist/src/migrations/update-22-0-0/consolidate-release-tag-config.js +2 -2
- package/dist/src/migrations/update-22-0-0/release-version-config-changes.js +2 -2
- package/dist/src/migrations/update-22-2-0/add-self-healing-to-gitignore.js +2 -2
- package/dist/src/migrations/update-22-6-0/add-claude-settings-local-to-git-ignore.js +2 -2
- package/dist/src/migrations/update-22-6-0/add-claude-worktrees-to-git-ignore.js +2 -2
- package/dist/src/migrations/update-22-7-0/add-polygraph-to-git-ignore.js +2 -2
- package/dist/src/migrations/update-23-0-0/add-migrate-runs-to-git-ignore.js +2 -2
- package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +2 -2
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.js +3 -3
- package/dist/src/utils/chunkify.d.ts +7 -1
- package/dist/src/utils/chunkify.js +10 -3
- package/dist/src/utils/formatters/index.d.ts +13 -0
- package/dist/src/utils/formatters/index.js +74 -0
- package/dist/src/utils/formatters/oxfmt.d.ts +32 -0
- package/dist/src/utils/formatters/oxfmt.js +764 -0
- package/dist/src/utils/formatters/prettier.d.ts +26 -0
- package/dist/src/utils/formatters/prettier.js +201 -0
- package/dist/src/utils/formatters/shared.d.ts +5 -0
- package/dist/src/utils/formatters/shared.js +8 -0
- package/dist/src/utils/ignore.d.ts +47 -5
- package/dist/src/utils/ignore.js +93 -11
- package/dist/src/utils/write-formatted-json-file.d.ts +3 -2
- package/dist/src/utils/write-formatted-json-file.js +5 -4
- package/package.json +11 -11
- package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.js +0 -88
- package/dist/src/utils/is-using-prettier.d.ts +0 -3
- 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
|
+
}
|