eslint-plugin-package-json 0.39.2 → 0.40.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/CHANGELOG.md +8 -2
- package/README.md +1 -1
- package/lib/rules/sort-collections.js +91 -72
- package/lib/rules/valid-bin.d.ts +4 -1
- package/lib/rules/valid-bin.js +50 -10
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# [0.40.0](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/compare/v0.39.2...v0.40.0) (2025-06-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **valid-bin:** add option for enforcing kebab-case ([#1113](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues/1113)) ([0024a4e](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/commit/0024a4e42c70684b81e942a9aaeace0322c02fef)), closes [#1081](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues/1081)
|
|
4
9
|
|
|
10
|
+
## [0.39.2](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/compare/v0.39.1...v0.39.2) (2025-06-15)
|
|
5
11
|
|
|
6
12
|
### Bug Fixes
|
|
7
13
|
|
|
8
|
-
|
|
14
|
+
- **deps:** update dependency detect-newline to v4 ([#875](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues/875)) ([26c08d9](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/commit/26c08d905a90729c011f3ff77d9e3784ad41cb7b))
|
|
9
15
|
|
|
10
16
|
## [0.39.1](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/compare/v0.39.0...v0.39.1) (2025-06-15)
|
|
11
17
|
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ The default settings don't conflict, and Prettier plugins can quickly fix up ord
|
|
|
138
138
|
| [sort-collections](docs/rules/sort-collections.md) | Dependencies, scripts, and configuration values must be declared in alphabetical order. | ✔️ ✅ | 🔧 | | |
|
|
139
139
|
| [unique-dependencies](docs/rules/unique-dependencies.md) | Checks a dependency isn't specified more than once (i.e. in `dependencies` and `devDependencies`) | ✔️ ✅ | | 💡 | |
|
|
140
140
|
| [valid-author](docs/rules/valid-author.md) | Enforce that the author field is a valid npm author specification | ✔️ ✅ | | | |
|
|
141
|
-
| [valid-bin](docs/rules/valid-bin.md) | Enforce that the `bin` property is valid. | ✔️ ✅ | |
|
|
141
|
+
| [valid-bin](docs/rules/valid-bin.md) | Enforce that the `bin` property is valid. | ✔️ ✅ | | 💡 | |
|
|
142
142
|
| [valid-local-dependency](docs/rules/valid-local-dependency.md) | Checks existence of local dependencies in the package.json | | | | ❌ |
|
|
143
143
|
| [valid-name](docs/rules/valid-name.md) | Enforce that package names are valid npm package names | ✔️ ✅ | | | |
|
|
144
144
|
| [valid-package-def](docs/rules/valid-package-def.md) | Enforce that package.json has all properties required by the npm spec | | | | ❌ |
|
|
@@ -1,84 +1,103 @@
|
|
|
1
1
|
import { createRule } from "../createRule.js";
|
|
2
|
-
const defaultCollections = [
|
|
3
|
-
"
|
|
4
|
-
"devDependencies",
|
|
2
|
+
const defaultCollections = /* @__PURE__ */ new Set([
|
|
3
|
+
"config",
|
|
5
4
|
"dependencies",
|
|
6
|
-
"
|
|
5
|
+
"devDependencies",
|
|
6
|
+
"exports",
|
|
7
|
+
"optionalDependencies",
|
|
7
8
|
"overrides",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
9
|
+
"peerDependencies",
|
|
10
|
+
"peerDependenciesMeta",
|
|
11
|
+
"scripts"
|
|
12
|
+
]);
|
|
11
13
|
const rule = createRule({
|
|
12
14
|
create(context) {
|
|
13
|
-
const toSort = context.options[0]
|
|
15
|
+
const toSort = context.options[0] ? new Set(context.options[0]) : defaultCollections;
|
|
14
16
|
return {
|
|
15
17
|
"JSONProperty:exit"(node) {
|
|
16
|
-
const { key, value } = node;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
18
|
+
const { key: nodeKey, value: collection } = node;
|
|
19
|
+
if (nodeKey.type !== "JSONLiteral" || collection.type !== "JSONObjectExpression") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const keyPartsReversed = [nodeKey.value];
|
|
23
|
+
for (
|
|
24
|
+
let currNode = node.parent;
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
26
|
+
currNode;
|
|
27
|
+
currNode = currNode.parent
|
|
28
|
+
) {
|
|
29
|
+
if (currNode.type === "JSONProperty" && currNode.key.type === "JSONLiteral") {
|
|
30
|
+
keyPartsReversed.push(currNode.key.value);
|
|
31
|
+
} else if (currNode.type === "JSONArrayExpression") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const key = keyPartsReversed.reverse().join(".");
|
|
36
|
+
if (!toSort.has(key)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const currentOrder = collection.properties;
|
|
40
|
+
const properties = new Set(
|
|
41
|
+
currentOrder.map(
|
|
42
|
+
(prop) => prop.key.value
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
const desiredOrder = currentOrder.slice().sort((a, b) => {
|
|
46
|
+
let aKey = a.key.value;
|
|
47
|
+
let bKey = b.key.value;
|
|
48
|
+
if (keyPartsReversed.at(-1) !== "scripts") {
|
|
49
|
+
return aKey > bKey ? 1 : -1;
|
|
50
|
+
} else {
|
|
51
|
+
let modifier = 0;
|
|
52
|
+
if (aKey.startsWith("pre") && properties.has(aKey.substring(3))) {
|
|
53
|
+
aKey = aKey.substring(3);
|
|
54
|
+
modifier -= 1;
|
|
55
|
+
} else if (aKey.startsWith("post") && properties.has(aKey.substring(4))) {
|
|
56
|
+
aKey = aKey.substring(4);
|
|
57
|
+
modifier += 1;
|
|
50
58
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
JSON.stringify(
|
|
63
|
-
desiredOrder.reduce((out, property) => {
|
|
64
|
-
out[property.key.value] = JSON.parse(
|
|
65
|
-
context.sourceCode.getText(
|
|
66
|
-
property.value
|
|
67
|
-
)
|
|
68
|
-
);
|
|
69
|
-
return out;
|
|
70
|
-
}, {}),
|
|
71
|
-
null,
|
|
72
|
-
2
|
|
73
|
-
).split("\n").join("\n ")
|
|
74
|
-
// nest indents
|
|
75
|
-
);
|
|
76
|
-
},
|
|
77
|
-
loc: collection.loc,
|
|
78
|
-
messageId: "notAlphabetized",
|
|
79
|
-
node
|
|
80
|
-
});
|
|
59
|
+
if (bKey.startsWith("pre") && properties.has(bKey.substring(3))) {
|
|
60
|
+
bKey = bKey.substring(3);
|
|
61
|
+
modifier += 1;
|
|
62
|
+
} else if (bKey.startsWith("post") && properties.has(bKey.substring(4))) {
|
|
63
|
+
bKey = bKey.substring(4);
|
|
64
|
+
modifier -= 1;
|
|
65
|
+
}
|
|
66
|
+
if (aKey === bKey) {
|
|
67
|
+
return modifier;
|
|
68
|
+
}
|
|
69
|
+
return aKey > bKey ? 1 : -1;
|
|
81
70
|
}
|
|
71
|
+
});
|
|
72
|
+
if (currentOrder.some(
|
|
73
|
+
(property, i) => desiredOrder[i] !== property
|
|
74
|
+
)) {
|
|
75
|
+
context.report({
|
|
76
|
+
data: {
|
|
77
|
+
key
|
|
78
|
+
},
|
|
79
|
+
fix(fixer) {
|
|
80
|
+
return fixer.replaceText(
|
|
81
|
+
collection,
|
|
82
|
+
JSON.stringify(
|
|
83
|
+
desiredOrder.reduce((out, property) => {
|
|
84
|
+
out[property.key.value] = JSON.parse(
|
|
85
|
+
context.sourceCode.getText(
|
|
86
|
+
property.value
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
return out;
|
|
90
|
+
}, {}),
|
|
91
|
+
null,
|
|
92
|
+
2
|
|
93
|
+
).split("\n").join("\n ")
|
|
94
|
+
// nest indents
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
loc: collection.loc,
|
|
98
|
+
messageId: "notAlphabetized",
|
|
99
|
+
node
|
|
100
|
+
});
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
};
|
package/lib/rules/valid-bin.d.ts
CHANGED
|
@@ -3,8 +3,11 @@ import * as jsonc_eslint_parser from 'jsonc-eslint-parser';
|
|
|
3
3
|
import { PackageJsonRuleContext } from '../createRule.js';
|
|
4
4
|
import 'estree';
|
|
5
5
|
|
|
6
|
+
type Options = [{
|
|
7
|
+
enforceCase: boolean;
|
|
8
|
+
}?];
|
|
6
9
|
declare const rule: {
|
|
7
|
-
create(context: PackageJsonRuleContext<
|
|
10
|
+
create(context: PackageJsonRuleContext<Options>): jsonc_eslint_parser.RuleListener;
|
|
8
11
|
meta: eslint.Rule.RuleMetaData;
|
|
9
12
|
};
|
|
10
13
|
|
package/lib/rules/valid-bin.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { kebabCase } from "change-case";
|
|
1
2
|
import { validateBin } from "package-json-validator";
|
|
2
3
|
import { createRule } from "../createRule.js";
|
|
3
4
|
import { formatErrors } from "../utils/formatErrors.js";
|
|
4
5
|
const rule = createRule({
|
|
5
6
|
create(context) {
|
|
7
|
+
const shouldEnforceCase = !!context.options[0]?.enforceCase;
|
|
6
8
|
return {
|
|
7
9
|
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=bin]"(node) {
|
|
8
10
|
const binValueNode = node.value;
|
|
@@ -10,16 +12,41 @@ const rule = createRule({
|
|
|
10
12
|
context.sourceCode.getText(binValueNode)
|
|
11
13
|
);
|
|
12
14
|
const errors = validateBin(binValue);
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
+
if (errors.length) {
|
|
16
|
+
context.report({
|
|
17
|
+
data: {
|
|
18
|
+
errors: formatErrors(errors)
|
|
19
|
+
},
|
|
20
|
+
messageId: "validationError",
|
|
21
|
+
node: binValueNode
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (shouldEnforceCase && node.value.type === "JSONObjectExpression") {
|
|
25
|
+
for (const property of node.value.properties) {
|
|
26
|
+
const key = property.key;
|
|
27
|
+
const kebabCaseKey = kebabCase(key.value);
|
|
28
|
+
if (kebabCaseKey !== key.value) {
|
|
29
|
+
context.report({
|
|
30
|
+
data: {
|
|
31
|
+
property: key.value
|
|
32
|
+
},
|
|
33
|
+
messageId: "invalidCase",
|
|
34
|
+
node: key,
|
|
35
|
+
suggest: [
|
|
36
|
+
{
|
|
37
|
+
fix: (fixer) => {
|
|
38
|
+
return fixer.replaceText(
|
|
39
|
+
key,
|
|
40
|
+
JSON.stringify(kebabCaseKey)
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
messageId: "convertToKebabCase"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
15
49
|
}
|
|
16
|
-
context.report({
|
|
17
|
-
data: {
|
|
18
|
-
errors: formatErrors(errors)
|
|
19
|
-
},
|
|
20
|
-
messageId: "validationError",
|
|
21
|
-
node: binValueNode
|
|
22
|
-
});
|
|
23
50
|
}
|
|
24
51
|
};
|
|
25
52
|
},
|
|
@@ -29,10 +56,23 @@ const rule = createRule({
|
|
|
29
56
|
description: "Enforce that the `bin` property is valid.",
|
|
30
57
|
recommended: true
|
|
31
58
|
},
|
|
59
|
+
hasSuggestions: true,
|
|
32
60
|
messages: {
|
|
61
|
+
convertToKebabCase: "Convert command name to kebab case.",
|
|
62
|
+
invalidCase: "Command name {{ property }} should be in kebab case.",
|
|
33
63
|
validationError: "Invalid bin: {{ errors }}"
|
|
34
64
|
},
|
|
35
|
-
schema: [
|
|
65
|
+
schema: [
|
|
66
|
+
{
|
|
67
|
+
properties: {
|
|
68
|
+
enforceCase: {
|
|
69
|
+
default: false,
|
|
70
|
+
type: "boolean"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
type: "object"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
36
76
|
type: "problem"
|
|
37
77
|
}
|
|
38
78
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-package-json",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Rules for consistent, readable, and valid package.json files. 🗂️",
|
|
5
5
|
"homepage": "https://github.com/JoshuaKGoldberg/eslint-plugin-package-json#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@altano/repository-tools": "^1.0.0",
|
|
49
|
+
"change-case": "^5.4.4",
|
|
49
50
|
"detect-indent": "7.0.1",
|
|
50
51
|
"detect-newline": "4.0.1",
|
|
51
52
|
"eslint-fix-utils": "^0.2.0",
|
|
@@ -78,9 +79,10 @@
|
|
|
78
79
|
"eslint-plugin-markdown": "5.1.0",
|
|
79
80
|
"eslint-plugin-n": "17.19.0",
|
|
80
81
|
"eslint-plugin-perfectionist": "4.14.0",
|
|
81
|
-
"eslint-plugin-regexp": "2.
|
|
82
|
+
"eslint-plugin-regexp": "2.9.0",
|
|
82
83
|
"eslint-plugin-yml": "1.18.0",
|
|
83
84
|
"husky": "9.1.7",
|
|
85
|
+
"jiti": "2.4.2",
|
|
84
86
|
"jsonc-eslint-parser": "2.4.0",
|
|
85
87
|
"knip": "5.60.0",
|
|
86
88
|
"lint-staged": "16.1.0",
|
|
@@ -94,14 +96,14 @@
|
|
|
94
96
|
"sentences-per-line": "0.3.0",
|
|
95
97
|
"tsup": "8.5.0",
|
|
96
98
|
"typescript": "5.8.2",
|
|
97
|
-
"typescript-eslint": "8.
|
|
99
|
+
"typescript-eslint": "8.34.0",
|
|
98
100
|
"vitest": "3.2.0"
|
|
99
101
|
},
|
|
100
102
|
"peerDependencies": {
|
|
101
103
|
"eslint": ">=8.0.0",
|
|
102
104
|
"jsonc-eslint-parser": "^2.0.0"
|
|
103
105
|
},
|
|
104
|
-
"packageManager": "pnpm@10.
|
|
106
|
+
"packageManager": "pnpm@10.12.1",
|
|
105
107
|
"engines": {
|
|
106
108
|
"node": "^=20.19.0 || >=22.12.0"
|
|
107
109
|
},
|