Skip to content

Commit e735595

Browse files
authored
feat(scripts-name-casing): add new rule (#1344)
<!-- πŸ‘‹ Hi, thanks for sending a PR to eslint-plugin-package-json! πŸ—‚ Please fill out all fields below and make sure each item is true and [x] checked. Otherwise we may not be able to review your PR. --> ## PR Checklist - [x] Addresses an existing open issue: fixes #61 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/blob/main/.github/CONTRIBUTING.md) were taken ## Overview This change adds a new rule for enforcing that keys of the `scripts` object use `kebab-case`. This is part of the new `stylelistic` config
1 parent 2372ac9 commit e735595

File tree

5 files changed

+158
-0
lines changed

5 files changed

+158
-0
lines changed

β€ŽREADME.mdβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ The default settings don't conflict, and Prettier plugins can quickly fix up ord
205205
| [require-types](docs/rules/require-types.md) | Requires the `types` property to be present. | | | | |
206206
| [require-version](docs/rules/require-version.md) | Requires the `version` property to be present. | βœ”οΈ βœ… | | | |
207207
| [restrict-dependency-ranges](docs/rules/restrict-dependency-ranges.md) | Restricts the range of dependencies to allow or disallow specific types of ranges. | | | πŸ’‘ | |
208+
| [scripts-name-casing](docs/rules/scripts-name-casing.md) | Enforce that names for `scripts` are in kebab case (optionally separated by colons). | 🎨 | | πŸ’‘ | |
208209
| [sort-collections](docs/rules/sort-collections.md) | Selected collections must be in a consistent order (lexicographical for most; lifecycle-aware for scripts). | βœ”οΈ βœ… | πŸ”§ | | |
209210
| [unique-dependencies](docs/rules/unique-dependencies.md) | Checks a dependency isn't specified more than once (i.e. in `dependencies` and `devDependencies`) | βœ”οΈ βœ… | | πŸ’‘ | |
210211
| [valid-author](docs/rules/valid-author.md) | Enforce that the `author` property is valid. | βœ”οΈ βœ… | | | |
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# scripts-name-casing
2+
3+
πŸ’Ό This rule is enabled in the 🎨 `stylistic` config.
4+
5+
πŸ’‘ This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
This rule enforces that the keys of the `scripts` object (representing the commands for each script) should be in [kebab case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case) (e.g. `"my-script"`).
10+
It also permits kebab case segments separated by colons (e.g. `"test:my-script"`).
11+
12+
Example of **incorrect** code:
13+
14+
```json
15+
{
16+
"scripts": {
17+
"invalidName": "node ./scripts/build.js",
18+
"another:invalidCommand": "node ./scripts/another.js"
19+
}
20+
}
21+
```
22+
23+
Example of **correct** code:
24+
25+
```json
26+
{
27+
"scripts": {
28+
"valid-command": "node ./scripts/build.js",
29+
"another:valid-command": "node ./scripts/another.js"
30+
}
31+
}
32+
```

β€Žsrc/plugin.tsβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { rule as orderProperties } from "./rules/order-properties.ts";
1111
import { rule as preferRepositoryShorthand } from "./rules/repository-shorthand.ts";
1212
import { rules as requireRules } from "./rules/require-properties.ts";
1313
import { rule as restrictDependencyRanges } from "./rules/restrict-dependency-ranges.ts";
14+
import { rule as scriptsNameCasing } from "./rules/scripts-name-casing.ts";
1415
import { rule as sortCollections } from "./rules/sort-collections.ts";
1516
import { rule as uniqueDependencies } from "./rules/unique-dependencies.ts";
1617
import { rule as validAuthor } from "./rules/valid-author.ts";
@@ -37,6 +38,7 @@ const rules: Record<string, PackageJsonRuleModule> = {
3738
...requireRules,
3839
"repository-shorthand": preferRepositoryShorthand,
3940
"restrict-dependency-ranges": restrictDependencyRanges,
41+
"scripts-name-casing": scriptsNameCasing,
4042
"sort-collections": sortCollections,
4143
"unique-dependencies": uniqueDependencies,
4244
...basicValidRules,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { kebabCase } from "change-case";
2+
import { AST as JsonAST } from "jsonc-eslint-parser";
3+
4+
import { createRule } from "../createRule.ts";
5+
6+
export const rule = createRule({
7+
create(context) {
8+
return {
9+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=scripts]"(
10+
node: JsonAST.JSONProperty,
11+
) {
12+
if (node.value.type === "JSONObjectExpression") {
13+
for (const property of node.value.properties) {
14+
const key = property.key as JsonAST.JSONStringLiteral;
15+
const kebabCaseKey = key.value
16+
.split(":")
17+
.map((segment) => kebabCase(segment))
18+
.join(":");
19+
if (kebabCaseKey !== key.value) {
20+
context.report({
21+
data: {
22+
property: key.value,
23+
},
24+
messageId: "invalidCase",
25+
node: key,
26+
suggest: [
27+
{
28+
data: {
29+
property: key.value,
30+
},
31+
fix: (fixer) => {
32+
return fixer.replaceText(
33+
key,
34+
JSON.stringify(kebabCaseKey),
35+
);
36+
},
37+
messageId: "convertToKebabCase",
38+
},
39+
],
40+
});
41+
}
42+
}
43+
}
44+
},
45+
};
46+
},
47+
meta: {
48+
docs: {
49+
category: "Stylistic",
50+
description:
51+
"Enforce that names for `scripts` are in kebab case (optionally separated by colons).",
52+
},
53+
hasSuggestions: true,
54+
messages: {
55+
convertToKebabCase: "Convert {{ property }} to kebab case.",
56+
invalidCase: "Script name {{ property }} should be in kebab case.",
57+
},
58+
schema: [],
59+
type: "suggestion",
60+
},
61+
name: "scripts-name-casing",
62+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { rule } from "../../rules/scripts-name-casing.ts";
2+
import { ruleTester } from "./ruleTester.ts";
3+
4+
ruleTester.run("scripts-name-casing", rule, {
5+
invalid: [
6+
{
7+
code: `{
8+
"scripts": {
9+
"silverMtZion": "silver-mt-zion.js",
10+
"NIN": "./nin.js"
11+
}
12+
}`,
13+
errors: [
14+
{
15+
data: {
16+
property: "silverMtZion",
17+
},
18+
line: 3,
19+
messageId: "invalidCase",
20+
suggestions: [
21+
{
22+
messageId: "convertToKebabCase",
23+
output: `{
24+
"scripts": {
25+
"silver-mt-zion": "silver-mt-zion.js",
26+
"NIN": "./nin.js"
27+
}
28+
}`,
29+
},
30+
],
31+
},
32+
{
33+
data: {
34+
property: "NIN",
35+
},
36+
line: 4,
37+
messageId: "invalidCase",
38+
suggestions: [
39+
{
40+
messageId: "convertToKebabCase",
41+
output: `{
42+
"scripts": {
43+
"silverMtZion": "silver-mt-zion.js",
44+
"nin": "./nin.js"
45+
}
46+
}`,
47+
},
48+
],
49+
},
50+
],
51+
},
52+
],
53+
valid: [
54+
"{}",
55+
`{ "scripts": "./silver-mt-zion.js" }`,
56+
`{ "scripts": "silver-mt-zion.js" }`,
57+
`{ "scripts": { "silver-mt-zion": "./silver-mt-zion.js" } }`,
58+
`{ "scripts": { "silver-mt-zion": "silver-mt-zion.js", "nin": "./nin.js" } }`,
59+
`{ "scripts": { "silver-mt-zion": "silver-mt-zion.js", "godspeed-you:black-emperor": "./gybe.js", "n:i:n": "./nin.js" } }`,
60+
],
61+
});

0 commit comments

Comments
Β (0)