@isentinel/eslint-config 6.1.0 → 6.2.0
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/README.md +98 -0
- package/dist/cli.mjs +231 -30
- package/dist/index.d.mts +76 -35
- package/dist/index.mjs +781 -197
- package/dist/lint-cli.mjs +234 -114
- package/dist/oxfmt.d.mts +132 -0
- package/dist/oxfmt.mjs +722 -0
- package/dist/oxlint.d.mts +14 -0
- package/dist/oxlint.mjs +440 -45
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -399,6 +399,7 @@ or `--`.
|
|
|
399
399
|
| `--oxlint-args "<args>"` | Extra arguments forwarded verbatim to oxlint. |
|
|
400
400
|
| `--files0-from <file\|->` | Read the paths to lint from a NUL-delimited UTF-8 list (`-` for stdin); an empty list exits 0. |
|
|
401
401
|
| `--print` | Print the composed commands without running them. |
|
|
402
|
+
| `--no-oxfmt` | Skip the oxfmt pass that runs when the project has an `oxfmt.config.*` file (see [Standalone oxfmt](#standalone-oxfmt)). |
|
|
402
403
|
| `-- <args>` | Forward args to the single selected tool (needs `--eslint`/`--oxlint`). |
|
|
403
404
|
| `-h`, `--help` | Show help. |
|
|
404
405
|
| `-v`, `--version` | Show the version. |
|
|
@@ -949,6 +950,103 @@ pnpm i -D oxlint oxlint-tsgolint
|
|
|
949
950
|
See [docs/oxlint.md](./docs/oxlint.md) for the per-rule mapping, what stays in
|
|
950
951
|
ESLint and why, and migration notes.
|
|
951
952
|
|
|
953
|
+
#### Standalone oxfmt
|
|
954
|
+
|
|
955
|
+
By default, oxfmt runs inside the linter as the `oxfmt/oxfmt` rule. To run it as
|
|
956
|
+
its own tool (`oxfmt`, editor format-on-save through the oxc extension), set
|
|
957
|
+
`formatter: "standalone"` in both factories and add an `oxfmt.config.ts` built
|
|
958
|
+
from the preset:
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
// oxfmt.config.ts
|
|
962
|
+
import { isentinelOxfmt } from "@isentinel/eslint-config/oxfmt";
|
|
963
|
+
|
|
964
|
+
export default isentinelOxfmt({ cwd: import.meta.dirname, printWidth: 100 });
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
// eslint.config.ts
|
|
969
|
+
import isentinel from "@isentinel/eslint-config";
|
|
970
|
+
|
|
971
|
+
export default isentinel({
|
|
972
|
+
formatter: "standalone",
|
|
973
|
+
});
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
`isentinelOxfmt` starts from the same options the linter uses (preset defaults,
|
|
977
|
+
then `.editorconfig`, then `.prettierrc`). Each key you pass replaces its base
|
|
978
|
+
value, except `ignorePatterns` and `overrides`, which are appended to the
|
|
979
|
+
preset's. The override order is the one the `oxfmt/oxfmt` rule uses: the
|
|
980
|
+
preset's Markdown, YAML and JSON5 overrides, then the `.editorconfig` sections
|
|
981
|
+
(without keys you set at the root), then yours, then the `.prettierrc`
|
|
982
|
+
`overrides`. Override globs without a slash are anchored to the config directory
|
|
983
|
+
(`*.json` becomes `./*.json`), because the rule matches them only there. `cwd`
|
|
984
|
+
is the directory to read `.prettierrc` and `.editorconfig` from (default
|
|
985
|
+
`process.cwd()`). Pass `import.meta.dirname`, as the wizard does, so the result
|
|
986
|
+
does not depend on where oxfmt runs.
|
|
987
|
+
|
|
988
|
+
`isentinelOxfmt` takes `formatters` and `componentExts`, with the same meaning
|
|
989
|
+
as in the factories, so oxfmt formats the same files the linter would. TOML,
|
|
990
|
+
`.mdx` and other files the linter does not format are ignored.
|
|
991
|
+
|
|
992
|
+
In standalone mode, oxfmt never sees the linter's options. The factories warn
|
|
993
|
+
once per lint run when you set any of these, because they do not change
|
|
994
|
+
formatting: `formatters` (any value except `true`), `componentExts`, `ignores`,
|
|
995
|
+
or `gitignore` (`false` or custom files). Set the equivalent in `isentinelOxfmt`
|
|
996
|
+
instead (`formatters`, `componentExts`, `ignorePatterns`, and format options).
|
|
997
|
+
`stylistic: false` turns off the stylistic lint rules but not oxfmt. The rule
|
|
998
|
+
widths also come from the oxfmt config only, not from
|
|
999
|
+
`formatters.prettierOptions` or `formatters.oxfmtOptions`. The setup wizard
|
|
1000
|
+
copies migrated `.eslintignore` patterns to both configs.
|
|
1001
|
+
|
|
1002
|
+
With `formatter: "standalone"`, the linter enables `oxfmt/oxfmt` nowhere but
|
|
1003
|
+
keeps the rules that turn off formatting conflicts. Lua formatting (Stylua)
|
|
1004
|
+
stays in ESLint, because oxfmt does not format Lua. The setup wizard can write
|
|
1005
|
+
all of this for you.
|
|
1006
|
+
|
|
1007
|
+
`isentinel-lint` runs `oxfmt --check` next to the linters whenever it finds an
|
|
1008
|
+
`oxfmt.config.ts` or `oxfmt.config.mts`, and `oxfmt --write` last with `--fix`.
|
|
1009
|
+
It does this whatever the `formatter` option is, so set
|
|
1010
|
+
`formatter: "standalone"` with such a file, or the files are formatted twice.
|
|
1011
|
+
Pass `--no-oxfmt` to skip the pass.
|
|
1012
|
+
|
|
1013
|
+
Both factories read the oxfmt config that oxfmt itself uses: `.oxfmtrc.json`,
|
|
1014
|
+
`.oxfmtrc.jsonc`, `oxfmt.config.ts` or `oxfmt.config.mts`, in the nearest
|
|
1015
|
+
directory from the working directory up to the filesystem root. More than one of
|
|
1016
|
+
them in one directory is an error, as in oxfmt. Its root options (without
|
|
1017
|
+
`ignorePatterns` and `overrides`) override `formatters.prettierOptions`, and
|
|
1018
|
+
`formatters.oxfmtOptions` overrides them.
|
|
1019
|
+
|
|
1020
|
+
Nested oxfmt configs (a config file in a subdirectory) are not used, in either
|
|
1021
|
+
mode. One root config formats the whole project: the `oxfmt/oxfmt` rule sets
|
|
1022
|
+
`disableNestedConfig`, `isentinel-lint` passes `--config`, the wizard's format
|
|
1023
|
+
scripts pass `--disable-nested-config`, and its VS Code settings set
|
|
1024
|
+
`oxc.fmt.disableNestedConfig`. Use `overrides` for per-folder options. The same
|
|
1025
|
+
holds for `.editorconfig`: only the nearest one at or above the root config is
|
|
1026
|
+
read, so a `.editorconfig` in a subdirectory does not change formatting.
|
|
1027
|
+
|
|
1028
|
+
`flawless/arrow-return-style` uses two widths. `maxLen` is your limit: an arrow
|
|
1029
|
+
becomes implicit only if it fits. It comes from `stylistic.maxLen`, else
|
|
1030
|
+
`printWidth`, and it can be shorter than the formatter width on purpose.
|
|
1031
|
+
`useOxfmt.printWidth` is the width the rule gives oxfmt to predict its output.
|
|
1032
|
+
It always comes from the root `printWidth` of the oxfmt config, so that `--fix`
|
|
1033
|
+
agrees with oxfmt. An override that changes `printWidth` or `tabWidth` for JS or
|
|
1034
|
+
TS files gets a warning, because the linter uses the root values. In Markdown
|
|
1035
|
+
code blocks, `useOxfmt` uses the Markdown widths instead, because oxfmt formats
|
|
1036
|
+
embedded code with the Markdown options. Each oxfmt, `.editorconfig` or
|
|
1037
|
+
`.prettierrc` override that changes the Markdown widths reaches only the code
|
|
1038
|
+
blocks of the files it matches. The comment length rules take their `tabSize`
|
|
1039
|
+
from the same oxfmt widths.
|
|
1040
|
+
|
|
1041
|
+
The stylistic `indent` and `quotes`, and the JSON bracket spacing, follow the
|
|
1042
|
+
oxfmt options (`useTabs`, `tabWidth`, `singleQuote`, `bracketSpacing`) unless
|
|
1043
|
+
you set them in `stylistic`.
|
|
1044
|
+
|
|
1045
|
+
Both modes write the same output, with one known difference: when one import
|
|
1046
|
+
merges into another (`import/no-duplicates`), eslint mode sorts the imports
|
|
1047
|
+
before the merge and standalone mode after it, so comments above the merged
|
|
1048
|
+
import can end in a different order.
|
|
1049
|
+
|
|
952
1050
|
#### Type-Aware Split
|
|
953
1051
|
|
|
954
1052
|
On large projects the type-aware rules dominate ESLint wall time; an explicit
|
package/dist/cli.mjs
CHANGED
|
@@ -1,15 +1,89 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { cancel, confirm, group, intro, log, multiselect, note, outro, select } from "@clack/prompts";
|
|
2
3
|
import ansis from "ansis";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import yargs from "yargs";
|
|
5
6
|
import { hideBin } from "yargs/helpers";
|
|
7
|
+
import "find-up-simple";
|
|
8
|
+
import "jsonc-eslint-parser";
|
|
9
|
+
import "local-pkg";
|
|
6
10
|
import fs from "node:fs";
|
|
7
|
-
import fsp from "node:fs/promises";
|
|
8
11
|
import path from "node:path";
|
|
12
|
+
import "semver";
|
|
13
|
+
import fsp from "node:fs/promises";
|
|
9
14
|
import { existsSync, readFileSync } from "fs";
|
|
10
15
|
import { execSync } from "node:child_process";
|
|
16
|
+
//#region src/guards.ts
|
|
17
|
+
/**
|
|
18
|
+
* Internal runtime type guards. Prefer these over `as` assertions so values
|
|
19
|
+
* crossing untyped boundaries (`JSON.parse`, dynamic `import`, plugin objects)
|
|
20
|
+
* are validated at runtime rather than asserted away.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Whether a value is a non-null, non-array object usable as a string-keyed
|
|
24
|
+
* record. Narrows `unknown` without an assertion.
|
|
25
|
+
*
|
|
26
|
+
* @param value - The value to test.
|
|
27
|
+
* @returns Whether the value is a plain object.
|
|
28
|
+
*/
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether a value is an array whose every element is a string.
|
|
34
|
+
*
|
|
35
|
+
* @param value - The value to test.
|
|
36
|
+
* @returns Whether the value is a `string` array.
|
|
37
|
+
*/
|
|
38
|
+
function isStringArray(value) {
|
|
39
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
40
|
+
}
|
|
41
|
+
createRequire(import.meta.url);
|
|
42
|
+
environmentMatcher("PATH", /\.pi[/\\]agent/), environmentMatcher("EDITOR", /devin/), environmentMatcher("TERM_PROGRAM", /kiro/, { noTty: true });
|
|
43
|
+
/**
|
|
44
|
+
* Build a probe that regex-tests one environment variable, for the agents that
|
|
45
|
+
* announce themselves inside a variable a human also sets.
|
|
46
|
+
*
|
|
47
|
+
* `noTty` marks a variable an interactive user shares with the agent (kiro's
|
|
48
|
+
* `TERM_PROGRAM` is set in its terminal too): an attached stdout TTY means a
|
|
49
|
+
* person is watching, so the probe declines.
|
|
50
|
+
*
|
|
51
|
+
* @param name - The environment variable to test.
|
|
52
|
+
* @param pattern - The pattern the value must match.
|
|
53
|
+
* @param options - Probe options.
|
|
54
|
+
* @param options.noTty - Whether an attached stdout TTY disqualifies the match.
|
|
55
|
+
* @returns The probe.
|
|
56
|
+
*/
|
|
57
|
+
function environmentMatcher(name, pattern, { noTty = false } = {}) {
|
|
58
|
+
return (environment) => {
|
|
59
|
+
if (noTty && process.stdout.isTTY) return false;
|
|
60
|
+
const value = environment[name];
|
|
61
|
+
return value !== void 0 && pattern.test(value);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** JSON oxfmt config files oxfmt discovers. */
|
|
65
|
+
const OXFMT_RC_FILES = [".oxfmtrc.json", ".oxfmtrc.jsonc"];
|
|
66
|
+
/**
|
|
67
|
+
* Oxfmt config modules oxfmt discovers. It loads other module extensions only
|
|
68
|
+
* through `--config`, never by search.
|
|
69
|
+
*/
|
|
70
|
+
const OXFMT_CONFIG_MODULES = ["oxfmt.config.ts", "oxfmt.config.mts"];
|
|
71
|
+
/** The config module the standalone setup writes. */
|
|
72
|
+
const OXFMT_CONFIG_MODULE = OXFMT_CONFIG_MODULES[0];
|
|
73
|
+
/** Every file name oxfmt discovers. One directory may hold only one of them. */
|
|
74
|
+
const OXFMT_CONFIG_NAMES = [...OXFMT_RC_FILES, ...OXFMT_CONFIG_MODULES];
|
|
75
|
+
/**
|
|
76
|
+
* List the oxfmt config files in one directory, without walking up.
|
|
77
|
+
*
|
|
78
|
+
* @param directory - Directory to check.
|
|
79
|
+
* @returns The file names found, sorted.
|
|
80
|
+
*/
|
|
81
|
+
function oxfmtConfigNamesIn(directory) {
|
|
82
|
+
return OXFMT_CONFIG_NAMES.filter((name) => fs.existsSync(path.join(directory, name))).sort();
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
11
85
|
//#region package.json
|
|
12
|
-
var version = "6.
|
|
86
|
+
var version = "6.2.0";
|
|
13
87
|
var package_default = {
|
|
14
88
|
name: "@isentinel/eslint-config",
|
|
15
89
|
version,
|
|
@@ -43,6 +117,10 @@ var package_default = {
|
|
|
43
117
|
"default": "./dist/index.mjs"
|
|
44
118
|
},
|
|
45
119
|
"./formatter-agents": "./dist/formatter-agents.mjs",
|
|
120
|
+
"./oxfmt": {
|
|
121
|
+
"types": "./dist/oxfmt.d.mts",
|
|
122
|
+
"default": "./dist/oxfmt.mjs"
|
|
123
|
+
},
|
|
46
124
|
"./oxlint": {
|
|
47
125
|
"types": "./dist/oxlint.d.mts",
|
|
48
126
|
"default": "./dist/oxlint.mjs"
|
|
@@ -73,6 +151,7 @@ var package_default = {
|
|
|
73
151
|
"release": "bumpp -r",
|
|
74
152
|
"stub": "tsdown",
|
|
75
153
|
"test": "nr gen && vitest run",
|
|
154
|
+
"test:parity": "vitest run --config vitest.parity.config.ts",
|
|
76
155
|
"test:watch": "vitest",
|
|
77
156
|
"typecheck": "tsc --noEmit",
|
|
78
157
|
"validate:oxlint": "node scripts/validate-oxlint.ts",
|
|
@@ -122,6 +201,7 @@ var package_default = {
|
|
|
122
201
|
"file-entry-cache": "catalog:prod",
|
|
123
202
|
"find-up-simple": "catalog:prod",
|
|
124
203
|
"globals": "catalog:prod",
|
|
204
|
+
"jiti": "catalog:prod",
|
|
125
205
|
"jsonc-eslint-parser": "catalog:prod",
|
|
126
206
|
"local-pkg": "catalog:prod",
|
|
127
207
|
"oxfmt": "catalog:prod",
|
|
@@ -161,7 +241,6 @@ var package_default = {
|
|
|
161
241
|
"eslint-plugin-react-x": "catalog:peer",
|
|
162
242
|
"eslint-plugin-testing-library": "catalog:peer",
|
|
163
243
|
"eslint-typegen": "catalog:dev",
|
|
164
|
-
"jiti": "catalog:dev",
|
|
165
244
|
"oxlint": "catalog:peer",
|
|
166
245
|
"oxlint-tsgolint": "catalog:peer",
|
|
167
246
|
"parse-gitignore-ts": "catalog:dev",
|
|
@@ -208,11 +287,43 @@ var package_default = {
|
|
|
208
287
|
};
|
|
209
288
|
//#endregion
|
|
210
289
|
//#region src/cli/constants.ts
|
|
211
|
-
|
|
290
|
+
/** The VS Code extension that runs oxfmt. */
|
|
291
|
+
const OXC_VSCODE_EXTENSION = "oxc.oxc-vscode";
|
|
292
|
+
/** Languages oxfmt formats on save in the standalone setup. */
|
|
293
|
+
const OXFMT_LANGUAGES = [
|
|
294
|
+
"css",
|
|
295
|
+
"graphql",
|
|
296
|
+
"html",
|
|
297
|
+
"javascript",
|
|
298
|
+
"javascriptreact",
|
|
299
|
+
"json",
|
|
300
|
+
"jsonc",
|
|
301
|
+
"less",
|
|
302
|
+
"markdown",
|
|
303
|
+
"scss",
|
|
304
|
+
"typescript",
|
|
305
|
+
"typescriptreact",
|
|
306
|
+
"yaml"
|
|
307
|
+
];
|
|
308
|
+
const eslintFormatterSettings = `
|
|
212
309
|
// Disable the default formatter, use eslint instead
|
|
213
310
|
"prettier.enable": false,
|
|
214
311
|
"editor.formatOnSave": false,
|
|
215
|
-
|
|
312
|
+
`;
|
|
313
|
+
const standaloneFormatterSettings = `
|
|
314
|
+
// Format with oxfmt; eslint only lints
|
|
315
|
+
"prettier.enable": false,
|
|
316
|
+
"eslint.format.enable": false,
|
|
317
|
+
// One root oxfmt config, as the CLI and lint runs use
|
|
318
|
+
"oxc.fmt.disableNestedConfig": true,
|
|
319
|
+
${OXFMT_LANGUAGES.map((language) => {
|
|
320
|
+
return ` "[${language}]": {
|
|
321
|
+
"editor.defaultFormatter": "${OXC_VSCODE_EXTENSION}",
|
|
322
|
+
"editor.formatOnSave": true
|
|
323
|
+
},`;
|
|
324
|
+
}).join("\n")}
|
|
325
|
+
`;
|
|
326
|
+
const lintSettings = `
|
|
216
327
|
// Auto fix
|
|
217
328
|
"editor.codeActionsOnSave": {
|
|
218
329
|
"source.fixAll.eslint": "explicit",
|
|
@@ -246,6 +357,36 @@ const vscodeSettingsString = `
|
|
|
246
357
|
"luau"
|
|
247
358
|
]
|
|
248
359
|
`;
|
|
360
|
+
/**
|
|
361
|
+
* The VS Code settings the wizard adds, as the body of a JSON object.
|
|
362
|
+
*
|
|
363
|
+
* @param formatter - Where formatting runs.
|
|
364
|
+
* @returns The settings text.
|
|
365
|
+
*/
|
|
366
|
+
function vscodeSettingsString(formatter = "eslint") {
|
|
367
|
+
return `${formatter === "standalone" ? standaloneFormatterSettings : eslintFormatterSettings}${lintSettings}`;
|
|
368
|
+
}
|
|
369
|
+
const formatterOptions = [{
|
|
370
|
+
hint: "oxfmt runs as an ESLint rule",
|
|
371
|
+
label: "ESLint",
|
|
372
|
+
value: "eslint"
|
|
373
|
+
}, {
|
|
374
|
+
hint: `oxfmt runs as its own tool, with ${OXFMT_CONFIG_MODULE}`,
|
|
375
|
+
label: "Standalone oxfmt",
|
|
376
|
+
value: "standalone"
|
|
377
|
+
}];
|
|
378
|
+
/**
|
|
379
|
+
* The {@link OXFMT_CONFIG_MODULE} the standalone setup writes.
|
|
380
|
+
*
|
|
381
|
+
* @param ignorePatterns - Patterns oxfmt should skip, as in ESLint `ignores`.
|
|
382
|
+
* @returns The module source.
|
|
383
|
+
*/
|
|
384
|
+
function oxfmtConfigContent(ignorePatterns = []) {
|
|
385
|
+
return `import { isentinelOxfmt } from "@isentinel/eslint-config/oxfmt";
|
|
386
|
+
|
|
387
|
+
export default isentinelOxfmt({ cwd: import.meta.dirname${ignorePatterns.length > 0 ? `, ignorePatterns: ${JSON.stringify(ignorePatterns)}` : ""} });
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
249
390
|
const frameworkOptions = [{
|
|
250
391
|
label: ansis.red("Test"),
|
|
251
392
|
value: "test"
|
|
@@ -263,17 +404,6 @@ const dependenciesMap = {
|
|
|
263
404
|
test: ["eslint-plugin-jest"]
|
|
264
405
|
};
|
|
265
406
|
//#endregion
|
|
266
|
-
//#region src/guards.ts
|
|
267
|
-
/**
|
|
268
|
-
* Whether a value is an array whose every element is a string.
|
|
269
|
-
*
|
|
270
|
-
* @param value - The value to test.
|
|
271
|
-
* @returns Whether the value is a `string` array.
|
|
272
|
-
*/
|
|
273
|
-
function isStringArray(value) {
|
|
274
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
275
|
-
}
|
|
276
|
-
//#endregion
|
|
277
407
|
//#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/parse-gitignore-ts/1.0.0/0cdd402c09c3520e4b8d62a0e9eaf59ef70b71773d70a71b070a2cf3609765b8/node_modules/parse-gitignore-ts/dist/index.mjs
|
|
278
408
|
var isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
279
409
|
var INVALID_PATH_CHARS_REGEX = /[<>:"|?*\n\r\t\f\x00-\x1F]/;
|
|
@@ -523,9 +653,18 @@ async function updateEslintFiles(result) {
|
|
|
523
653
|
const configLines = [];
|
|
524
654
|
if (eslintIgnores.length) configLines.push(`ignores: ${JSON.stringify(eslintIgnores)},`);
|
|
525
655
|
for (const framework of result.frameworks) configLines.push(`${framework}: true,`);
|
|
656
|
+
if (result.formatter === "standalone") configLines.push("formatter: \"standalone\",");
|
|
526
657
|
const eslintConfigContent = getEslintConfigContent(configLines.map((index) => ` ${index}`).join("\n"), []);
|
|
527
658
|
await fsp.writeFile(pathFlatConfig, eslintConfigContent);
|
|
528
659
|
log.success(ansis.green(`Created ${configFileName}`));
|
|
660
|
+
if (result.formatter === "standalone") {
|
|
661
|
+
const [existing] = oxfmtConfigNamesIn(cwd);
|
|
662
|
+
if (existing !== void 0) log.warn(ansis.yellow(`${existing} already exists, left unchanged.`));
|
|
663
|
+
else {
|
|
664
|
+
await fsp.writeFile(path.join(cwd, OXFMT_CONFIG_MODULE), oxfmtConfigContent(eslintIgnores));
|
|
665
|
+
log.success(ansis.green(`Created ${OXFMT_CONFIG_MODULE}`));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
529
668
|
const files = fs.readdirSync(cwd);
|
|
530
669
|
const legacyConfig = [];
|
|
531
670
|
for (const file of files) if (LEGACY_CONFIG_REGEX.test(file) && !file.includes("eslint.config.")) legacyConfig.push(file);
|
|
@@ -538,7 +677,8 @@ const versionsMap = {
|
|
|
538
677
|
"eslint-plugin-jest": "29.16.0",
|
|
539
678
|
"eslint-plugin-react-jsx": "5.17.3",
|
|
540
679
|
"eslint-plugin-react-naming-convention": "5.17.3",
|
|
541
|
-
"eslint-plugin-react-x": "5.17.3"
|
|
680
|
+
"eslint-plugin-react-x": "5.17.3",
|
|
681
|
+
"oxfmt": "0.72.0"
|
|
542
682
|
};
|
|
543
683
|
//#endregion
|
|
544
684
|
//#region src/cli/stages/update-package-json.ts
|
|
@@ -547,20 +687,28 @@ const LINT_SCRIPTS = {
|
|
|
547
687
|
"lint": "isentinel-lint",
|
|
548
688
|
"lint:fix": "isentinel-lint --fix"
|
|
549
689
|
};
|
|
690
|
+
/** Scripts the standalone formatter setup adds, so oxfmt runs on its own. */
|
|
691
|
+
const FORMAT_SCRIPTS = {
|
|
692
|
+
"format": "oxfmt --disable-nested-config",
|
|
693
|
+
"format:check": "oxfmt --check --disable-nested-config"
|
|
694
|
+
};
|
|
550
695
|
/**
|
|
551
|
-
* Merge the lint scripts into a `scripts` block in
|
|
552
|
-
* always added. An existing script with identical
|
|
553
|
-
* existing script with different content is
|
|
554
|
-
* otherwise only overwritten when
|
|
696
|
+
* Merge the lint scripts (or `options.desired`) into a `scripts` block in
|
|
697
|
+
* place. Missing scripts are always added. An existing script with identical
|
|
698
|
+
* content is left untouched. An existing script with different content is
|
|
699
|
+
* preserved in skip-prompt mode, and otherwise only overwritten when
|
|
700
|
+
* {@link ConfirmOverwrite} resolves true.
|
|
555
701
|
*
|
|
556
702
|
* @param scripts - The consumer's `scripts` block, mutated in place.
|
|
557
|
-
* @param options - Skip-prompt flag
|
|
703
|
+
* @param options - Skip-prompt flag, the overwrite confirmation callback and
|
|
704
|
+
* the scripts to merge (default {@link LINT_SCRIPTS}).
|
|
558
705
|
* @returns Which scripts were added and which were overwritten.
|
|
559
706
|
*/
|
|
560
|
-
async function
|
|
707
|
+
async function mergeScripts(scripts, options) {
|
|
561
708
|
const added = [];
|
|
562
709
|
const overwritten = [];
|
|
563
|
-
|
|
710
|
+
const entries = Object.entries(options.desired ?? LINT_SCRIPTS);
|
|
711
|
+
for (const [name, desired] of entries) {
|
|
564
712
|
const existing = scripts[name];
|
|
565
713
|
if (existing === void 0) {
|
|
566
714
|
scripts[name] = desired;
|
|
@@ -587,6 +735,7 @@ async function updatePackageJson(result, skipPrompt = false) {
|
|
|
587
735
|
parsedPackage.devDependencies ??= {};
|
|
588
736
|
parsedPackage.devDependencies["@isentinel/eslint-config"] = `^${version}`;
|
|
589
737
|
parsedPackage.devDependencies["eslint"] ??= versionsMap.eslint;
|
|
738
|
+
if (result.formatter === "standalone") parsedPackage.devDependencies["oxfmt"] ??= versionsMap.oxfmt;
|
|
590
739
|
const addedPackages = [];
|
|
591
740
|
for (const framework of result.frameworks) {
|
|
592
741
|
if (!(framework in dependenciesMap)) continue;
|
|
@@ -599,17 +748,21 @@ async function updatePackageJson(result, skipPrompt = false) {
|
|
|
599
748
|
if (addedPackages.length) note(ansis.dim(addedPackages.join(", ")), "Added packages");
|
|
600
749
|
parsedPackage.scripts ??= {};
|
|
601
750
|
const { scripts = {} } = parsedPackage;
|
|
602
|
-
const { added, overwritten } = await
|
|
751
|
+
const { added, overwritten } = await mergeScripts(scripts, {
|
|
603
752
|
confirmOverwrite: async (name, existing, desired) => {
|
|
604
753
|
return await confirm({
|
|
605
754
|
initialValue: false,
|
|
606
755
|
message: `A "${name}" script already exists ("${existing}"). Overwrite it with "${desired}"?`
|
|
607
756
|
}) === true;
|
|
608
757
|
},
|
|
758
|
+
desired: result.formatter === "standalone" ? {
|
|
759
|
+
...LINT_SCRIPTS,
|
|
760
|
+
...FORMAT_SCRIPTS
|
|
761
|
+
} : LINT_SCRIPTS,
|
|
609
762
|
skipPrompt
|
|
610
763
|
});
|
|
611
764
|
const touched = [...added, ...overwritten];
|
|
612
|
-
if (touched.length) note(ansis.dim(touched.map((name) => `${name}: ${scripts[name]}`).join("\n")), "
|
|
765
|
+
if (touched.length) note(ansis.dim(touched.map((name) => `${name}: ${scripts[name]}`).join("\n")), "Scripts");
|
|
613
766
|
await fsp.writeFile(pathPackageJSON, JSON.stringify(parsedPackage, null, 2));
|
|
614
767
|
log.success(ansis.green("Changes wrote to package.json"));
|
|
615
768
|
}
|
|
@@ -617,22 +770,55 @@ async function updatePackageJson(result, skipPrompt = false) {
|
|
|
617
770
|
//#region src/cli/stages/update-vscode-settings.ts
|
|
618
771
|
async function updateVscodeSettings(result) {
|
|
619
772
|
if (!result.updateVscodeSettings) return;
|
|
773
|
+
const settings = vscodeSettingsString(result.formatter);
|
|
620
774
|
const cwd = process.cwd();
|
|
621
775
|
const dotVscodePath = path.join(cwd, ".vscode");
|
|
622
776
|
const settingsPath = path.join(dotVscodePath, "settings.json");
|
|
623
777
|
if (!fs.existsSync(dotVscodePath)) await fsp.mkdir(dotVscodePath, { recursive: true });
|
|
624
778
|
if (!fs.existsSync(settingsPath)) {
|
|
625
|
-
await fsp.writeFile(settingsPath, `{${
|
|
779
|
+
await fsp.writeFile(settingsPath, `{${settings}}\n`, "utf-8");
|
|
626
780
|
log.success(ansis.green("Created .vscode/settings.json"));
|
|
781
|
+
await recommendExtensions(dotVscodePath, result);
|
|
627
782
|
return;
|
|
628
783
|
}
|
|
629
784
|
let settingsContent = await fsp.readFile(settingsPath, "utf8");
|
|
630
785
|
settingsContent = settingsContent.trim();
|
|
631
786
|
if (settingsContent.endsWith("}")) settingsContent = settingsContent.slice(0, -1).trimEnd();
|
|
632
787
|
settingsContent += settingsContent.endsWith(",") || settingsContent.endsWith("{") ? "" : ",";
|
|
633
|
-
settingsContent += `${
|
|
788
|
+
settingsContent += `${settings}}\n`;
|
|
634
789
|
await fsp.writeFile(settingsPath, settingsContent, "utf-8");
|
|
635
790
|
log.success(ansis.green("Updated .vscode/settings.json"));
|
|
791
|
+
await recommendExtensions(dotVscodePath, result);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Add the oxc extension to `.vscode/extensions.json` for the standalone
|
|
795
|
+
* formatter setup. A file that is not plain JSON is left alone with a warning.
|
|
796
|
+
*
|
|
797
|
+
* @param dotVscodePath - The `.vscode` directory.
|
|
798
|
+
* @param result - The prompt result.
|
|
799
|
+
*/
|
|
800
|
+
async function recommendExtensions(dotVscodePath, result) {
|
|
801
|
+
if (result.formatter !== "standalone") return;
|
|
802
|
+
const extensionsPath = path.join(dotVscodePath, "extensions.json");
|
|
803
|
+
let extensions = {};
|
|
804
|
+
if (fs.existsSync(extensionsPath)) {
|
|
805
|
+
let parsed;
|
|
806
|
+
try {
|
|
807
|
+
parsed = JSON.parse(await fsp.readFile(extensionsPath, "utf8"));
|
|
808
|
+
} catch {
|
|
809
|
+
parsed = void 0;
|
|
810
|
+
}
|
|
811
|
+
if (!isRecord(parsed)) {
|
|
812
|
+
log.warn(ansis.yellow(`Could not read .vscode/extensions.json; add "${OXC_VSCODE_EXTENSION}" to its recommendations manually.`));
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
extensions = parsed;
|
|
816
|
+
}
|
|
817
|
+
const recommendations = isStringArray(extensions["recommendations"]) ? extensions["recommendations"] : [];
|
|
818
|
+
if (recommendations.includes("oxc.oxc-vscode")) return;
|
|
819
|
+
extensions["recommendations"] = [...recommendations, OXC_VSCODE_EXTENSION];
|
|
820
|
+
await fsp.writeFile(extensionsPath, `${JSON.stringify(extensions, null, 2)}\n`, "utf-8");
|
|
821
|
+
log.success(ansis.green("Recommended the oxc extension in .vscode/extensions.json"));
|
|
636
822
|
}
|
|
637
823
|
//#endregion
|
|
638
824
|
//#region src/cli/run.ts
|
|
@@ -646,13 +832,15 @@ async function run(options = {}) {
|
|
|
646
832
|
log.warn(ansis.yellow(`${eslintConfigFiles[0]} already exists, migration wizard exited.`));
|
|
647
833
|
return process.exit(1);
|
|
648
834
|
}
|
|
835
|
+
const argumentFormatter = formatterOptions.find(({ value }) => value === options.formatter)?.value;
|
|
649
836
|
let result = {
|
|
837
|
+
formatter: argumentFormatter ?? "eslint",
|
|
650
838
|
frameworks: argumentTemplate ?? [],
|
|
651
839
|
uncommittedConfirmed: false,
|
|
652
840
|
updateVscodeSettings: true
|
|
653
841
|
};
|
|
654
842
|
if (argumentSkipPrompt !== true) {
|
|
655
|
-
const { frameworks: groupedFrameworks, uncommittedConfirmed, updateVscodeSettings: shouldUpdateVscode } = await group({
|
|
843
|
+
const { formatter: groupedFormatter, frameworks: groupedFrameworks, uncommittedConfirmed, updateVscodeSettings: shouldUpdateVscode } = await group({
|
|
656
844
|
uncommittedConfirmed: async () => {
|
|
657
845
|
if (isGitClean()) return true;
|
|
658
846
|
return confirm({
|
|
@@ -660,6 +848,14 @@ async function run(options = {}) {
|
|
|
660
848
|
message: "There are uncommitted changes in the current repository, are you sure to continue?"
|
|
661
849
|
});
|
|
662
850
|
},
|
|
851
|
+
formatter: async ({ results }) => {
|
|
852
|
+
if (argumentFormatter !== void 0 || results.uncommittedConfirmed !== true) return;
|
|
853
|
+
return select({
|
|
854
|
+
initialValue: "eslint",
|
|
855
|
+
message: "Where should formatting run?",
|
|
856
|
+
options: formatterOptions
|
|
857
|
+
});
|
|
858
|
+
},
|
|
663
859
|
frameworks: async ({ results }) => {
|
|
664
860
|
if ((argumentTemplate && argumentTemplate.length > 0 && argumentTemplate.every((template) => frameworks.includes(template))) === true || results.uncommittedConfirmed !== true) return;
|
|
665
861
|
const message = argumentTemplate && argumentTemplate.length > 0 ? `"${argumentTemplate.join(", ")}" isn't a valid template. Please choose from below: ` : "Select a framework:";
|
|
@@ -681,6 +877,7 @@ async function run(options = {}) {
|
|
|
681
877
|
process.exit(0);
|
|
682
878
|
} });
|
|
683
879
|
result = {
|
|
880
|
+
formatter: argumentFormatter ?? (groupedFormatter === "standalone" ? "standalone" : "eslint"),
|
|
684
881
|
frameworks: isStringArray(groupedFrameworks) ? groupedFrameworks.filter((value) => {
|
|
685
882
|
return frameworks.some((option) => option === value);
|
|
686
883
|
}) : [],
|
|
@@ -708,6 +905,10 @@ yargs(hideBin(process.argv)).scriptName("@isentinel/eslint-config").usage("").co
|
|
|
708
905
|
alias: "y",
|
|
709
906
|
description: "Skip prompts and use default values",
|
|
710
907
|
type: "boolean"
|
|
908
|
+
}).option("formatter", {
|
|
909
|
+
choices: ["eslint", "standalone"],
|
|
910
|
+
description: "Where formatting runs: in ESLint, or oxfmt on its own",
|
|
911
|
+
type: "string"
|
|
711
912
|
}).option("template", {
|
|
712
913
|
alias: "t",
|
|
713
914
|
description: "Use the framework template for optimal customization: react, jest",
|