exadev-eslint-config 2.14.0 → 2.16.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 +22 -3
- package/dist/index.cjs +317 -23
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +316 -22
- package/package.json +53 -52
package/README.md
CHANGED
|
@@ -162,11 +162,29 @@ Trailing arguments are arbitrary flat-config objects, appended in order after ev
|
|
|
162
162
|
|
|
163
163
|
## RFC 8785 canonical JSON formatting
|
|
164
164
|
|
|
165
|
-
Every JSON file
|
|
165
|
+
Every JSON file is linted against [`eslint-plugin-json-canonical`](https://github.com/ExaDev/eslint-plugin-json-canonical) v2 -- plain UTF-16 code-unit key ordering, canonical number formatting, canonical string escaping, and (as of that plugin's own v2) pretty-printed layout (2-space indentation, one member/element per line, a trailing newline), per [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785). This is bundled unconditionally, the same way jsdoc/tsdoc support is: `eslint-plugin-json-canonical` is a plain dependency of this package, so every consumer already has it. There is no option to turn it off.
|
|
166
166
|
|
|
167
|
-
The plugin's own
|
|
167
|
+
The plugin's own full-canonicalization rule (`no-insignificant-whitespace`, which collapses a document to a single compacted line with no whitespace at all) is deliberately not part of the config this package extends -- it stays available for a consumer to opt into directly for their own genuine canonicalization pass (hashing, signing, byte-for-byte comparison).
|
|
168
168
|
|
|
169
|
-
`**/tsconfig*.json
|
|
169
|
+
`**/*.jsonc`, `**/tsconfig*.json`, and `**/turbo.json` get the plugin's `configs.contentOnlyJsonc` instead of the plain-JSON config: they genuinely carry comments (TypeScript and turbo both accept them), which `@eslint/json`'s `json/json` language has no concept of and fails to parse, and neither pretty-printing nor whitespace-collapsing has a well-defined answer for a comment's own attachment to a member once its surrounding whitespace is rewritten. Content canonicalization (key order, number/string formatting) still applies to these files under `json/jsonc`.
|
|
170
|
+
|
|
171
|
+
`**/package.json` gets everything the plain-JSON config gives every other file -- including pretty-printed layout -- except `json/sort-keys`, turned back off in its own override block, since its key order is the separate, syncpack-aware concern the next section covers.
|
|
172
|
+
|
|
173
|
+
## Optional package.json key ordering
|
|
174
|
+
|
|
175
|
+
`exadevConfig({ packageJsonKeyOrder: true })` enables `exadev/package-json-key-order` for `**/package.json`, requiring the same key order [`syncpack format`](https://syncpack.dev/command/format) would produce -- `sortFirst` fields (`name`, `description`, `version`, `author` by default) pinned to the top in that exact order, then every other top-level key alphabetically; and, inside each `sortAz`-listed field's own object or array value (`dependencies`, `devDependencies`, `scripts`, `keywords`, and the rest of syncpack's own default list), its members/elements sorted the same way. Confirmed directly against real `syncpack@15` output, not assumed from its docs -- see this rule's own source comment for the exact reverse-engineering method (a symbol-before-digit-before-letter, case-insensitive comparison syncpack's docs don't specify precisely enough to derive from prose alone).
|
|
176
|
+
|
|
177
|
+
This exists for a project that wants real `package.json` canonicalization without installing syncpack, and so a project that already has syncpack never sees the two fight: `eslint --fix` and `syncpack format` converge on the identical output.
|
|
178
|
+
|
|
179
|
+
| Value | `options.packageJsonKeyOrder` |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `true` | Force on -- throws if `@eslint/json` isn't resolvable |
|
|
182
|
+
| `false` | Force off -- always `[]`, no resolution attempted |
|
|
183
|
+
| `undefined` / omitted | Auto-detect (the default): on unless the project already has a syncpack config (a `.syncpackrc*`/`syncpack.config.*` file, or a `"syncpack"` key in its own `package.json`), since syncpack already produces this exact order for free |
|
|
184
|
+
|
|
185
|
+
Like React/Next.js support, this needs its own optional peer resolvable -- `pnpm add -D @eslint/json` -- and, unlike them, also needs its `json/json` language registered for the file (this option's own config block does that for you; nothing extra to wire up).
|
|
186
|
+
|
|
187
|
+
Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-detection is (see the table above) -- `packageJsonKeyOrder: true`/`false` only forces the tri-state explicitly, it isn't the only way to reach it. Not part of `plugin.configs.recommended`, and not available as a `plugin.configs.packageJsonKeyOrder` explicit-tier config the way `.react`/`.nextjs` are, since wiring it through `plugin.configs` would need `plugin.ts` and this option's own config builder to import each other.
|
|
170
188
|
|
|
171
189
|
## Rules
|
|
172
190
|
|
|
@@ -190,6 +208,7 @@ The plugin's own whitespace-collapsing rule (`no-insignificant-whitespace`, whic
|
|
|
190
208
|
| `no-enum-number-widening` | | A bare (non-literal) `number` is accepted anywhere a numeric enum is expected, without checking it is actually one of the enum's members -- only a numeric *literal* gets range-checked by `tsc`. No autofix: the only provably safe fix is a genuine runtime membership check against the enum's own values, which is a behavioural choice a mechanical fix cannot responsibly make. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
191
209
|
| `no-enum-reverse-lookup-widening` | suggestion | Indexing a numeric enum's reverse mapping (`Direction[n]`) with a bare (non-literal) `number`, or with a different enum's member, types as plain `string` for any index, including one outside the enum's actual members, where it genuinely returns `undefined` at runtime -- `tsc` does not range-check even a numeric literal index here. When the indexed expression is the init of a variable with an explicit `: string` annotation, a suggestion widens it to `: string \| undefined`, forcing later uses as a bare `string` to surface as real compile errors; every other syntactic position gets a plain report with no fix, and no case gets a full `--fix` autofix. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
192
210
|
| `prefer-numeric-sort-compare` | suggestion | A deliberately narrow addition alongside `@typescript-eslint/require-array-sort-compare` (which already flags any bare `.sort()`/`.toSorted()` except on a plain string array, with no fix): when the array's element type is definitively `number`, a suggestion offers an ascending compare function (`(a, b) => a - b`), since the default comparator sorts lexicographically (`[1, 2, 10].sort()` becomes `[1, 10, 2]`). Not a full autofix -- descending order is a real, if less common, alternative intent. Requires type information -- only in the default (type-checked) export, not `plugin.configs.recommended`, since it needs the checker to confirm the array's element type. |
|
|
211
|
+
| `package-json-key-order` | ✓ | Requires `package.json`'s keys to be ordered the same way `syncpack format` would order them. See [Optional package.json key ordering](#optional-package-json-key-ordering) -- opt-in via `exadevConfig({ packageJsonKeyOrder: true })`, not part of `recommended`/`barrel`. A JSON-language rule (`@eslint/json`'s `json/json`), not a TSESLint one -- needs no type information and doesn't apply to any `.ts`/`.js` file. |
|
|
193
212
|
|
|
194
213
|
## Barrel policy
|
|
195
214
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,15 +31,16 @@ eslint_plugin_tsdoc = __toESM(eslint_plugin_tsdoc, 1);
|
|
|
31
31
|
let eslint_plugin_json_canonical = require("eslint-plugin-json-canonical");
|
|
32
32
|
eslint_plugin_json_canonical = __toESM(eslint_plugin_json_canonical, 1);
|
|
33
33
|
let node_module = require("node:module");
|
|
34
|
-
let
|
|
35
|
-
_eslint_js = __toESM(_eslint_js, 1);
|
|
36
|
-
let typescript_eslint = require("typescript-eslint");
|
|
37
|
-
typescript_eslint = __toESM(typescript_eslint, 1);
|
|
34
|
+
let node_fs = require("node:fs");
|
|
38
35
|
let node_path = require("node:path");
|
|
39
36
|
let _typescript_eslint_utils = require("@typescript-eslint/utils");
|
|
40
37
|
let typescript = require("typescript");
|
|
41
38
|
typescript = __toESM(typescript, 1);
|
|
42
39
|
let ts_api_utils = require("ts-api-utils");
|
|
40
|
+
let _eslint_js = require("@eslint/js");
|
|
41
|
+
_eslint_js = __toESM(_eslint_js, 1);
|
|
42
|
+
let typescript_eslint = require("typescript-eslint");
|
|
43
|
+
typescript_eslint = __toESM(typescript_eslint, 1);
|
|
43
44
|
//#region src/jsdoc.ts
|
|
44
45
|
const JS_TS_FILE_PATTERNS$1 = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
45
46
|
const jsdocConfig = eslint_plugin_jsdoc.default.configs["flat/recommended-tsdoc-error"];
|
|
@@ -80,17 +81,36 @@ const jsdocAndTsdoc = [{
|
|
|
80
81
|
function isSingleFlatConfig(value) {
|
|
81
82
|
return typeof value === "object" && value !== null && !Array.isArray(value) && "language" in value;
|
|
82
83
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
function requireConfig(name) {
|
|
85
|
+
const config = eslint_plugin_json_canonical.default.configs?.[name];
|
|
86
|
+
if (!isSingleFlatConfig(config)) throw new Error(`eslint-plugin-json-canonical: expected configs.${name} to be a single flat config object, got an array or undefined`);
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
const recommended = requireConfig("recommended");
|
|
90
|
+
const contentOnlyJsonc = requireConfig("contentOnlyJsonc");
|
|
91
|
+
const jsonCanonicalConfig = [
|
|
92
|
+
{
|
|
93
|
+
files: ["**/*.json"],
|
|
94
|
+
ignores: [
|
|
95
|
+
"**/*.jsonc",
|
|
96
|
+
"**/tsconfig*.json",
|
|
97
|
+
"**/turbo.json"
|
|
98
|
+
],
|
|
99
|
+
...recommended
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
files: [
|
|
103
|
+
"**/*.jsonc",
|
|
104
|
+
"**/tsconfig*.json",
|
|
105
|
+
"**/turbo.json"
|
|
106
|
+
],
|
|
107
|
+
...contentOnlyJsonc
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
files: ["**/package.json"],
|
|
111
|
+
rules: { "json/sort-keys": "off" }
|
|
112
|
+
}
|
|
113
|
+
];
|
|
94
114
|
//#endregion
|
|
95
115
|
//#region src/optional-plugin.ts
|
|
96
116
|
const nodeRequire = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
@@ -101,16 +121,16 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
101
121
|
return;
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
|
-
function isRecord(value) {
|
|
124
|
+
function isRecord$1(value) {
|
|
105
125
|
return typeof value === "object" && value !== null;
|
|
106
126
|
}
|
|
107
127
|
function isFlatConfig$1(value) {
|
|
108
|
-
return isRecord(value);
|
|
128
|
+
return isRecord$1(value);
|
|
109
129
|
}
|
|
110
130
|
function normalizeLegacyParserOptions(record) {
|
|
111
131
|
if (!("parserOptions" in record)) return record;
|
|
112
132
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
113
|
-
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
133
|
+
const existingLanguageOptions = isRecord$1(languageOptions) ? languageOptions : {};
|
|
114
134
|
return {
|
|
115
135
|
...rest,
|
|
116
136
|
languageOptions: {
|
|
@@ -122,10 +142,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
122
142
|
function readFlatConfig(module, path) {
|
|
123
143
|
let current = module;
|
|
124
144
|
for (const key of path) {
|
|
125
|
-
if (!isRecord(current)) return void 0;
|
|
145
|
+
if (!isRecord$1(current)) return void 0;
|
|
126
146
|
current = current[key];
|
|
127
147
|
}
|
|
128
|
-
if (!isRecord(current)) return void 0;
|
|
148
|
+
if (!isRecord$1(current)) return void 0;
|
|
129
149
|
const normalized = normalizeLegacyParserOptions(current);
|
|
130
150
|
return isFlatConfig$1(normalized) ? normalized : void 0;
|
|
131
151
|
}
|
|
@@ -139,6 +159,9 @@ function buildNextjsConfig(options = {}) {
|
|
|
139
159
|
return nextConfig === void 0 ? [] : [nextConfig];
|
|
140
160
|
}
|
|
141
161
|
//#endregion
|
|
162
|
+
//#region package.json
|
|
163
|
+
var version = "2.16.0";
|
|
164
|
+
//#endregion
|
|
142
165
|
//#region src/react.ts
|
|
143
166
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
144
167
|
const INSTALL_COMMAND = "pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y";
|
|
@@ -176,9 +199,6 @@ function buildReactConfig(options = {}) {
|
|
|
176
199
|
}));
|
|
177
200
|
}
|
|
178
201
|
//#endregion
|
|
179
|
-
//#region package.json
|
|
180
|
-
var version = "2.14.0";
|
|
181
|
-
//#endregion
|
|
182
202
|
//#region src/rules/barrel-helpers.ts
|
|
183
203
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
184
204
|
function basenameOf(filename) {
|
|
@@ -1234,6 +1254,215 @@ const noSideEffectsInIndex = {
|
|
|
1234
1254
|
}
|
|
1235
1255
|
};
|
|
1236
1256
|
//#endregion
|
|
1257
|
+
//#region src/rules/package-json-key-order.ts
|
|
1258
|
+
const DEFAULT_SORT_FIRST = [
|
|
1259
|
+
"name",
|
|
1260
|
+
"description",
|
|
1261
|
+
"version",
|
|
1262
|
+
"author"
|
|
1263
|
+
];
|
|
1264
|
+
const DEFAULT_SORT_AZ = [
|
|
1265
|
+
"bin",
|
|
1266
|
+
"contributors",
|
|
1267
|
+
"dependencies",
|
|
1268
|
+
"devDependencies",
|
|
1269
|
+
"keywords",
|
|
1270
|
+
"peerDependencies",
|
|
1271
|
+
"resolutions",
|
|
1272
|
+
"scripts"
|
|
1273
|
+
];
|
|
1274
|
+
function categorize(char) {
|
|
1275
|
+
if (char >= "0" && char <= "9") return 1;
|
|
1276
|
+
if (/[a-z]/iu.test(char)) return 2;
|
|
1277
|
+
return 0;
|
|
1278
|
+
}
|
|
1279
|
+
/** Case-insensitive, symbol-before-digit-before-letter comparison matching real `syncpack format` output. Returns a negative number if `a` sorts before `b`, positive if after, zero if equal. */
|
|
1280
|
+
function compareSyncpackKey(a, b) {
|
|
1281
|
+
const length = Math.min(a.length, b.length);
|
|
1282
|
+
for (let index = 0; index < length; index += 1) {
|
|
1283
|
+
const charA = a[index] ?? "";
|
|
1284
|
+
const charB = b[index] ?? "";
|
|
1285
|
+
const categoryA = categorize(charA);
|
|
1286
|
+
const categoryB = categorize(charB);
|
|
1287
|
+
if (categoryA !== categoryB) return categoryA - categoryB;
|
|
1288
|
+
const foldedA = categoryA === 2 ? charA.toLowerCase() : charA;
|
|
1289
|
+
const foldedB = categoryB === 2 ? charB.toLowerCase() : charB;
|
|
1290
|
+
if (foldedA !== foldedB) return foldedA < foldedB ? -1 : 1;
|
|
1291
|
+
}
|
|
1292
|
+
return a.length - b.length;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* The pure decision for an adjacent pair of top-level keys, independent of ESLint/momoa: given syncpack's own `sortFirst` list, is `curr` allowed to immediately follow `prev`? A pinned key (present in `sortFirst`) must appear in exactly `sortFirst`'s own order; every other key sorts after every pinned key, and among themselves by `compareSyncpackKey`. Kept separate from `create()` below so it can be unit-tested directly, matching the same pattern `no-uphill-dependency`'s `checkDependencies` in the monorepo-template uses.
|
|
1296
|
+
*/
|
|
1297
|
+
function isValidTopLevelOrder(prev, curr, sortFirst) {
|
|
1298
|
+
const prevIndex = sortFirst.indexOf(prev);
|
|
1299
|
+
const currIndex = sortFirst.indexOf(curr);
|
|
1300
|
+
if (prevIndex !== -1 && currIndex !== -1) return prevIndex < currIndex;
|
|
1301
|
+
if (prevIndex !== -1) return true;
|
|
1302
|
+
if (currIndex !== -1) return false;
|
|
1303
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1304
|
+
}
|
|
1305
|
+
/** The pure decision for an adjacent pair of keys/elements inside a `sortAz`-listed field's own object or array value: plain `compareSyncpackKey` order, no pinning. */
|
|
1306
|
+
function isValidSortAzOrder(prev, curr) {
|
|
1307
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* The target permutation for a whole container's own keys, given a comparator over adjacent pairs (either `isValidTopLevelOrder`-shaped or `isValidSortAzOrder`-shaped). Returns, for each output position, the ORIGINAL index of the key that belongs there -- e.g. `[2, 0, 1]` means the key currently at index 2 comes first. A stable sort (ties keep their original relative order), matching real syncpack output for keys that compare equal (see `compareSyncpackKey`'s own case-insensitive-letter test case).
|
|
1311
|
+
*
|
|
1312
|
+
* Reordering by computing this full permutation once, rather than reporting and fixing one out-of-order adjacent pair at a time (the technique `@eslint/json`'s own `sort-keys` rule uses), is deliberate: an adjacent-swap fixer only makes one pass of bubble-sort-style progress per lint pass, and a realistically scrambled `package.json` (many top-level keys all needing to move several positions, several nested objects also needing sorting) was confirmed directly to need up to 11 fix passes to fully converge -- one more than ESLint's own `Linter#verifyAndFix` will ever run (`MAX_AUTOFIX_PASSES` is 10), meaning a single real `eslint --fix` invocation could leave such a file only partially reordered. Computing the whole target order up front and replacing every affected key's position in one combined fix converges in a single pass regardless of how scrambled the input is.
|
|
1313
|
+
*/
|
|
1314
|
+
function computeOrderPermutation(keys, isValidOrder) {
|
|
1315
|
+
return keys.map((_, index) => index).map((index) => ({
|
|
1316
|
+
key: keys[index] ?? "",
|
|
1317
|
+
index
|
|
1318
|
+
})).sort((a, b) => {
|
|
1319
|
+
if (isValidOrder(a.key, b.key) && isValidOrder(b.key, a.key)) return a.index - b.index;
|
|
1320
|
+
return isValidOrder(a.key, b.key) ? -1 : 1;
|
|
1321
|
+
}).map((entry) => entry.index);
|
|
1322
|
+
}
|
|
1323
|
+
function isIdentityPermutation(permutation) {
|
|
1324
|
+
return permutation.every((originalIndex, position) => originalIndex === position);
|
|
1325
|
+
}
|
|
1326
|
+
function isStringNode(node) {
|
|
1327
|
+
return node.type === "String";
|
|
1328
|
+
}
|
|
1329
|
+
function getMemberKeyName(member) {
|
|
1330
|
+
return member.name.type === "String" ? member.name.value : void 0;
|
|
1331
|
+
}
|
|
1332
|
+
function hasRange(node) {
|
|
1333
|
+
return node.range !== void 0;
|
|
1334
|
+
}
|
|
1335
|
+
const commentTypes = /* @__PURE__ */ new Set(["LineComment", "BlockComment"]);
|
|
1336
|
+
/**
|
|
1337
|
+
* Enforces the same `package.json` key order `syncpack format` would produce: `sortFirst` fields pinned to the top in that exact order, then every other top-level key alphabetically; and, inside each `sortAz`-listed field's own object or array value, its members/elements sorted the same way (no pinning there -- `sortFirst` only ever applies at the top level).
|
|
1338
|
+
*
|
|
1339
|
+
* This exists so a project using `@exadev/eslint-config` without syncpack still gets real `package.json` canonicalization, and so a project using both never sees them fight: `eslint --fix` and `syncpack format` converge on the identical output, confirmed directly against real `syncpack@15` output rather than assumed from its docs (see `compareSyncpackKey`'s own comment). Deliberately narrower than `@eslint/json`'s own `sort-keys` rule, which only supports a single alphabetical order for every key with no field-specific pinning -- not a fit for syncpack's own two-tier scheme.
|
|
1340
|
+
*
|
|
1341
|
+
* Not part of `plugin.configs.recommended`: opt-in, the same tri-state shape as `react`/`nextjs` (see `docs.md`'s own usage example), since a project without syncpack needs to choose this deliberately, and a project with syncpack needs to confirm the two tools agree before turning it on.
|
|
1342
|
+
*/
|
|
1343
|
+
const packageJsonKeyOrder = {
|
|
1344
|
+
meta: {
|
|
1345
|
+
type: "layout",
|
|
1346
|
+
fixable: "code",
|
|
1347
|
+
languages: ["json/json", "json/jsonc"],
|
|
1348
|
+
docs: {
|
|
1349
|
+
recommended: false,
|
|
1350
|
+
description: "Require package.json keys to be ordered the same way `syncpack format` would order them.",
|
|
1351
|
+
url: "https://github.com/ExaDev/eslint-config/blob/main/src/rules/package-json-key-order.ts"
|
|
1352
|
+
},
|
|
1353
|
+
schema: [{
|
|
1354
|
+
type: "object",
|
|
1355
|
+
properties: {
|
|
1356
|
+
sortFirst: {
|
|
1357
|
+
type: "array",
|
|
1358
|
+
items: { type: "string" }
|
|
1359
|
+
},
|
|
1360
|
+
sortAz: {
|
|
1361
|
+
type: "array",
|
|
1362
|
+
items: { type: "string" }
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
additionalProperties: false
|
|
1366
|
+
}],
|
|
1367
|
+
defaultOptions: [{}],
|
|
1368
|
+
messages: { outOfOrder: "This key order does not match the order \"syncpack format\" would produce: \"{{curr}}\" should come before \"{{prev}}\" here." }
|
|
1369
|
+
},
|
|
1370
|
+
create(context) {
|
|
1371
|
+
const [options] = context.options;
|
|
1372
|
+
const sortFirst = options.sortFirst ?? DEFAULT_SORT_FIRST;
|
|
1373
|
+
const sortAz = new Set(options.sortAz ?? DEFAULT_SORT_AZ);
|
|
1374
|
+
const { sourceCode } = context;
|
|
1375
|
+
function hasAdjacentComment(node) {
|
|
1376
|
+
const before = sourceCode.getTokenBefore(node, { includeComments: true });
|
|
1377
|
+
let after = sourceCode.getTokenAfter(node, { includeComments: true });
|
|
1378
|
+
if (after?.type === "Comma") after = sourceCode.getTokenAfter(after, { includeComments: true });
|
|
1379
|
+
return before !== null && commentTypes.has(before.type) || after !== null && commentTypes.has(after.type);
|
|
1380
|
+
}
|
|
1381
|
+
function buildReorderedText(nodes, permutation) {
|
|
1382
|
+
const separators = nodes.slice(0, -1).map((node, index) => {
|
|
1383
|
+
const next = nodes[index + 1];
|
|
1384
|
+
return hasRange(node) && next !== void 0 && hasRange(next) ? sourceCode.text.slice(node.range[1], next.range[0]) : "";
|
|
1385
|
+
});
|
|
1386
|
+
return permutation.reduce((result, originalIndex, position) => {
|
|
1387
|
+
const text = sourceCode.getText(nodes[originalIndex]);
|
|
1388
|
+
return position === 0 ? text : `${result}${separators[position - 1] ?? ""}${text}`;
|
|
1389
|
+
}, "");
|
|
1390
|
+
}
|
|
1391
|
+
function getNodeRanges(nodes) {
|
|
1392
|
+
const ranges = [];
|
|
1393
|
+
for (const node of nodes) {
|
|
1394
|
+
if (!hasRange(node)) return void 0;
|
|
1395
|
+
ranges.push(node.range);
|
|
1396
|
+
}
|
|
1397
|
+
return ranges;
|
|
1398
|
+
}
|
|
1399
|
+
function reportWholeContainerReorder(nodes, keys, isValidOrder, reportLoc) {
|
|
1400
|
+
const permutation = computeOrderPermutation(keys, isValidOrder);
|
|
1401
|
+
if (isIdentityPermutation(permutation)) return;
|
|
1402
|
+
const firstNode = nodes[0];
|
|
1403
|
+
const lastNode = nodes[nodes.length - 1];
|
|
1404
|
+
if (firstNode === void 0 || lastNode === void 0) return;
|
|
1405
|
+
let violationIndex = 1;
|
|
1406
|
+
for (let index = 1; index < keys.length; index += 1) if (!isValidOrder(keys[index - 1] ?? "", keys[index] ?? "")) {
|
|
1407
|
+
violationIndex = index;
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
context.report({
|
|
1411
|
+
loc: reportLoc(nodes[violationIndex] ?? firstNode),
|
|
1412
|
+
messageId: "outOfOrder",
|
|
1413
|
+
data: {
|
|
1414
|
+
curr: keys[violationIndex] ?? "",
|
|
1415
|
+
prev: keys[violationIndex - 1] ?? ""
|
|
1416
|
+
},
|
|
1417
|
+
fix(fixer) {
|
|
1418
|
+
const ranges = getNodeRanges(nodes);
|
|
1419
|
+
if (ranges === void 0 || nodes.some((node) => hasAdjacentComment(node))) return null;
|
|
1420
|
+
const firstRange = ranges[0];
|
|
1421
|
+
const lastRange = ranges[ranges.length - 1];
|
|
1422
|
+
if (firstRange === void 0 || lastRange === void 0) return null;
|
|
1423
|
+
const rewritten = buildReorderedText(nodes, permutation);
|
|
1424
|
+
return fixer.replaceTextRange([firstRange[0], lastRange[1]], rewritten);
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
function checkMembers(members, isValidOrder) {
|
|
1429
|
+
const keys = [];
|
|
1430
|
+
for (const member of members) {
|
|
1431
|
+
const key = getMemberKeyName(member);
|
|
1432
|
+
if (key === void 0) return;
|
|
1433
|
+
keys.push(key);
|
|
1434
|
+
}
|
|
1435
|
+
reportWholeContainerReorder(members, keys, isValidOrder, (member) => member.name.loc.start);
|
|
1436
|
+
}
|
|
1437
|
+
function checkElements(elements) {
|
|
1438
|
+
const stringValues = [];
|
|
1439
|
+
for (const element of elements) {
|
|
1440
|
+
if (!isStringNode(element.value)) return;
|
|
1441
|
+
stringValues.push(element.value);
|
|
1442
|
+
}
|
|
1443
|
+
reportWholeContainerReorder(stringValues, stringValues.map((value) => value.value), isValidSortAzOrder, (value) => value.loc.start);
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
Object(node, parent) {
|
|
1447
|
+
if (parent === void 0) return;
|
|
1448
|
+
if (parent.type === "Document") {
|
|
1449
|
+
checkMembers(node.members, (prev, curr) => isValidTopLevelOrder(prev, curr, sortFirst));
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (parent.type === "Member") {
|
|
1453
|
+
const key = getMemberKeyName(parent);
|
|
1454
|
+
if (key !== void 0 && sortAz.has(key)) checkMembers(node.members, isValidSortAzOrder);
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
Array(node, parent) {
|
|
1458
|
+
if (parent?.type !== "Member") return;
|
|
1459
|
+
const key = getMemberKeyName(parent);
|
|
1460
|
+
if (key !== void 0 && sortAz.has(key)) checkElements(node.elements);
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
//#endregion
|
|
1237
1466
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
1238
1467
|
const createRule$2 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1239
1468
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
@@ -1456,6 +1685,7 @@ const plugin = {
|
|
|
1456
1685
|
"no-pointless-reassignment": noPointlessReassignment,
|
|
1457
1686
|
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1458
1687
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1688
|
+
"package-json-key-order": packageJsonKeyOrder,
|
|
1459
1689
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1460
1690
|
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1461
1691
|
"prefer-readonly-object-param": preferReadonlyObjectParam
|
|
@@ -1489,6 +1719,69 @@ const plugin = {
|
|
|
1489
1719
|
}
|
|
1490
1720
|
};
|
|
1491
1721
|
//#endregion
|
|
1722
|
+
//#region src/package-json-key-order.ts
|
|
1723
|
+
const SYNCPACK_CONFIG_FILENAMES = [
|
|
1724
|
+
".syncpackrc",
|
|
1725
|
+
".syncpackrc.json",
|
|
1726
|
+
".syncpackrc.yaml",
|
|
1727
|
+
".syncpackrc.yml",
|
|
1728
|
+
".syncpackrc.js",
|
|
1729
|
+
".syncpackrc.cjs",
|
|
1730
|
+
".syncpackrc.mjs",
|
|
1731
|
+
"syncpack.config.js",
|
|
1732
|
+
"syncpack.config.cjs",
|
|
1733
|
+
"syncpack.config.mjs",
|
|
1734
|
+
"syncpack.config.ts"
|
|
1735
|
+
];
|
|
1736
|
+
function packageJsonHasSyncpackKey(cwd) {
|
|
1737
|
+
try {
|
|
1738
|
+
const raw = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(cwd, "package.json"), "utf8"));
|
|
1739
|
+
return typeof raw === "object" && raw !== null && "syncpack" in raw;
|
|
1740
|
+
} catch {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
/** Auto-detection signal for the `enabled: undefined` case: does this project already have syncpack configured? Mirrors the role `tryRequire`'s peer-package resolution plays for `react.ts`/`nextjs.ts`, but for a tool detected by config presence rather than by an installed package, since syncpack's own output is what this rule is emulating, not a peer this rule depends on. */
|
|
1745
|
+
function hasSyncpackConfig(cwd) {
|
|
1746
|
+
return SYNCPACK_CONFIG_FILENAMES.some((filename) => (0, node_fs.existsSync)((0, node_path.join)(cwd, filename))) || packageJsonHasSyncpackKey(cwd);
|
|
1747
|
+
}
|
|
1748
|
+
function isRecord(value) {
|
|
1749
|
+
return typeof value === "object" && value !== null;
|
|
1750
|
+
}
|
|
1751
|
+
function isJsonLanguagePlugin(value) {
|
|
1752
|
+
if (!isRecord(value)) return false;
|
|
1753
|
+
const languages = value["languages"];
|
|
1754
|
+
return isRecord(languages) && "json" in languages;
|
|
1755
|
+
}
|
|
1756
|
+
function resolveJsonPlugin(value) {
|
|
1757
|
+
if (isRecord(value) && isJsonLanguagePlugin(value["default"])) return value["default"];
|
|
1758
|
+
return isJsonLanguagePlugin(value) ? value : void 0;
|
|
1759
|
+
}
|
|
1760
|
+
const JSON_PLUGIN_INSTALL_COMMAND = "pnpm add -D @eslint/json";
|
|
1761
|
+
function buildPackageJsonKeyOrderConfig(options = {}) {
|
|
1762
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1763
|
+
if (options.enabled === false) return [];
|
|
1764
|
+
if (options.enabled === void 0 && hasSyncpackConfig(cwd)) return [];
|
|
1765
|
+
const jsonPlugin = resolveJsonPlugin(tryRequire("@eslint/json", options.requireFn));
|
|
1766
|
+
if (jsonPlugin === void 0) {
|
|
1767
|
+
if (options.enabled === true) throw new Error(`@exadev/eslint-config: package.json key ordering was explicitly requested but '@eslint/json' could not be resolved. Install it with: ${JSON_PLUGIN_INSTALL_COMMAND}`);
|
|
1768
|
+
return [];
|
|
1769
|
+
}
|
|
1770
|
+
const ruleOptions = {
|
|
1771
|
+
...options.sortFirst !== void 0 && { sortFirst: options.sortFirst },
|
|
1772
|
+
...options.sortAz !== void 0 && { sortAz: options.sortAz }
|
|
1773
|
+
};
|
|
1774
|
+
return [{
|
|
1775
|
+
files: ["**/package.json"],
|
|
1776
|
+
language: "json/json",
|
|
1777
|
+
plugins: {
|
|
1778
|
+
exadev: plugin,
|
|
1779
|
+
json: jsonPlugin
|
|
1780
|
+
},
|
|
1781
|
+
rules: { "exadev/package-json-key-order": ["error", ruleOptions] }
|
|
1782
|
+
}];
|
|
1783
|
+
}
|
|
1784
|
+
//#endregion
|
|
1492
1785
|
//#region src/recommended-type-checked.ts
|
|
1493
1786
|
const TEST_FILE_PATTERNS = "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
1494
1787
|
const JS_TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
@@ -1583,6 +1876,7 @@ function exadevConfig(options = {}, ...userConfigs) {
|
|
|
1583
1876
|
...jsonCanonicalConfig,
|
|
1584
1877
|
...buildReactConfig({ enabled: options.react }),
|
|
1585
1878
|
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
1879
|
+
...buildPackageJsonKeyOrderConfig({ enabled: options.packageJsonKeyOrder }),
|
|
1586
1880
|
...userConfigs
|
|
1587
1881
|
]);
|
|
1588
1882
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -7,6 +7,7 @@ type PublicConfigArray = Config[];
|
|
|
7
7
|
interface ExadevConfigOptions {
|
|
8
8
|
readonly react?: boolean;
|
|
9
9
|
readonly nextjs?: boolean;
|
|
10
|
+
readonly packageJsonKeyOrder?: boolean;
|
|
10
11
|
}
|
|
11
12
|
declare function exadevConfig(options?: ExadevConfigOptions, ...userConfigs: readonly TSESLint.FlatConfig.Config[]): PublicConfigArray;
|
|
12
13
|
declare const defaultConfig: PublicConfigArray;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ type PublicConfigArray = Config[];
|
|
|
7
7
|
interface ExadevConfigOptions {
|
|
8
8
|
readonly react?: boolean;
|
|
9
9
|
readonly nextjs?: boolean;
|
|
10
|
+
readonly packageJsonKeyOrder?: boolean;
|
|
10
11
|
}
|
|
11
12
|
declare function exadevConfig(options?: ExadevConfigOptions, ...userConfigs: readonly TSESLint.FlatConfig.Config[]): PublicConfigArray;
|
|
12
13
|
declare const defaultConfig: PublicConfigArray;
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,13 @@ import jsdoc from "eslint-plugin-jsdoc";
|
|
|
2
2
|
import tsdoc from "eslint-plugin-tsdoc";
|
|
3
3
|
import jsonCanonical from "eslint-plugin-json-canonical";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { posix } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join, posix } from "node:path";
|
|
8
7
|
import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
9
8
|
import * as ts from "typescript";
|
|
10
9
|
import { isPropertyReadonlyInType, isTypeReference } from "ts-api-utils";
|
|
10
|
+
import js from "@eslint/js";
|
|
11
|
+
import tseslint from "typescript-eslint";
|
|
11
12
|
//#region src/jsdoc.ts
|
|
12
13
|
const JS_TS_FILE_PATTERNS$1 = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
13
14
|
const jsdocConfig = jsdoc.configs["flat/recommended-tsdoc-error"];
|
|
@@ -48,17 +49,36 @@ const jsdocAndTsdoc = [{
|
|
|
48
49
|
function isSingleFlatConfig(value) {
|
|
49
50
|
return typeof value === "object" && value !== null && !Array.isArray(value) && "language" in value;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
function requireConfig(name) {
|
|
53
|
+
const config = jsonCanonical.configs?.[name];
|
|
54
|
+
if (!isSingleFlatConfig(config)) throw new Error(`eslint-plugin-json-canonical: expected configs.${name} to be a single flat config object, got an array or undefined`);
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
const recommended = requireConfig("recommended");
|
|
58
|
+
const contentOnlyJsonc = requireConfig("contentOnlyJsonc");
|
|
59
|
+
const jsonCanonicalConfig = [
|
|
60
|
+
{
|
|
61
|
+
files: ["**/*.json"],
|
|
62
|
+
ignores: [
|
|
63
|
+
"**/*.jsonc",
|
|
64
|
+
"**/tsconfig*.json",
|
|
65
|
+
"**/turbo.json"
|
|
66
|
+
],
|
|
67
|
+
...recommended
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
files: [
|
|
71
|
+
"**/*.jsonc",
|
|
72
|
+
"**/tsconfig*.json",
|
|
73
|
+
"**/turbo.json"
|
|
74
|
+
],
|
|
75
|
+
...contentOnlyJsonc
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
files: ["**/package.json"],
|
|
79
|
+
rules: { "json/sort-keys": "off" }
|
|
80
|
+
}
|
|
81
|
+
];
|
|
62
82
|
//#endregion
|
|
63
83
|
//#region src/optional-plugin.ts
|
|
64
84
|
const nodeRequire = createRequire(import.meta.url);
|
|
@@ -69,16 +89,16 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
69
89
|
return;
|
|
70
90
|
}
|
|
71
91
|
}
|
|
72
|
-
function isRecord(value) {
|
|
92
|
+
function isRecord$1(value) {
|
|
73
93
|
return typeof value === "object" && value !== null;
|
|
74
94
|
}
|
|
75
95
|
function isFlatConfig$1(value) {
|
|
76
|
-
return isRecord(value);
|
|
96
|
+
return isRecord$1(value);
|
|
77
97
|
}
|
|
78
98
|
function normalizeLegacyParserOptions(record) {
|
|
79
99
|
if (!("parserOptions" in record)) return record;
|
|
80
100
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
81
|
-
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
101
|
+
const existingLanguageOptions = isRecord$1(languageOptions) ? languageOptions : {};
|
|
82
102
|
return {
|
|
83
103
|
...rest,
|
|
84
104
|
languageOptions: {
|
|
@@ -90,10 +110,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
90
110
|
function readFlatConfig(module, path) {
|
|
91
111
|
let current = module;
|
|
92
112
|
for (const key of path) {
|
|
93
|
-
if (!isRecord(current)) return void 0;
|
|
113
|
+
if (!isRecord$1(current)) return void 0;
|
|
94
114
|
current = current[key];
|
|
95
115
|
}
|
|
96
|
-
if (!isRecord(current)) return void 0;
|
|
116
|
+
if (!isRecord$1(current)) return void 0;
|
|
97
117
|
const normalized = normalizeLegacyParserOptions(current);
|
|
98
118
|
return isFlatConfig$1(normalized) ? normalized : void 0;
|
|
99
119
|
}
|
|
@@ -107,6 +127,9 @@ function buildNextjsConfig(options = {}) {
|
|
|
107
127
|
return nextConfig === void 0 ? [] : [nextConfig];
|
|
108
128
|
}
|
|
109
129
|
//#endregion
|
|
130
|
+
//#region package.json
|
|
131
|
+
var version = "2.16.0";
|
|
132
|
+
//#endregion
|
|
110
133
|
//#region src/react.ts
|
|
111
134
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
112
135
|
const INSTALL_COMMAND = "pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y";
|
|
@@ -144,9 +167,6 @@ function buildReactConfig(options = {}) {
|
|
|
144
167
|
}));
|
|
145
168
|
}
|
|
146
169
|
//#endregion
|
|
147
|
-
//#region package.json
|
|
148
|
-
var version = "2.14.0";
|
|
149
|
-
//#endregion
|
|
150
170
|
//#region src/rules/barrel-helpers.ts
|
|
151
171
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
152
172
|
function basenameOf(filename) {
|
|
@@ -1202,6 +1222,215 @@ const noSideEffectsInIndex = {
|
|
|
1202
1222
|
}
|
|
1203
1223
|
};
|
|
1204
1224
|
//#endregion
|
|
1225
|
+
//#region src/rules/package-json-key-order.ts
|
|
1226
|
+
const DEFAULT_SORT_FIRST = [
|
|
1227
|
+
"name",
|
|
1228
|
+
"description",
|
|
1229
|
+
"version",
|
|
1230
|
+
"author"
|
|
1231
|
+
];
|
|
1232
|
+
const DEFAULT_SORT_AZ = [
|
|
1233
|
+
"bin",
|
|
1234
|
+
"contributors",
|
|
1235
|
+
"dependencies",
|
|
1236
|
+
"devDependencies",
|
|
1237
|
+
"keywords",
|
|
1238
|
+
"peerDependencies",
|
|
1239
|
+
"resolutions",
|
|
1240
|
+
"scripts"
|
|
1241
|
+
];
|
|
1242
|
+
function categorize(char) {
|
|
1243
|
+
if (char >= "0" && char <= "9") return 1;
|
|
1244
|
+
if (/[a-z]/iu.test(char)) return 2;
|
|
1245
|
+
return 0;
|
|
1246
|
+
}
|
|
1247
|
+
/** Case-insensitive, symbol-before-digit-before-letter comparison matching real `syncpack format` output. Returns a negative number if `a` sorts before `b`, positive if after, zero if equal. */
|
|
1248
|
+
function compareSyncpackKey(a, b) {
|
|
1249
|
+
const length = Math.min(a.length, b.length);
|
|
1250
|
+
for (let index = 0; index < length; index += 1) {
|
|
1251
|
+
const charA = a[index] ?? "";
|
|
1252
|
+
const charB = b[index] ?? "";
|
|
1253
|
+
const categoryA = categorize(charA);
|
|
1254
|
+
const categoryB = categorize(charB);
|
|
1255
|
+
if (categoryA !== categoryB) return categoryA - categoryB;
|
|
1256
|
+
const foldedA = categoryA === 2 ? charA.toLowerCase() : charA;
|
|
1257
|
+
const foldedB = categoryB === 2 ? charB.toLowerCase() : charB;
|
|
1258
|
+
if (foldedA !== foldedB) return foldedA < foldedB ? -1 : 1;
|
|
1259
|
+
}
|
|
1260
|
+
return a.length - b.length;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* The pure decision for an adjacent pair of top-level keys, independent of ESLint/momoa: given syncpack's own `sortFirst` list, is `curr` allowed to immediately follow `prev`? A pinned key (present in `sortFirst`) must appear in exactly `sortFirst`'s own order; every other key sorts after every pinned key, and among themselves by `compareSyncpackKey`. Kept separate from `create()` below so it can be unit-tested directly, matching the same pattern `no-uphill-dependency`'s `checkDependencies` in the monorepo-template uses.
|
|
1264
|
+
*/
|
|
1265
|
+
function isValidTopLevelOrder(prev, curr, sortFirst) {
|
|
1266
|
+
const prevIndex = sortFirst.indexOf(prev);
|
|
1267
|
+
const currIndex = sortFirst.indexOf(curr);
|
|
1268
|
+
if (prevIndex !== -1 && currIndex !== -1) return prevIndex < currIndex;
|
|
1269
|
+
if (prevIndex !== -1) return true;
|
|
1270
|
+
if (currIndex !== -1) return false;
|
|
1271
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1272
|
+
}
|
|
1273
|
+
/** The pure decision for an adjacent pair of keys/elements inside a `sortAz`-listed field's own object or array value: plain `compareSyncpackKey` order, no pinning. */
|
|
1274
|
+
function isValidSortAzOrder(prev, curr) {
|
|
1275
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* The target permutation for a whole container's own keys, given a comparator over adjacent pairs (either `isValidTopLevelOrder`-shaped or `isValidSortAzOrder`-shaped). Returns, for each output position, the ORIGINAL index of the key that belongs there -- e.g. `[2, 0, 1]` means the key currently at index 2 comes first. A stable sort (ties keep their original relative order), matching real syncpack output for keys that compare equal (see `compareSyncpackKey`'s own case-insensitive-letter test case).
|
|
1279
|
+
*
|
|
1280
|
+
* Reordering by computing this full permutation once, rather than reporting and fixing one out-of-order adjacent pair at a time (the technique `@eslint/json`'s own `sort-keys` rule uses), is deliberate: an adjacent-swap fixer only makes one pass of bubble-sort-style progress per lint pass, and a realistically scrambled `package.json` (many top-level keys all needing to move several positions, several nested objects also needing sorting) was confirmed directly to need up to 11 fix passes to fully converge -- one more than ESLint's own `Linter#verifyAndFix` will ever run (`MAX_AUTOFIX_PASSES` is 10), meaning a single real `eslint --fix` invocation could leave such a file only partially reordered. Computing the whole target order up front and replacing every affected key's position in one combined fix converges in a single pass regardless of how scrambled the input is.
|
|
1281
|
+
*/
|
|
1282
|
+
function computeOrderPermutation(keys, isValidOrder) {
|
|
1283
|
+
return keys.map((_, index) => index).map((index) => ({
|
|
1284
|
+
key: keys[index] ?? "",
|
|
1285
|
+
index
|
|
1286
|
+
})).sort((a, b) => {
|
|
1287
|
+
if (isValidOrder(a.key, b.key) && isValidOrder(b.key, a.key)) return a.index - b.index;
|
|
1288
|
+
return isValidOrder(a.key, b.key) ? -1 : 1;
|
|
1289
|
+
}).map((entry) => entry.index);
|
|
1290
|
+
}
|
|
1291
|
+
function isIdentityPermutation(permutation) {
|
|
1292
|
+
return permutation.every((originalIndex, position) => originalIndex === position);
|
|
1293
|
+
}
|
|
1294
|
+
function isStringNode(node) {
|
|
1295
|
+
return node.type === "String";
|
|
1296
|
+
}
|
|
1297
|
+
function getMemberKeyName(member) {
|
|
1298
|
+
return member.name.type === "String" ? member.name.value : void 0;
|
|
1299
|
+
}
|
|
1300
|
+
function hasRange(node) {
|
|
1301
|
+
return node.range !== void 0;
|
|
1302
|
+
}
|
|
1303
|
+
const commentTypes = /* @__PURE__ */ new Set(["LineComment", "BlockComment"]);
|
|
1304
|
+
/**
|
|
1305
|
+
* Enforces the same `package.json` key order `syncpack format` would produce: `sortFirst` fields pinned to the top in that exact order, then every other top-level key alphabetically; and, inside each `sortAz`-listed field's own object or array value, its members/elements sorted the same way (no pinning there -- `sortFirst` only ever applies at the top level).
|
|
1306
|
+
*
|
|
1307
|
+
* This exists so a project using `@exadev/eslint-config` without syncpack still gets real `package.json` canonicalization, and so a project using both never sees them fight: `eslint --fix` and `syncpack format` converge on the identical output, confirmed directly against real `syncpack@15` output rather than assumed from its docs (see `compareSyncpackKey`'s own comment). Deliberately narrower than `@eslint/json`'s own `sort-keys` rule, which only supports a single alphabetical order for every key with no field-specific pinning -- not a fit for syncpack's own two-tier scheme.
|
|
1308
|
+
*
|
|
1309
|
+
* Not part of `plugin.configs.recommended`: opt-in, the same tri-state shape as `react`/`nextjs` (see `docs.md`'s own usage example), since a project without syncpack needs to choose this deliberately, and a project with syncpack needs to confirm the two tools agree before turning it on.
|
|
1310
|
+
*/
|
|
1311
|
+
const packageJsonKeyOrder = {
|
|
1312
|
+
meta: {
|
|
1313
|
+
type: "layout",
|
|
1314
|
+
fixable: "code",
|
|
1315
|
+
languages: ["json/json", "json/jsonc"],
|
|
1316
|
+
docs: {
|
|
1317
|
+
recommended: false,
|
|
1318
|
+
description: "Require package.json keys to be ordered the same way `syncpack format` would order them.",
|
|
1319
|
+
url: "https://github.com/ExaDev/eslint-config/blob/main/src/rules/package-json-key-order.ts"
|
|
1320
|
+
},
|
|
1321
|
+
schema: [{
|
|
1322
|
+
type: "object",
|
|
1323
|
+
properties: {
|
|
1324
|
+
sortFirst: {
|
|
1325
|
+
type: "array",
|
|
1326
|
+
items: { type: "string" }
|
|
1327
|
+
},
|
|
1328
|
+
sortAz: {
|
|
1329
|
+
type: "array",
|
|
1330
|
+
items: { type: "string" }
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
additionalProperties: false
|
|
1334
|
+
}],
|
|
1335
|
+
defaultOptions: [{}],
|
|
1336
|
+
messages: { outOfOrder: "This key order does not match the order \"syncpack format\" would produce: \"{{curr}}\" should come before \"{{prev}}\" here." }
|
|
1337
|
+
},
|
|
1338
|
+
create(context) {
|
|
1339
|
+
const [options] = context.options;
|
|
1340
|
+
const sortFirst = options.sortFirst ?? DEFAULT_SORT_FIRST;
|
|
1341
|
+
const sortAz = new Set(options.sortAz ?? DEFAULT_SORT_AZ);
|
|
1342
|
+
const { sourceCode } = context;
|
|
1343
|
+
function hasAdjacentComment(node) {
|
|
1344
|
+
const before = sourceCode.getTokenBefore(node, { includeComments: true });
|
|
1345
|
+
let after = sourceCode.getTokenAfter(node, { includeComments: true });
|
|
1346
|
+
if (after?.type === "Comma") after = sourceCode.getTokenAfter(after, { includeComments: true });
|
|
1347
|
+
return before !== null && commentTypes.has(before.type) || after !== null && commentTypes.has(after.type);
|
|
1348
|
+
}
|
|
1349
|
+
function buildReorderedText(nodes, permutation) {
|
|
1350
|
+
const separators = nodes.slice(0, -1).map((node, index) => {
|
|
1351
|
+
const next = nodes[index + 1];
|
|
1352
|
+
return hasRange(node) && next !== void 0 && hasRange(next) ? sourceCode.text.slice(node.range[1], next.range[0]) : "";
|
|
1353
|
+
});
|
|
1354
|
+
return permutation.reduce((result, originalIndex, position) => {
|
|
1355
|
+
const text = sourceCode.getText(nodes[originalIndex]);
|
|
1356
|
+
return position === 0 ? text : `${result}${separators[position - 1] ?? ""}${text}`;
|
|
1357
|
+
}, "");
|
|
1358
|
+
}
|
|
1359
|
+
function getNodeRanges(nodes) {
|
|
1360
|
+
const ranges = [];
|
|
1361
|
+
for (const node of nodes) {
|
|
1362
|
+
if (!hasRange(node)) return void 0;
|
|
1363
|
+
ranges.push(node.range);
|
|
1364
|
+
}
|
|
1365
|
+
return ranges;
|
|
1366
|
+
}
|
|
1367
|
+
function reportWholeContainerReorder(nodes, keys, isValidOrder, reportLoc) {
|
|
1368
|
+
const permutation = computeOrderPermutation(keys, isValidOrder);
|
|
1369
|
+
if (isIdentityPermutation(permutation)) return;
|
|
1370
|
+
const firstNode = nodes[0];
|
|
1371
|
+
const lastNode = nodes[nodes.length - 1];
|
|
1372
|
+
if (firstNode === void 0 || lastNode === void 0) return;
|
|
1373
|
+
let violationIndex = 1;
|
|
1374
|
+
for (let index = 1; index < keys.length; index += 1) if (!isValidOrder(keys[index - 1] ?? "", keys[index] ?? "")) {
|
|
1375
|
+
violationIndex = index;
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
context.report({
|
|
1379
|
+
loc: reportLoc(nodes[violationIndex] ?? firstNode),
|
|
1380
|
+
messageId: "outOfOrder",
|
|
1381
|
+
data: {
|
|
1382
|
+
curr: keys[violationIndex] ?? "",
|
|
1383
|
+
prev: keys[violationIndex - 1] ?? ""
|
|
1384
|
+
},
|
|
1385
|
+
fix(fixer) {
|
|
1386
|
+
const ranges = getNodeRanges(nodes);
|
|
1387
|
+
if (ranges === void 0 || nodes.some((node) => hasAdjacentComment(node))) return null;
|
|
1388
|
+
const firstRange = ranges[0];
|
|
1389
|
+
const lastRange = ranges[ranges.length - 1];
|
|
1390
|
+
if (firstRange === void 0 || lastRange === void 0) return null;
|
|
1391
|
+
const rewritten = buildReorderedText(nodes, permutation);
|
|
1392
|
+
return fixer.replaceTextRange([firstRange[0], lastRange[1]], rewritten);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
function checkMembers(members, isValidOrder) {
|
|
1397
|
+
const keys = [];
|
|
1398
|
+
for (const member of members) {
|
|
1399
|
+
const key = getMemberKeyName(member);
|
|
1400
|
+
if (key === void 0) return;
|
|
1401
|
+
keys.push(key);
|
|
1402
|
+
}
|
|
1403
|
+
reportWholeContainerReorder(members, keys, isValidOrder, (member) => member.name.loc.start);
|
|
1404
|
+
}
|
|
1405
|
+
function checkElements(elements) {
|
|
1406
|
+
const stringValues = [];
|
|
1407
|
+
for (const element of elements) {
|
|
1408
|
+
if (!isStringNode(element.value)) return;
|
|
1409
|
+
stringValues.push(element.value);
|
|
1410
|
+
}
|
|
1411
|
+
reportWholeContainerReorder(stringValues, stringValues.map((value) => value.value), isValidSortAzOrder, (value) => value.loc.start);
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
Object(node, parent) {
|
|
1415
|
+
if (parent === void 0) return;
|
|
1416
|
+
if (parent.type === "Document") {
|
|
1417
|
+
checkMembers(node.members, (prev, curr) => isValidTopLevelOrder(prev, curr, sortFirst));
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (parent.type === "Member") {
|
|
1421
|
+
const key = getMemberKeyName(parent);
|
|
1422
|
+
if (key !== void 0 && sortAz.has(key)) checkMembers(node.members, isValidSortAzOrder);
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
Array(node, parent) {
|
|
1426
|
+
if (parent?.type !== "Member") return;
|
|
1427
|
+
const key = getMemberKeyName(parent);
|
|
1428
|
+
if (key !== void 0 && sortAz.has(key)) checkElements(node.elements);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
//#endregion
|
|
1205
1434
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
1206
1435
|
const createRule$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1207
1436
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
@@ -1424,6 +1653,7 @@ const plugin = {
|
|
|
1424
1653
|
"no-pointless-reassignment": noPointlessReassignment,
|
|
1425
1654
|
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1426
1655
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1656
|
+
"package-json-key-order": packageJsonKeyOrder,
|
|
1427
1657
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1428
1658
|
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1429
1659
|
"prefer-readonly-object-param": preferReadonlyObjectParam
|
|
@@ -1457,6 +1687,69 @@ const plugin = {
|
|
|
1457
1687
|
}
|
|
1458
1688
|
};
|
|
1459
1689
|
//#endregion
|
|
1690
|
+
//#region src/package-json-key-order.ts
|
|
1691
|
+
const SYNCPACK_CONFIG_FILENAMES = [
|
|
1692
|
+
".syncpackrc",
|
|
1693
|
+
".syncpackrc.json",
|
|
1694
|
+
".syncpackrc.yaml",
|
|
1695
|
+
".syncpackrc.yml",
|
|
1696
|
+
".syncpackrc.js",
|
|
1697
|
+
".syncpackrc.cjs",
|
|
1698
|
+
".syncpackrc.mjs",
|
|
1699
|
+
"syncpack.config.js",
|
|
1700
|
+
"syncpack.config.cjs",
|
|
1701
|
+
"syncpack.config.mjs",
|
|
1702
|
+
"syncpack.config.ts"
|
|
1703
|
+
];
|
|
1704
|
+
function packageJsonHasSyncpackKey(cwd) {
|
|
1705
|
+
try {
|
|
1706
|
+
const raw = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
1707
|
+
return typeof raw === "object" && raw !== null && "syncpack" in raw;
|
|
1708
|
+
} catch {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
/** Auto-detection signal for the `enabled: undefined` case: does this project already have syncpack configured? Mirrors the role `tryRequire`'s peer-package resolution plays for `react.ts`/`nextjs.ts`, but for a tool detected by config presence rather than by an installed package, since syncpack's own output is what this rule is emulating, not a peer this rule depends on. */
|
|
1713
|
+
function hasSyncpackConfig(cwd) {
|
|
1714
|
+
return SYNCPACK_CONFIG_FILENAMES.some((filename) => existsSync(join(cwd, filename))) || packageJsonHasSyncpackKey(cwd);
|
|
1715
|
+
}
|
|
1716
|
+
function isRecord(value) {
|
|
1717
|
+
return typeof value === "object" && value !== null;
|
|
1718
|
+
}
|
|
1719
|
+
function isJsonLanguagePlugin(value) {
|
|
1720
|
+
if (!isRecord(value)) return false;
|
|
1721
|
+
const languages = value["languages"];
|
|
1722
|
+
return isRecord(languages) && "json" in languages;
|
|
1723
|
+
}
|
|
1724
|
+
function resolveJsonPlugin(value) {
|
|
1725
|
+
if (isRecord(value) && isJsonLanguagePlugin(value["default"])) return value["default"];
|
|
1726
|
+
return isJsonLanguagePlugin(value) ? value : void 0;
|
|
1727
|
+
}
|
|
1728
|
+
const JSON_PLUGIN_INSTALL_COMMAND = "pnpm add -D @eslint/json";
|
|
1729
|
+
function buildPackageJsonKeyOrderConfig(options = {}) {
|
|
1730
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1731
|
+
if (options.enabled === false) return [];
|
|
1732
|
+
if (options.enabled === void 0 && hasSyncpackConfig(cwd)) return [];
|
|
1733
|
+
const jsonPlugin = resolveJsonPlugin(tryRequire("@eslint/json", options.requireFn));
|
|
1734
|
+
if (jsonPlugin === void 0) {
|
|
1735
|
+
if (options.enabled === true) throw new Error(`@exadev/eslint-config: package.json key ordering was explicitly requested but '@eslint/json' could not be resolved. Install it with: ${JSON_PLUGIN_INSTALL_COMMAND}`);
|
|
1736
|
+
return [];
|
|
1737
|
+
}
|
|
1738
|
+
const ruleOptions = {
|
|
1739
|
+
...options.sortFirst !== void 0 && { sortFirst: options.sortFirst },
|
|
1740
|
+
...options.sortAz !== void 0 && { sortAz: options.sortAz }
|
|
1741
|
+
};
|
|
1742
|
+
return [{
|
|
1743
|
+
files: ["**/package.json"],
|
|
1744
|
+
language: "json/json",
|
|
1745
|
+
plugins: {
|
|
1746
|
+
exadev: plugin,
|
|
1747
|
+
json: jsonPlugin
|
|
1748
|
+
},
|
|
1749
|
+
rules: { "exadev/package-json-key-order": ["error", ruleOptions] }
|
|
1750
|
+
}];
|
|
1751
|
+
}
|
|
1752
|
+
//#endregion
|
|
1460
1753
|
//#region src/recommended-type-checked.ts
|
|
1461
1754
|
const TEST_FILE_PATTERNS = "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
1462
1755
|
const JS_TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
@@ -1551,6 +1844,7 @@ function exadevConfig(options = {}, ...userConfigs) {
|
|
|
1551
1844
|
...jsonCanonicalConfig,
|
|
1552
1845
|
...buildReactConfig({ enabled: options.react }),
|
|
1553
1846
|
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
1847
|
+
...buildPackageJsonKeyOrderConfig({ enabled: options.packageJsonKeyOrder }),
|
|
1554
1848
|
...userConfigs
|
|
1555
1849
|
]);
|
|
1556
1850
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exadev-eslint-config",
|
|
3
|
-
"version": "2.14.0",
|
|
4
3
|
"description": "Shared custom ESLint rules and plugin for ExaDev projects",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
4
|
+
"version": "2.16.0",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@eslint/js": "^10.0.1",
|
|
7
|
+
"@typescript-eslint/utils": "8.67.0",
|
|
8
|
+
"eslint-plugin-jsdoc": "64.3.6",
|
|
9
|
+
"eslint-plugin-json-canonical": "2.0.0",
|
|
10
|
+
"eslint-plugin-tsdoc": "^0.5.2",
|
|
11
|
+
"ts-api-utils": "^2.5.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
15
|
+
"@commitlint/cli": "^21.2.1",
|
|
16
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
17
|
+
"@eslint/json": "2.1.0",
|
|
18
|
+
"@humanwhocodes/momoa": "^3.3.13",
|
|
19
|
+
"@next/eslint-plugin-next": "^16.3.2",
|
|
20
|
+
"@semantic-release/changelog": "^7.0.0",
|
|
21
|
+
"@semantic-release/git": "^11.0.1",
|
|
22
|
+
"@types/node": "^24.9.2",
|
|
23
|
+
"@typescript-eslint/rule-tester": "^8.67.0",
|
|
24
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
25
|
+
"eslint": "^10.8.0",
|
|
26
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
27
|
+
"eslint-plugin-react": "^7.37.5",
|
|
28
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
29
|
+
"husky": "^9.1.7",
|
|
30
|
+
"lint-staged": "^17.2.0",
|
|
31
|
+
"publint": "^0.3.21",
|
|
32
|
+
"semantic-release": "^25.0.8",
|
|
33
|
+
"tsdown": "^0.22.13",
|
|
34
|
+
"turbo": "^2.10.8",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"typescript-eslint": "^8.67.0",
|
|
37
|
+
"vitest": "^4.1.10"
|
|
38
|
+
},
|
|
8
39
|
"engines": {
|
|
9
40
|
"node": ">=20"
|
|
10
41
|
},
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
|
-
},
|
|
14
|
-
"main": "./dist/index.cjs",
|
|
15
|
-
"module": "./dist/index.js",
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
42
|
"exports": {
|
|
18
43
|
".": {
|
|
19
44
|
"types": {
|
|
@@ -29,13 +54,12 @@
|
|
|
29
54
|
],
|
|
30
55
|
"keywords": [
|
|
31
56
|
"eslint",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
57
|
+
"eslint-plugin",
|
|
58
|
+
"eslintplugin"
|
|
34
59
|
],
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"main": "./dist/index.cjs",
|
|
62
|
+
"module": "./dist/index.js",
|
|
39
63
|
"peerDependencies": {
|
|
40
64
|
"@next/eslint-plugin-next": "^16.3.2",
|
|
41
65
|
"eslint": ">=10.0.0",
|
|
@@ -59,48 +83,25 @@
|
|
|
59
83
|
"optional": true
|
|
60
84
|
}
|
|
61
85
|
},
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"@commitlint/cli": "^21.2.1",
|
|
65
|
-
"@commitlint/config-conventional": "^21.2.0",
|
|
66
|
-
"@eslint/json": "2.1.0",
|
|
67
|
-
"@next/eslint-plugin-next": "^16.3.2",
|
|
68
|
-
"@semantic-release/changelog": "^7.0.0",
|
|
69
|
-
"@semantic-release/git": "^11.0.1",
|
|
70
|
-
"@types/node": "^24.9.2",
|
|
71
|
-
"@typescript-eslint/rule-tester": "^8.67.0",
|
|
72
|
-
"@vitest/coverage-v8": "^4.1.10",
|
|
73
|
-
"eslint": "^10.8.0",
|
|
74
|
-
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
75
|
-
"eslint-plugin-react": "^7.37.5",
|
|
76
|
-
"eslint-plugin-react-hooks": "^7.1.1",
|
|
77
|
-
"husky": "^9.1.7",
|
|
78
|
-
"lint-staged": "^17.2.0",
|
|
79
|
-
"publint": "^0.3.21",
|
|
80
|
-
"semantic-release": "^25.0.8",
|
|
81
|
-
"tsdown": "^0.22.13",
|
|
82
|
-
"turbo": "^2.10.8",
|
|
83
|
-
"typescript": "^6.0.3",
|
|
84
|
-
"typescript-eslint": "^8.67.0",
|
|
85
|
-
"vitest": "^4.1.10"
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
86
88
|
},
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"eslint-plugin-jsdoc": "64.3.6",
|
|
91
|
-
"eslint-plugin-json-canonical": "1.0.0",
|
|
92
|
-
"eslint-plugin-tsdoc": "^0.5.2",
|
|
93
|
-
"ts-api-utils": "^2.5.0"
|
|
89
|
+
"repository": {
|
|
90
|
+
"type": "git",
|
|
91
|
+
"url": "git+https://github.com/ExaDev/eslint-config.git"
|
|
94
92
|
},
|
|
93
|
+
"sideEffects": false,
|
|
94
|
+
"type": "module",
|
|
95
|
+
"types": "./dist/index.d.ts",
|
|
95
96
|
"scripts": {
|
|
96
|
-
"build": "turbo run _build",
|
|
97
97
|
"_build": "tsdown",
|
|
98
|
-
"lint": "turbo run _lint",
|
|
99
98
|
"_lint": "eslint . --fix --cache --max-warnings 0",
|
|
100
|
-
"
|
|
99
|
+
"_test": "vitest run",
|
|
101
100
|
"_typecheck": "tsc -p tsconfig.json",
|
|
101
|
+
"build": "turbo run _build",
|
|
102
|
+
"lint": "turbo run _lint",
|
|
103
|
+
"release": "semantic-release",
|
|
102
104
|
"test": "turbo run _test",
|
|
103
|
-
"
|
|
104
|
-
"release": "semantic-release"
|
|
105
|
+
"typecheck": "turbo run _typecheck"
|
|
105
106
|
}
|
|
106
107
|
}
|