exadev-eslint-config 2.14.0 → 2.15.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 +18 -1
- package/dist/index.cjs +287 -12
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +286 -11
- package/package.json +53 -52
package/README.md
CHANGED
|
@@ -166,7 +166,23 @@ Every JSON file (`**/*.json`, excluding `**/tsconfig*.json`, `**/turbo.json`, an
|
|
|
166
166
|
|
|
167
167
|
The plugin's own whitespace-collapsing rule (`no-insignificant-whitespace`, which reformats a document to a single compacted line) is deliberately not part of `configs.recommended` and is not enabled here either -- it stays available for a consumer to opt into directly for their own genuine canonicalization pass.
|
|
168
168
|
|
|
169
|
-
`**/tsconfig*.json` and `**/turbo.json` are excluded because they genuinely carry comments (TypeScript and turbo both accept them), which `@eslint/json`'s `json/json` language -- what this plugin's recommended config hard-codes -- has no concept of and fails to parse. `**/package.json`
|
|
169
|
+
`**/tsconfig*.json` and `**/turbo.json` are excluded because they genuinely carry comments (TypeScript and turbo both accept them), which `@eslint/json`'s `json/json` language -- what this plugin's recommended config hard-codes -- has no concept of and fails to parse. `**/package.json`'s own key order is excluded from this bundle specifically (its number-format/string-escaping rules still apply) since ordering is the separate, syncpack-aware concern the next section covers.
|
|
170
|
+
|
|
171
|
+
## Optional package.json key ordering
|
|
172
|
+
|
|
173
|
+
`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).
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
| Value | `options.packageJsonKeyOrder` |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `true` | Force on -- throws if `@eslint/json` isn't resolvable |
|
|
180
|
+
| `false` | Force off -- always `[]`, no resolution attempted |
|
|
181
|
+
| `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 |
|
|
182
|
+
|
|
183
|
+
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).
|
|
184
|
+
|
|
185
|
+
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
186
|
|
|
171
187
|
## Rules
|
|
172
188
|
|
|
@@ -190,6 +206,7 @@ The plugin's own whitespace-collapsing rule (`no-insignificant-whitespace`, whic
|
|
|
190
206
|
| `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
207
|
| `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
208
|
| `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. |
|
|
209
|
+
| `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
210
|
|
|
194
211
|
## Barrel policy
|
|
195
212
|
|
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"];
|
|
@@ -101,16 +102,16 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
101
102
|
return;
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
|
-
function isRecord(value) {
|
|
105
|
+
function isRecord$1(value) {
|
|
105
106
|
return typeof value === "object" && value !== null;
|
|
106
107
|
}
|
|
107
108
|
function isFlatConfig$1(value) {
|
|
108
|
-
return isRecord(value);
|
|
109
|
+
return isRecord$1(value);
|
|
109
110
|
}
|
|
110
111
|
function normalizeLegacyParserOptions(record) {
|
|
111
112
|
if (!("parserOptions" in record)) return record;
|
|
112
113
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
113
|
-
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
114
|
+
const existingLanguageOptions = isRecord$1(languageOptions) ? languageOptions : {};
|
|
114
115
|
return {
|
|
115
116
|
...rest,
|
|
116
117
|
languageOptions: {
|
|
@@ -122,10 +123,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
122
123
|
function readFlatConfig(module, path) {
|
|
123
124
|
let current = module;
|
|
124
125
|
for (const key of path) {
|
|
125
|
-
if (!isRecord(current)) return void 0;
|
|
126
|
+
if (!isRecord$1(current)) return void 0;
|
|
126
127
|
current = current[key];
|
|
127
128
|
}
|
|
128
|
-
if (!isRecord(current)) return void 0;
|
|
129
|
+
if (!isRecord$1(current)) return void 0;
|
|
129
130
|
const normalized = normalizeLegacyParserOptions(current);
|
|
130
131
|
return isFlatConfig$1(normalized) ? normalized : void 0;
|
|
131
132
|
}
|
|
@@ -139,6 +140,9 @@ function buildNextjsConfig(options = {}) {
|
|
|
139
140
|
return nextConfig === void 0 ? [] : [nextConfig];
|
|
140
141
|
}
|
|
141
142
|
//#endregion
|
|
143
|
+
//#region package.json
|
|
144
|
+
var version = "2.15.0";
|
|
145
|
+
//#endregion
|
|
142
146
|
//#region src/react.ts
|
|
143
147
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
144
148
|
const INSTALL_COMMAND = "pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y";
|
|
@@ -176,9 +180,6 @@ function buildReactConfig(options = {}) {
|
|
|
176
180
|
}));
|
|
177
181
|
}
|
|
178
182
|
//#endregion
|
|
179
|
-
//#region package.json
|
|
180
|
-
var version = "2.14.0";
|
|
181
|
-
//#endregion
|
|
182
183
|
//#region src/rules/barrel-helpers.ts
|
|
183
184
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
184
185
|
function basenameOf(filename) {
|
|
@@ -1234,6 +1235,215 @@ const noSideEffectsInIndex = {
|
|
|
1234
1235
|
}
|
|
1235
1236
|
};
|
|
1236
1237
|
//#endregion
|
|
1238
|
+
//#region src/rules/package-json-key-order.ts
|
|
1239
|
+
const DEFAULT_SORT_FIRST = [
|
|
1240
|
+
"name",
|
|
1241
|
+
"description",
|
|
1242
|
+
"version",
|
|
1243
|
+
"author"
|
|
1244
|
+
];
|
|
1245
|
+
const DEFAULT_SORT_AZ = [
|
|
1246
|
+
"bin",
|
|
1247
|
+
"contributors",
|
|
1248
|
+
"dependencies",
|
|
1249
|
+
"devDependencies",
|
|
1250
|
+
"keywords",
|
|
1251
|
+
"peerDependencies",
|
|
1252
|
+
"resolutions",
|
|
1253
|
+
"scripts"
|
|
1254
|
+
];
|
|
1255
|
+
function categorize(char) {
|
|
1256
|
+
if (char >= "0" && char <= "9") return 1;
|
|
1257
|
+
if (/[a-z]/iu.test(char)) return 2;
|
|
1258
|
+
return 0;
|
|
1259
|
+
}
|
|
1260
|
+
/** 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. */
|
|
1261
|
+
function compareSyncpackKey(a, b) {
|
|
1262
|
+
const length = Math.min(a.length, b.length);
|
|
1263
|
+
for (let index = 0; index < length; index += 1) {
|
|
1264
|
+
const charA = a[index] ?? "";
|
|
1265
|
+
const charB = b[index] ?? "";
|
|
1266
|
+
const categoryA = categorize(charA);
|
|
1267
|
+
const categoryB = categorize(charB);
|
|
1268
|
+
if (categoryA !== categoryB) return categoryA - categoryB;
|
|
1269
|
+
const foldedA = categoryA === 2 ? charA.toLowerCase() : charA;
|
|
1270
|
+
const foldedB = categoryB === 2 ? charB.toLowerCase() : charB;
|
|
1271
|
+
if (foldedA !== foldedB) return foldedA < foldedB ? -1 : 1;
|
|
1272
|
+
}
|
|
1273
|
+
return a.length - b.length;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* 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.
|
|
1277
|
+
*/
|
|
1278
|
+
function isValidTopLevelOrder(prev, curr, sortFirst) {
|
|
1279
|
+
const prevIndex = sortFirst.indexOf(prev);
|
|
1280
|
+
const currIndex = sortFirst.indexOf(curr);
|
|
1281
|
+
if (prevIndex !== -1 && currIndex !== -1) return prevIndex < currIndex;
|
|
1282
|
+
if (prevIndex !== -1) return true;
|
|
1283
|
+
if (currIndex !== -1) return false;
|
|
1284
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1285
|
+
}
|
|
1286
|
+
/** 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. */
|
|
1287
|
+
function isValidSortAzOrder(prev, curr) {
|
|
1288
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* 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).
|
|
1292
|
+
*
|
|
1293
|
+
* 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.
|
|
1294
|
+
*/
|
|
1295
|
+
function computeOrderPermutation(keys, isValidOrder) {
|
|
1296
|
+
return keys.map((_, index) => index).map((index) => ({
|
|
1297
|
+
key: keys[index] ?? "",
|
|
1298
|
+
index
|
|
1299
|
+
})).sort((a, b) => {
|
|
1300
|
+
if (isValidOrder(a.key, b.key) && isValidOrder(b.key, a.key)) return a.index - b.index;
|
|
1301
|
+
return isValidOrder(a.key, b.key) ? -1 : 1;
|
|
1302
|
+
}).map((entry) => entry.index);
|
|
1303
|
+
}
|
|
1304
|
+
function isIdentityPermutation(permutation) {
|
|
1305
|
+
return permutation.every((originalIndex, position) => originalIndex === position);
|
|
1306
|
+
}
|
|
1307
|
+
function isStringNode(node) {
|
|
1308
|
+
return node.type === "String";
|
|
1309
|
+
}
|
|
1310
|
+
function getMemberKeyName(member) {
|
|
1311
|
+
return member.name.type === "String" ? member.name.value : void 0;
|
|
1312
|
+
}
|
|
1313
|
+
function hasRange(node) {
|
|
1314
|
+
return node.range !== void 0;
|
|
1315
|
+
}
|
|
1316
|
+
const commentTypes = /* @__PURE__ */ new Set(["LineComment", "BlockComment"]);
|
|
1317
|
+
/**
|
|
1318
|
+
* 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).
|
|
1319
|
+
*
|
|
1320
|
+
* 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.
|
|
1321
|
+
*
|
|
1322
|
+
* 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.
|
|
1323
|
+
*/
|
|
1324
|
+
const packageJsonKeyOrder = {
|
|
1325
|
+
meta: {
|
|
1326
|
+
type: "layout",
|
|
1327
|
+
fixable: "code",
|
|
1328
|
+
languages: ["json/json", "json/jsonc"],
|
|
1329
|
+
docs: {
|
|
1330
|
+
recommended: false,
|
|
1331
|
+
description: "Require package.json keys to be ordered the same way `syncpack format` would order them.",
|
|
1332
|
+
url: "https://github.com/ExaDev/eslint-config/blob/main/src/rules/package-json-key-order.ts"
|
|
1333
|
+
},
|
|
1334
|
+
schema: [{
|
|
1335
|
+
type: "object",
|
|
1336
|
+
properties: {
|
|
1337
|
+
sortFirst: {
|
|
1338
|
+
type: "array",
|
|
1339
|
+
items: { type: "string" }
|
|
1340
|
+
},
|
|
1341
|
+
sortAz: {
|
|
1342
|
+
type: "array",
|
|
1343
|
+
items: { type: "string" }
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
additionalProperties: false
|
|
1347
|
+
}],
|
|
1348
|
+
defaultOptions: [{}],
|
|
1349
|
+
messages: { outOfOrder: "This key order does not match the order \"syncpack format\" would produce: \"{{curr}}\" should come before \"{{prev}}\" here." }
|
|
1350
|
+
},
|
|
1351
|
+
create(context) {
|
|
1352
|
+
const [options] = context.options;
|
|
1353
|
+
const sortFirst = options.sortFirst ?? DEFAULT_SORT_FIRST;
|
|
1354
|
+
const sortAz = new Set(options.sortAz ?? DEFAULT_SORT_AZ);
|
|
1355
|
+
const { sourceCode } = context;
|
|
1356
|
+
function hasAdjacentComment(node) {
|
|
1357
|
+
const before = sourceCode.getTokenBefore(node, { includeComments: true });
|
|
1358
|
+
let after = sourceCode.getTokenAfter(node, { includeComments: true });
|
|
1359
|
+
if (after?.type === "Comma") after = sourceCode.getTokenAfter(after, { includeComments: true });
|
|
1360
|
+
return before !== null && commentTypes.has(before.type) || after !== null && commentTypes.has(after.type);
|
|
1361
|
+
}
|
|
1362
|
+
function buildReorderedText(nodes, permutation) {
|
|
1363
|
+
const separators = nodes.slice(0, -1).map((node, index) => {
|
|
1364
|
+
const next = nodes[index + 1];
|
|
1365
|
+
return hasRange(node) && next !== void 0 && hasRange(next) ? sourceCode.text.slice(node.range[1], next.range[0]) : "";
|
|
1366
|
+
});
|
|
1367
|
+
return permutation.reduce((result, originalIndex, position) => {
|
|
1368
|
+
const text = sourceCode.getText(nodes[originalIndex]);
|
|
1369
|
+
return position === 0 ? text : `${result}${separators[position - 1] ?? ""}${text}`;
|
|
1370
|
+
}, "");
|
|
1371
|
+
}
|
|
1372
|
+
function getNodeRanges(nodes) {
|
|
1373
|
+
const ranges = [];
|
|
1374
|
+
for (const node of nodes) {
|
|
1375
|
+
if (!hasRange(node)) return void 0;
|
|
1376
|
+
ranges.push(node.range);
|
|
1377
|
+
}
|
|
1378
|
+
return ranges;
|
|
1379
|
+
}
|
|
1380
|
+
function reportWholeContainerReorder(nodes, keys, isValidOrder, reportLoc) {
|
|
1381
|
+
const permutation = computeOrderPermutation(keys, isValidOrder);
|
|
1382
|
+
if (isIdentityPermutation(permutation)) return;
|
|
1383
|
+
const firstNode = nodes[0];
|
|
1384
|
+
const lastNode = nodes[nodes.length - 1];
|
|
1385
|
+
if (firstNode === void 0 || lastNode === void 0) return;
|
|
1386
|
+
let violationIndex = 1;
|
|
1387
|
+
for (let index = 1; index < keys.length; index += 1) if (!isValidOrder(keys[index - 1] ?? "", keys[index] ?? "")) {
|
|
1388
|
+
violationIndex = index;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
context.report({
|
|
1392
|
+
loc: reportLoc(nodes[violationIndex] ?? firstNode),
|
|
1393
|
+
messageId: "outOfOrder",
|
|
1394
|
+
data: {
|
|
1395
|
+
curr: keys[violationIndex] ?? "",
|
|
1396
|
+
prev: keys[violationIndex - 1] ?? ""
|
|
1397
|
+
},
|
|
1398
|
+
fix(fixer) {
|
|
1399
|
+
const ranges = getNodeRanges(nodes);
|
|
1400
|
+
if (ranges === void 0 || nodes.some((node) => hasAdjacentComment(node))) return null;
|
|
1401
|
+
const firstRange = ranges[0];
|
|
1402
|
+
const lastRange = ranges[ranges.length - 1];
|
|
1403
|
+
if (firstRange === void 0 || lastRange === void 0) return null;
|
|
1404
|
+
const rewritten = buildReorderedText(nodes, permutation);
|
|
1405
|
+
return fixer.replaceTextRange([firstRange[0], lastRange[1]], rewritten);
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
function checkMembers(members, isValidOrder) {
|
|
1410
|
+
const keys = [];
|
|
1411
|
+
for (const member of members) {
|
|
1412
|
+
const key = getMemberKeyName(member);
|
|
1413
|
+
if (key === void 0) return;
|
|
1414
|
+
keys.push(key);
|
|
1415
|
+
}
|
|
1416
|
+
reportWholeContainerReorder(members, keys, isValidOrder, (member) => member.name.loc.start);
|
|
1417
|
+
}
|
|
1418
|
+
function checkElements(elements) {
|
|
1419
|
+
const stringValues = [];
|
|
1420
|
+
for (const element of elements) {
|
|
1421
|
+
if (!isStringNode(element.value)) return;
|
|
1422
|
+
stringValues.push(element.value);
|
|
1423
|
+
}
|
|
1424
|
+
reportWholeContainerReorder(stringValues, stringValues.map((value) => value.value), isValidSortAzOrder, (value) => value.loc.start);
|
|
1425
|
+
}
|
|
1426
|
+
return {
|
|
1427
|
+
Object(node, parent) {
|
|
1428
|
+
if (parent === void 0) return;
|
|
1429
|
+
if (parent.type === "Document") {
|
|
1430
|
+
checkMembers(node.members, (prev, curr) => isValidTopLevelOrder(prev, curr, sortFirst));
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (parent.type === "Member") {
|
|
1434
|
+
const key = getMemberKeyName(parent);
|
|
1435
|
+
if (key !== void 0 && sortAz.has(key)) checkMembers(node.members, isValidSortAzOrder);
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
Array(node, parent) {
|
|
1439
|
+
if (parent?.type !== "Member") return;
|
|
1440
|
+
const key = getMemberKeyName(parent);
|
|
1441
|
+
if (key !== void 0 && sortAz.has(key)) checkElements(node.elements);
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
//#endregion
|
|
1237
1447
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
1238
1448
|
const createRule$2 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1239
1449
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
@@ -1456,6 +1666,7 @@ const plugin = {
|
|
|
1456
1666
|
"no-pointless-reassignment": noPointlessReassignment,
|
|
1457
1667
|
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1458
1668
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1669
|
+
"package-json-key-order": packageJsonKeyOrder,
|
|
1459
1670
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1460
1671
|
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1461
1672
|
"prefer-readonly-object-param": preferReadonlyObjectParam
|
|
@@ -1489,6 +1700,69 @@ const plugin = {
|
|
|
1489
1700
|
}
|
|
1490
1701
|
};
|
|
1491
1702
|
//#endregion
|
|
1703
|
+
//#region src/package-json-key-order.ts
|
|
1704
|
+
const SYNCPACK_CONFIG_FILENAMES = [
|
|
1705
|
+
".syncpackrc",
|
|
1706
|
+
".syncpackrc.json",
|
|
1707
|
+
".syncpackrc.yaml",
|
|
1708
|
+
".syncpackrc.yml",
|
|
1709
|
+
".syncpackrc.js",
|
|
1710
|
+
".syncpackrc.cjs",
|
|
1711
|
+
".syncpackrc.mjs",
|
|
1712
|
+
"syncpack.config.js",
|
|
1713
|
+
"syncpack.config.cjs",
|
|
1714
|
+
"syncpack.config.mjs",
|
|
1715
|
+
"syncpack.config.ts"
|
|
1716
|
+
];
|
|
1717
|
+
function packageJsonHasSyncpackKey(cwd) {
|
|
1718
|
+
try {
|
|
1719
|
+
const raw = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(cwd, "package.json"), "utf8"));
|
|
1720
|
+
return typeof raw === "object" && raw !== null && "syncpack" in raw;
|
|
1721
|
+
} catch {
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
/** 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. */
|
|
1726
|
+
function hasSyncpackConfig(cwd) {
|
|
1727
|
+
return SYNCPACK_CONFIG_FILENAMES.some((filename) => (0, node_fs.existsSync)((0, node_path.join)(cwd, filename))) || packageJsonHasSyncpackKey(cwd);
|
|
1728
|
+
}
|
|
1729
|
+
function isRecord(value) {
|
|
1730
|
+
return typeof value === "object" && value !== null;
|
|
1731
|
+
}
|
|
1732
|
+
function isJsonLanguagePlugin(value) {
|
|
1733
|
+
if (!isRecord(value)) return false;
|
|
1734
|
+
const languages = value["languages"];
|
|
1735
|
+
return isRecord(languages) && "json" in languages;
|
|
1736
|
+
}
|
|
1737
|
+
function resolveJsonPlugin(value) {
|
|
1738
|
+
if (isRecord(value) && isJsonLanguagePlugin(value["default"])) return value["default"];
|
|
1739
|
+
return isJsonLanguagePlugin(value) ? value : void 0;
|
|
1740
|
+
}
|
|
1741
|
+
const JSON_PLUGIN_INSTALL_COMMAND = "pnpm add -D @eslint/json";
|
|
1742
|
+
function buildPackageJsonKeyOrderConfig(options = {}) {
|
|
1743
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1744
|
+
if (options.enabled === false) return [];
|
|
1745
|
+
if (options.enabled === void 0 && hasSyncpackConfig(cwd)) return [];
|
|
1746
|
+
const jsonPlugin = resolveJsonPlugin(tryRequire("@eslint/json", options.requireFn));
|
|
1747
|
+
if (jsonPlugin === void 0) {
|
|
1748
|
+
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}`);
|
|
1749
|
+
return [];
|
|
1750
|
+
}
|
|
1751
|
+
const ruleOptions = {
|
|
1752
|
+
...options.sortFirst !== void 0 && { sortFirst: options.sortFirst },
|
|
1753
|
+
...options.sortAz !== void 0 && { sortAz: options.sortAz }
|
|
1754
|
+
};
|
|
1755
|
+
return [{
|
|
1756
|
+
files: ["**/package.json"],
|
|
1757
|
+
language: "json/json",
|
|
1758
|
+
plugins: {
|
|
1759
|
+
exadev: plugin,
|
|
1760
|
+
json: jsonPlugin
|
|
1761
|
+
},
|
|
1762
|
+
rules: { "exadev/package-json-key-order": ["error", ruleOptions] }
|
|
1763
|
+
}];
|
|
1764
|
+
}
|
|
1765
|
+
//#endregion
|
|
1492
1766
|
//#region src/recommended-type-checked.ts
|
|
1493
1767
|
const TEST_FILE_PATTERNS = "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
1494
1768
|
const JS_TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
@@ -1583,6 +1857,7 @@ function exadevConfig(options = {}, ...userConfigs) {
|
|
|
1583
1857
|
...jsonCanonicalConfig,
|
|
1584
1858
|
...buildReactConfig({ enabled: options.react }),
|
|
1585
1859
|
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
1860
|
+
...buildPackageJsonKeyOrderConfig({ enabled: options.packageJsonKeyOrder }),
|
|
1586
1861
|
...userConfigs
|
|
1587
1862
|
]);
|
|
1588
1863
|
}
|
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"];
|
|
@@ -69,16 +70,16 @@ function tryRequire(specifier, requireFn = nodeRequire) {
|
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
|
-
function isRecord(value) {
|
|
73
|
+
function isRecord$1(value) {
|
|
73
74
|
return typeof value === "object" && value !== null;
|
|
74
75
|
}
|
|
75
76
|
function isFlatConfig$1(value) {
|
|
76
|
-
return isRecord(value);
|
|
77
|
+
return isRecord$1(value);
|
|
77
78
|
}
|
|
78
79
|
function normalizeLegacyParserOptions(record) {
|
|
79
80
|
if (!("parserOptions" in record)) return record;
|
|
80
81
|
const { parserOptions, languageOptions, ...rest } = record;
|
|
81
|
-
const existingLanguageOptions = isRecord(languageOptions) ? languageOptions : {};
|
|
82
|
+
const existingLanguageOptions = isRecord$1(languageOptions) ? languageOptions : {};
|
|
82
83
|
return {
|
|
83
84
|
...rest,
|
|
84
85
|
languageOptions: {
|
|
@@ -90,10 +91,10 @@ function normalizeLegacyParserOptions(record) {
|
|
|
90
91
|
function readFlatConfig(module, path) {
|
|
91
92
|
let current = module;
|
|
92
93
|
for (const key of path) {
|
|
93
|
-
if (!isRecord(current)) return void 0;
|
|
94
|
+
if (!isRecord$1(current)) return void 0;
|
|
94
95
|
current = current[key];
|
|
95
96
|
}
|
|
96
|
-
if (!isRecord(current)) return void 0;
|
|
97
|
+
if (!isRecord$1(current)) return void 0;
|
|
97
98
|
const normalized = normalizeLegacyParserOptions(current);
|
|
98
99
|
return isFlatConfig$1(normalized) ? normalized : void 0;
|
|
99
100
|
}
|
|
@@ -107,6 +108,9 @@ function buildNextjsConfig(options = {}) {
|
|
|
107
108
|
return nextConfig === void 0 ? [] : [nextConfig];
|
|
108
109
|
}
|
|
109
110
|
//#endregion
|
|
111
|
+
//#region package.json
|
|
112
|
+
var version = "2.15.0";
|
|
113
|
+
//#endregion
|
|
110
114
|
//#region src/react.ts
|
|
111
115
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
112
116
|
const INSTALL_COMMAND = "pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y";
|
|
@@ -144,9 +148,6 @@ function buildReactConfig(options = {}) {
|
|
|
144
148
|
}));
|
|
145
149
|
}
|
|
146
150
|
//#endregion
|
|
147
|
-
//#region package.json
|
|
148
|
-
var version = "2.14.0";
|
|
149
|
-
//#endregion
|
|
150
151
|
//#region src/rules/barrel-helpers.ts
|
|
151
152
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
152
153
|
function basenameOf(filename) {
|
|
@@ -1202,6 +1203,215 @@ const noSideEffectsInIndex = {
|
|
|
1202
1203
|
}
|
|
1203
1204
|
};
|
|
1204
1205
|
//#endregion
|
|
1206
|
+
//#region src/rules/package-json-key-order.ts
|
|
1207
|
+
const DEFAULT_SORT_FIRST = [
|
|
1208
|
+
"name",
|
|
1209
|
+
"description",
|
|
1210
|
+
"version",
|
|
1211
|
+
"author"
|
|
1212
|
+
];
|
|
1213
|
+
const DEFAULT_SORT_AZ = [
|
|
1214
|
+
"bin",
|
|
1215
|
+
"contributors",
|
|
1216
|
+
"dependencies",
|
|
1217
|
+
"devDependencies",
|
|
1218
|
+
"keywords",
|
|
1219
|
+
"peerDependencies",
|
|
1220
|
+
"resolutions",
|
|
1221
|
+
"scripts"
|
|
1222
|
+
];
|
|
1223
|
+
function categorize(char) {
|
|
1224
|
+
if (char >= "0" && char <= "9") return 1;
|
|
1225
|
+
if (/[a-z]/iu.test(char)) return 2;
|
|
1226
|
+
return 0;
|
|
1227
|
+
}
|
|
1228
|
+
/** 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. */
|
|
1229
|
+
function compareSyncpackKey(a, b) {
|
|
1230
|
+
const length = Math.min(a.length, b.length);
|
|
1231
|
+
for (let index = 0; index < length; index += 1) {
|
|
1232
|
+
const charA = a[index] ?? "";
|
|
1233
|
+
const charB = b[index] ?? "";
|
|
1234
|
+
const categoryA = categorize(charA);
|
|
1235
|
+
const categoryB = categorize(charB);
|
|
1236
|
+
if (categoryA !== categoryB) return categoryA - categoryB;
|
|
1237
|
+
const foldedA = categoryA === 2 ? charA.toLowerCase() : charA;
|
|
1238
|
+
const foldedB = categoryB === 2 ? charB.toLowerCase() : charB;
|
|
1239
|
+
if (foldedA !== foldedB) return foldedA < foldedB ? -1 : 1;
|
|
1240
|
+
}
|
|
1241
|
+
return a.length - b.length;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* 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.
|
|
1245
|
+
*/
|
|
1246
|
+
function isValidTopLevelOrder(prev, curr, sortFirst) {
|
|
1247
|
+
const prevIndex = sortFirst.indexOf(prev);
|
|
1248
|
+
const currIndex = sortFirst.indexOf(curr);
|
|
1249
|
+
if (prevIndex !== -1 && currIndex !== -1) return prevIndex < currIndex;
|
|
1250
|
+
if (prevIndex !== -1) return true;
|
|
1251
|
+
if (currIndex !== -1) return false;
|
|
1252
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1253
|
+
}
|
|
1254
|
+
/** 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. */
|
|
1255
|
+
function isValidSortAzOrder(prev, curr) {
|
|
1256
|
+
return compareSyncpackKey(prev, curr) <= 0;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* 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).
|
|
1260
|
+
*
|
|
1261
|
+
* 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.
|
|
1262
|
+
*/
|
|
1263
|
+
function computeOrderPermutation(keys, isValidOrder) {
|
|
1264
|
+
return keys.map((_, index) => index).map((index) => ({
|
|
1265
|
+
key: keys[index] ?? "",
|
|
1266
|
+
index
|
|
1267
|
+
})).sort((a, b) => {
|
|
1268
|
+
if (isValidOrder(a.key, b.key) && isValidOrder(b.key, a.key)) return a.index - b.index;
|
|
1269
|
+
return isValidOrder(a.key, b.key) ? -1 : 1;
|
|
1270
|
+
}).map((entry) => entry.index);
|
|
1271
|
+
}
|
|
1272
|
+
function isIdentityPermutation(permutation) {
|
|
1273
|
+
return permutation.every((originalIndex, position) => originalIndex === position);
|
|
1274
|
+
}
|
|
1275
|
+
function isStringNode(node) {
|
|
1276
|
+
return node.type === "String";
|
|
1277
|
+
}
|
|
1278
|
+
function getMemberKeyName(member) {
|
|
1279
|
+
return member.name.type === "String" ? member.name.value : void 0;
|
|
1280
|
+
}
|
|
1281
|
+
function hasRange(node) {
|
|
1282
|
+
return node.range !== void 0;
|
|
1283
|
+
}
|
|
1284
|
+
const commentTypes = /* @__PURE__ */ new Set(["LineComment", "BlockComment"]);
|
|
1285
|
+
/**
|
|
1286
|
+
* 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).
|
|
1287
|
+
*
|
|
1288
|
+
* 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.
|
|
1289
|
+
*
|
|
1290
|
+
* 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.
|
|
1291
|
+
*/
|
|
1292
|
+
const packageJsonKeyOrder = {
|
|
1293
|
+
meta: {
|
|
1294
|
+
type: "layout",
|
|
1295
|
+
fixable: "code",
|
|
1296
|
+
languages: ["json/json", "json/jsonc"],
|
|
1297
|
+
docs: {
|
|
1298
|
+
recommended: false,
|
|
1299
|
+
description: "Require package.json keys to be ordered the same way `syncpack format` would order them.",
|
|
1300
|
+
url: "https://github.com/ExaDev/eslint-config/blob/main/src/rules/package-json-key-order.ts"
|
|
1301
|
+
},
|
|
1302
|
+
schema: [{
|
|
1303
|
+
type: "object",
|
|
1304
|
+
properties: {
|
|
1305
|
+
sortFirst: {
|
|
1306
|
+
type: "array",
|
|
1307
|
+
items: { type: "string" }
|
|
1308
|
+
},
|
|
1309
|
+
sortAz: {
|
|
1310
|
+
type: "array",
|
|
1311
|
+
items: { type: "string" }
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
additionalProperties: false
|
|
1315
|
+
}],
|
|
1316
|
+
defaultOptions: [{}],
|
|
1317
|
+
messages: { outOfOrder: "This key order does not match the order \"syncpack format\" would produce: \"{{curr}}\" should come before \"{{prev}}\" here." }
|
|
1318
|
+
},
|
|
1319
|
+
create(context) {
|
|
1320
|
+
const [options] = context.options;
|
|
1321
|
+
const sortFirst = options.sortFirst ?? DEFAULT_SORT_FIRST;
|
|
1322
|
+
const sortAz = new Set(options.sortAz ?? DEFAULT_SORT_AZ);
|
|
1323
|
+
const { sourceCode } = context;
|
|
1324
|
+
function hasAdjacentComment(node) {
|
|
1325
|
+
const before = sourceCode.getTokenBefore(node, { includeComments: true });
|
|
1326
|
+
let after = sourceCode.getTokenAfter(node, { includeComments: true });
|
|
1327
|
+
if (after?.type === "Comma") after = sourceCode.getTokenAfter(after, { includeComments: true });
|
|
1328
|
+
return before !== null && commentTypes.has(before.type) || after !== null && commentTypes.has(after.type);
|
|
1329
|
+
}
|
|
1330
|
+
function buildReorderedText(nodes, permutation) {
|
|
1331
|
+
const separators = nodes.slice(0, -1).map((node, index) => {
|
|
1332
|
+
const next = nodes[index + 1];
|
|
1333
|
+
return hasRange(node) && next !== void 0 && hasRange(next) ? sourceCode.text.slice(node.range[1], next.range[0]) : "";
|
|
1334
|
+
});
|
|
1335
|
+
return permutation.reduce((result, originalIndex, position) => {
|
|
1336
|
+
const text = sourceCode.getText(nodes[originalIndex]);
|
|
1337
|
+
return position === 0 ? text : `${result}${separators[position - 1] ?? ""}${text}`;
|
|
1338
|
+
}, "");
|
|
1339
|
+
}
|
|
1340
|
+
function getNodeRanges(nodes) {
|
|
1341
|
+
const ranges = [];
|
|
1342
|
+
for (const node of nodes) {
|
|
1343
|
+
if (!hasRange(node)) return void 0;
|
|
1344
|
+
ranges.push(node.range);
|
|
1345
|
+
}
|
|
1346
|
+
return ranges;
|
|
1347
|
+
}
|
|
1348
|
+
function reportWholeContainerReorder(nodes, keys, isValidOrder, reportLoc) {
|
|
1349
|
+
const permutation = computeOrderPermutation(keys, isValidOrder);
|
|
1350
|
+
if (isIdentityPermutation(permutation)) return;
|
|
1351
|
+
const firstNode = nodes[0];
|
|
1352
|
+
const lastNode = nodes[nodes.length - 1];
|
|
1353
|
+
if (firstNode === void 0 || lastNode === void 0) return;
|
|
1354
|
+
let violationIndex = 1;
|
|
1355
|
+
for (let index = 1; index < keys.length; index += 1) if (!isValidOrder(keys[index - 1] ?? "", keys[index] ?? "")) {
|
|
1356
|
+
violationIndex = index;
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
context.report({
|
|
1360
|
+
loc: reportLoc(nodes[violationIndex] ?? firstNode),
|
|
1361
|
+
messageId: "outOfOrder",
|
|
1362
|
+
data: {
|
|
1363
|
+
curr: keys[violationIndex] ?? "",
|
|
1364
|
+
prev: keys[violationIndex - 1] ?? ""
|
|
1365
|
+
},
|
|
1366
|
+
fix(fixer) {
|
|
1367
|
+
const ranges = getNodeRanges(nodes);
|
|
1368
|
+
if (ranges === void 0 || nodes.some((node) => hasAdjacentComment(node))) return null;
|
|
1369
|
+
const firstRange = ranges[0];
|
|
1370
|
+
const lastRange = ranges[ranges.length - 1];
|
|
1371
|
+
if (firstRange === void 0 || lastRange === void 0) return null;
|
|
1372
|
+
const rewritten = buildReorderedText(nodes, permutation);
|
|
1373
|
+
return fixer.replaceTextRange([firstRange[0], lastRange[1]], rewritten);
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
function checkMembers(members, isValidOrder) {
|
|
1378
|
+
const keys = [];
|
|
1379
|
+
for (const member of members) {
|
|
1380
|
+
const key = getMemberKeyName(member);
|
|
1381
|
+
if (key === void 0) return;
|
|
1382
|
+
keys.push(key);
|
|
1383
|
+
}
|
|
1384
|
+
reportWholeContainerReorder(members, keys, isValidOrder, (member) => member.name.loc.start);
|
|
1385
|
+
}
|
|
1386
|
+
function checkElements(elements) {
|
|
1387
|
+
const stringValues = [];
|
|
1388
|
+
for (const element of elements) {
|
|
1389
|
+
if (!isStringNode(element.value)) return;
|
|
1390
|
+
stringValues.push(element.value);
|
|
1391
|
+
}
|
|
1392
|
+
reportWholeContainerReorder(stringValues, stringValues.map((value) => value.value), isValidSortAzOrder, (value) => value.loc.start);
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
Object(node, parent) {
|
|
1396
|
+
if (parent === void 0) return;
|
|
1397
|
+
if (parent.type === "Document") {
|
|
1398
|
+
checkMembers(node.members, (prev, curr) => isValidTopLevelOrder(prev, curr, sortFirst));
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (parent.type === "Member") {
|
|
1402
|
+
const key = getMemberKeyName(parent);
|
|
1403
|
+
if (key !== void 0 && sortAz.has(key)) checkMembers(node.members, isValidSortAzOrder);
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
Array(node, parent) {
|
|
1407
|
+
if (parent?.type !== "Member") return;
|
|
1408
|
+
const key = getMemberKeyName(parent);
|
|
1409
|
+
if (key !== void 0 && sortAz.has(key)) checkElements(node.elements);
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
//#endregion
|
|
1205
1415
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
1206
1416
|
const createRule$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1207
1417
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
@@ -1424,6 +1634,7 @@ const plugin = {
|
|
|
1424
1634
|
"no-pointless-reassignment": noPointlessReassignment,
|
|
1425
1635
|
"no-set-instanceof-mutation": noSetInstanceofMutation,
|
|
1426
1636
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1637
|
+
"package-json-key-order": packageJsonKeyOrder,
|
|
1427
1638
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
1428
1639
|
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1429
1640
|
"prefer-readonly-object-param": preferReadonlyObjectParam
|
|
@@ -1457,6 +1668,69 @@ const plugin = {
|
|
|
1457
1668
|
}
|
|
1458
1669
|
};
|
|
1459
1670
|
//#endregion
|
|
1671
|
+
//#region src/package-json-key-order.ts
|
|
1672
|
+
const SYNCPACK_CONFIG_FILENAMES = [
|
|
1673
|
+
".syncpackrc",
|
|
1674
|
+
".syncpackrc.json",
|
|
1675
|
+
".syncpackrc.yaml",
|
|
1676
|
+
".syncpackrc.yml",
|
|
1677
|
+
".syncpackrc.js",
|
|
1678
|
+
".syncpackrc.cjs",
|
|
1679
|
+
".syncpackrc.mjs",
|
|
1680
|
+
"syncpack.config.js",
|
|
1681
|
+
"syncpack.config.cjs",
|
|
1682
|
+
"syncpack.config.mjs",
|
|
1683
|
+
"syncpack.config.ts"
|
|
1684
|
+
];
|
|
1685
|
+
function packageJsonHasSyncpackKey(cwd) {
|
|
1686
|
+
try {
|
|
1687
|
+
const raw = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
1688
|
+
return typeof raw === "object" && raw !== null && "syncpack" in raw;
|
|
1689
|
+
} catch {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
/** 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. */
|
|
1694
|
+
function hasSyncpackConfig(cwd) {
|
|
1695
|
+
return SYNCPACK_CONFIG_FILENAMES.some((filename) => existsSync(join(cwd, filename))) || packageJsonHasSyncpackKey(cwd);
|
|
1696
|
+
}
|
|
1697
|
+
function isRecord(value) {
|
|
1698
|
+
return typeof value === "object" && value !== null;
|
|
1699
|
+
}
|
|
1700
|
+
function isJsonLanguagePlugin(value) {
|
|
1701
|
+
if (!isRecord(value)) return false;
|
|
1702
|
+
const languages = value["languages"];
|
|
1703
|
+
return isRecord(languages) && "json" in languages;
|
|
1704
|
+
}
|
|
1705
|
+
function resolveJsonPlugin(value) {
|
|
1706
|
+
if (isRecord(value) && isJsonLanguagePlugin(value["default"])) return value["default"];
|
|
1707
|
+
return isJsonLanguagePlugin(value) ? value : void 0;
|
|
1708
|
+
}
|
|
1709
|
+
const JSON_PLUGIN_INSTALL_COMMAND = "pnpm add -D @eslint/json";
|
|
1710
|
+
function buildPackageJsonKeyOrderConfig(options = {}) {
|
|
1711
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1712
|
+
if (options.enabled === false) return [];
|
|
1713
|
+
if (options.enabled === void 0 && hasSyncpackConfig(cwd)) return [];
|
|
1714
|
+
const jsonPlugin = resolveJsonPlugin(tryRequire("@eslint/json", options.requireFn));
|
|
1715
|
+
if (jsonPlugin === void 0) {
|
|
1716
|
+
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}`);
|
|
1717
|
+
return [];
|
|
1718
|
+
}
|
|
1719
|
+
const ruleOptions = {
|
|
1720
|
+
...options.sortFirst !== void 0 && { sortFirst: options.sortFirst },
|
|
1721
|
+
...options.sortAz !== void 0 && { sortAz: options.sortAz }
|
|
1722
|
+
};
|
|
1723
|
+
return [{
|
|
1724
|
+
files: ["**/package.json"],
|
|
1725
|
+
language: "json/json",
|
|
1726
|
+
plugins: {
|
|
1727
|
+
exadev: plugin,
|
|
1728
|
+
json: jsonPlugin
|
|
1729
|
+
},
|
|
1730
|
+
rules: { "exadev/package-json-key-order": ["error", ruleOptions] }
|
|
1731
|
+
}];
|
|
1732
|
+
}
|
|
1733
|
+
//#endregion
|
|
1460
1734
|
//#region src/recommended-type-checked.ts
|
|
1461
1735
|
const TEST_FILE_PATTERNS = "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
1462
1736
|
const JS_TS_FILE_PATTERNS = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
@@ -1551,6 +1825,7 @@ function exadevConfig(options = {}, ...userConfigs) {
|
|
|
1551
1825
|
...jsonCanonicalConfig,
|
|
1552
1826
|
...buildReactConfig({ enabled: options.react }),
|
|
1553
1827
|
...buildNextjsConfig({ enabled: options.nextjs }),
|
|
1828
|
+
...buildPackageJsonKeyOrderConfig({ enabled: options.packageJsonKeyOrder }),
|
|
1554
1829
|
...userConfigs
|
|
1555
1830
|
]);
|
|
1556
1831
|
}
|
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.15.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": "1.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
|
}
|