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 CHANGED
@@ -1,11 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## [0.39.2](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/compare/v0.39.1...v0.39.2) (2025-06-15)
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
- * **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))
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
- "scripts",
4
- "devDependencies",
2
+ const defaultCollections = /* @__PURE__ */ new Set([
3
+ "config",
5
4
  "dependencies",
6
- "peerDependencies",
5
+ "devDependencies",
6
+ "exports",
7
+ "optionalDependencies",
7
8
  "overrides",
8
- "config",
9
- "exports"
10
- ];
9
+ "peerDependencies",
10
+ "peerDependenciesMeta",
11
+ "scripts"
12
+ ]);
11
13
  const rule = createRule({
12
14
  create(context) {
13
- const toSort = context.options[0] || defaultCollections;
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
- const collection = value;
18
- if (collection.type === "JSONObjectExpression" && toSort.includes(key.value)) {
19
- const currentOrder = collection.properties;
20
- const scripts = new Set(
21
- currentOrder.map(
22
- (prop) => prop.key.value
23
- )
24
- );
25
- const desiredOrder = currentOrder.slice().sort((a, b) => {
26
- let aKey = a.key.value;
27
- let bKey = b.key.value;
28
- if (key.value !== "scripts") {
29
- return aKey > bKey ? 1 : -1;
30
- } else {
31
- let modifier = 0;
32
- if (aKey.startsWith("pre") && scripts.has(aKey.substring(3))) {
33
- aKey = aKey.substring(3);
34
- modifier -= 1;
35
- } else if (aKey.startsWith("post") && scripts.has(aKey.substring(4))) {
36
- aKey = aKey.substring(4);
37
- modifier += 1;
38
- }
39
- if (bKey.startsWith("pre") && scripts.has(bKey.substring(3))) {
40
- bKey = bKey.substring(3);
41
- modifier += 1;
42
- } else if (bKey.startsWith("post") && scripts.has(bKey.substring(4))) {
43
- bKey = bKey.substring(4);
44
- modifier -= 1;
45
- }
46
- if (aKey === bKey) {
47
- return modifier;
48
- }
49
- return aKey > bKey ? 1 : -1;
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
- if (currentOrder.some(
53
- (property, i) => desiredOrder[i] !== property
54
- )) {
55
- context.report({
56
- data: {
57
- key: key.value
58
- },
59
- fix(fixer) {
60
- return fixer.replaceText(
61
- collection,
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
  };
@@ -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<unknown[]>): jsonc_eslint_parser.RuleListener;
10
+ create(context: PackageJsonRuleContext<Options>): jsonc_eslint_parser.RuleListener;
8
11
  meta: eslint.Rule.RuleMetaData;
9
12
  };
10
13
 
@@ -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 (!errors.length) {
14
- return;
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.39.2",
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.8.0",
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.33.0",
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.11.0",
106
+ "packageManager": "pnpm@10.12.1",
105
107
  "engines": {
106
108
  "node": "^=20.19.0 || >=22.12.0"
107
109
  },