@dvukovic/style-guide 0.3.118 → 0.3.120
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/package.json +4 -1
- package/src/eslint/configs/package-json.js +32 -0
- package/src/eslint/configs/package-json.test.js +105 -1
- package/src/eslint/plugins/package-json.js +6 -2
- package/src/eslint/rules/no-restricted-dependencies/no-restricted-dependencies.js +29 -12
- package/src/eslint/rules/no-restricted-dependencies/no-restricted-dependencies.test.js +116 -39
- package/src/eslint/rules/require-properties/require-properties.js +76 -0
- package/src/eslint/rules/require-properties/require-properties.test.js +164 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dvukovic/style-guide",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.120",
|
|
4
4
|
"description": "My own style guide",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -111,6 +111,9 @@
|
|
|
111
111
|
"engines": {
|
|
112
112
|
"node": ">=24.0.0"
|
|
113
113
|
},
|
|
114
|
+
"volta": {
|
|
115
|
+
"node": "24.4.1"
|
|
116
|
+
},
|
|
114
117
|
"publishConfig": {
|
|
115
118
|
"access": "public",
|
|
116
119
|
"registry": "https://registry.npmjs.org"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { packageJson as packageJsonRules } from "../plugins/package-json.js"
|
|
2
2
|
|
|
3
|
+
const DEFAULT_WORKSPACE_PATTERNS = ["**/packages/**/package.json", "**/apps/**/package.json"]
|
|
4
|
+
|
|
3
5
|
export const packageJsonConfigs = [packageJsonRules]
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -15,3 +17,33 @@ export function packageJson(config) {
|
|
|
15
17
|
...config,
|
|
16
18
|
}
|
|
17
19
|
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Package.json ESLint configuration for monorepos
|
|
23
|
+
*
|
|
24
|
+
* Returns two configs:
|
|
25
|
+
*
|
|
26
|
+
* - Root package.json: all rules including volta.node requirement
|
|
27
|
+
* - Nested packages: same rules but no volta.node requirement
|
|
28
|
+
*
|
|
29
|
+
* @param {{ workspacePatterns?: string[] } & import("@eslint/config-helpers").ConfigWithExtends} [config]
|
|
30
|
+
* @returns {import("@eslint/config-helpers").ConfigWithExtends[]}
|
|
31
|
+
*/
|
|
32
|
+
export function packageJsonWorkspace(config) {
|
|
33
|
+
const { workspacePatterns = DEFAULT_WORKSPACE_PATTERNS, ...rest } = config ?? {}
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
extends: [...packageJsonConfigs, ...(rest?.extends ?? [])],
|
|
38
|
+
files: ["package.json"],
|
|
39
|
+
...rest,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
extends: [...packageJsonConfigs],
|
|
43
|
+
files: workspacePatterns,
|
|
44
|
+
rules: {
|
|
45
|
+
"dvukovic/require-properties": "off",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ESLint } from "eslint"
|
|
2
|
+
import { defineConfig } from "eslint/config"
|
|
2
3
|
|
|
3
|
-
import { packageJsonConfigs } from "./package-json.js"
|
|
4
|
+
import { packageJsonConfigs, packageJsonWorkspace } from "./package-json.js"
|
|
4
5
|
|
|
5
6
|
const eslint = new ESLint({
|
|
6
7
|
overrideConfig: [...packageJsonConfigs, { files: ["**/package.json"] }],
|
|
@@ -48,3 +49,106 @@ describe("package-json", () => {
|
|
|
48
49
|
expect(results[0]?.errorCount).toBeGreaterThan(0)
|
|
49
50
|
})
|
|
50
51
|
})
|
|
52
|
+
|
|
53
|
+
describe("packageJsonWorkspace", () => {
|
|
54
|
+
test("returns array of two configs", () => {
|
|
55
|
+
const configs = packageJsonWorkspace()
|
|
56
|
+
|
|
57
|
+
expect(configs).toHaveLength(2)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("root config targets package.json", () => {
|
|
61
|
+
const configs = packageJsonWorkspace()
|
|
62
|
+
|
|
63
|
+
expect(configs[0].files).toEqual(["package.json"])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("nested config targets default workspace patterns", () => {
|
|
67
|
+
const configs = packageJsonWorkspace()
|
|
68
|
+
|
|
69
|
+
expect(configs[1].files).toEqual(["**/packages/**/package.json", "**/apps/**/package.json"])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("nested config disables require-properties rule", () => {
|
|
73
|
+
const configs = packageJsonWorkspace()
|
|
74
|
+
|
|
75
|
+
expect(configs[1].rules).toEqual({
|
|
76
|
+
"dvukovic/require-properties": "off",
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("accepts custom workspace patterns", () => {
|
|
81
|
+
const configs = packageJsonWorkspace({
|
|
82
|
+
workspacePatterns: ["**/libs/**/package.json"],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(configs[1].files).toEqual(["**/libs/**/package.json"])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("passes through extends to root config", () => {
|
|
89
|
+
const customExtends = [{ rules: { "custom/rule": "error" } }]
|
|
90
|
+
const configs = packageJsonWorkspace({ extends: customExtends })
|
|
91
|
+
|
|
92
|
+
expect(configs[0].extends).toContain(customExtends[0])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("root config requires volta.node", async () => {
|
|
96
|
+
const workspaceEslint = new ESLint({
|
|
97
|
+
overrideConfig: defineConfig(...packageJsonWorkspace()),
|
|
98
|
+
overrideConfigFile: true,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const packageWithoutVolta = JSON.stringify(
|
|
102
|
+
{
|
|
103
|
+
author: "Test Author",
|
|
104
|
+
description: "A test package",
|
|
105
|
+
engines: { node: ">=24.0.0" },
|
|
106
|
+
license: "MIT",
|
|
107
|
+
name: "test-package",
|
|
108
|
+
repository: { type: "git", url: "https://github.com/test/test" },
|
|
109
|
+
version: "1.0.0",
|
|
110
|
+
},
|
|
111
|
+
null,
|
|
112
|
+
4,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const results = await workspaceEslint.lintText(packageWithoutVolta, {
|
|
116
|
+
filePath: "package.json",
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const hasVoltaError = results[0]?.messages.some((message) => {
|
|
120
|
+
return message.ruleId === "dvukovic/require-properties"
|
|
121
|
+
})
|
|
122
|
+
expect(hasVoltaError).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("nested config does not require volta.node", async () => {
|
|
126
|
+
const workspaceEslint = new ESLint({
|
|
127
|
+
overrideConfig: defineConfig(...packageJsonWorkspace()),
|
|
128
|
+
overrideConfigFile: true,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const packageWithoutVolta = JSON.stringify(
|
|
132
|
+
{
|
|
133
|
+
author: "Test Author",
|
|
134
|
+
description: "A test package",
|
|
135
|
+
engines: { node: ">=24.0.0" },
|
|
136
|
+
license: "MIT",
|
|
137
|
+
name: "test-package",
|
|
138
|
+
repository: { type: "git", url: "https://github.com/test/test" },
|
|
139
|
+
version: "1.0.0",
|
|
140
|
+
},
|
|
141
|
+
null,
|
|
142
|
+
4,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const results = await workspaceEslint.lintText(packageWithoutVolta, {
|
|
146
|
+
filePath: "packages/my-package/package.json",
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const hasVoltaError = results[0]?.messages.some((message) => {
|
|
150
|
+
return message.ruleId === "dvukovic/require-properties"
|
|
151
|
+
})
|
|
152
|
+
expect(hasVoltaError).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -2,6 +2,7 @@ import plugin from "eslint-plugin-package-json"
|
|
|
2
2
|
import jsoncParser from "jsonc-eslint-parser"
|
|
3
3
|
|
|
4
4
|
import { noRestrictedDependencies } from "../rules/no-restricted-dependencies/no-restricted-dependencies.js"
|
|
5
|
+
import { requireProperties } from "../rules/require-properties/require-properties.js"
|
|
5
6
|
import { validEnginesNode } from "../rules/valid-engines-node/valid-engines-node.js"
|
|
6
7
|
|
|
7
8
|
/** @type {import("@eslint/config-helpers").Config} */
|
|
@@ -13,6 +14,7 @@ export const packageJson = {
|
|
|
13
14
|
dvukovic: {
|
|
14
15
|
rules: {
|
|
15
16
|
"no-restricted-dependencies": noRestrictedDependencies,
|
|
17
|
+
"require-properties": requireProperties,
|
|
16
18
|
"valid-engines-node": validEnginesNode,
|
|
17
19
|
},
|
|
18
20
|
},
|
|
@@ -22,19 +24,21 @@ export const packageJson = {
|
|
|
22
24
|
"dvukovic/no-restricted-dependencies": [
|
|
23
25
|
"error",
|
|
24
26
|
{
|
|
25
|
-
|
|
27
|
+
dependencies: [
|
|
26
28
|
"@types/*",
|
|
27
29
|
"cspell",
|
|
28
30
|
"eslint",
|
|
31
|
+
"lambda",
|
|
29
32
|
"lodash",
|
|
30
|
-
"npm-package-json-lint",
|
|
31
33
|
"prettier",
|
|
32
34
|
"ramda",
|
|
33
35
|
"stylelint",
|
|
34
36
|
"typeorm",
|
|
35
37
|
],
|
|
38
|
+
peerDependencies: ["@types/*"],
|
|
36
39
|
},
|
|
37
40
|
],
|
|
41
|
+
"dvukovic/require-properties": ["error", { properties: ["volta.node"] }],
|
|
38
42
|
"dvukovic/valid-engines-node": ["error", { versions: ["24"] }],
|
|
39
43
|
"package-json/bin-name-casing": "error",
|
|
40
44
|
"package-json/no-empty-fields": "error",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
const DEPENDENCY_TYPES =
|
|
1
|
+
const DEPENDENCY_TYPES = [
|
|
2
2
|
"dependencies",
|
|
3
3
|
"devDependencies",
|
|
4
4
|
"optionalDependencies",
|
|
5
5
|
"peerDependencies",
|
|
6
|
-
]
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
const DEPENDENCY_TYPES_SET = new Set(DEPENDENCY_TYPES)
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Checks if a package name matches a restriction pattern. Supports exact matches and glob-like
|
|
@@ -28,11 +30,6 @@ function matchesPattern(packageName, pattern) {
|
|
|
28
30
|
export const noRestrictedDependencies = {
|
|
29
31
|
create(context) {
|
|
30
32
|
const options = context.options[0] || {}
|
|
31
|
-
const restricted = options.packages || []
|
|
32
|
-
|
|
33
|
-
if (restricted.length === 0) {
|
|
34
|
-
return {}
|
|
35
|
-
}
|
|
36
33
|
|
|
37
34
|
return {
|
|
38
35
|
JSONProperty(node) {
|
|
@@ -42,7 +39,7 @@ export const noRestrictedDependencies = {
|
|
|
42
39
|
|
|
43
40
|
const propertyName = node.key.value
|
|
44
41
|
|
|
45
|
-
if (!
|
|
42
|
+
if (!DEPENDENCY_TYPES_SET.has(propertyName)) {
|
|
46
43
|
return
|
|
47
44
|
}
|
|
48
45
|
|
|
@@ -50,6 +47,12 @@ export const noRestrictedDependencies = {
|
|
|
50
47
|
return
|
|
51
48
|
}
|
|
52
49
|
|
|
50
|
+
const patterns = options[propertyName] || []
|
|
51
|
+
|
|
52
|
+
if (patterns.length === 0) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
53
56
|
for (const dep of node.value.properties) {
|
|
54
57
|
if (dep.key.type !== "JSONLiteral" || typeof dep.key.value !== "string") {
|
|
55
58
|
continue
|
|
@@ -57,7 +60,7 @@ export const noRestrictedDependencies = {
|
|
|
57
60
|
|
|
58
61
|
const packageName = dep.key.value
|
|
59
62
|
|
|
60
|
-
for (const pattern of
|
|
63
|
+
for (const pattern of patterns) {
|
|
61
64
|
if (matchesPattern(packageName, pattern)) {
|
|
62
65
|
context.report({
|
|
63
66
|
data: {
|
|
@@ -86,9 +89,23 @@ export const noRestrictedDependencies = {
|
|
|
86
89
|
{
|
|
87
90
|
additionalProperties: false,
|
|
88
91
|
properties: {
|
|
89
|
-
|
|
90
|
-
description:
|
|
91
|
-
|
|
92
|
+
dependencies: {
|
|
93
|
+
description: "Restricted patterns in dependencies",
|
|
94
|
+
items: { type: "string" },
|
|
95
|
+
type: "array",
|
|
96
|
+
},
|
|
97
|
+
devDependencies: {
|
|
98
|
+
description: "Restricted patterns in devDependencies",
|
|
99
|
+
items: { type: "string" },
|
|
100
|
+
type: "array",
|
|
101
|
+
},
|
|
102
|
+
optionalDependencies: {
|
|
103
|
+
description: "Restricted patterns in optionalDependencies",
|
|
104
|
+
items: { type: "string" },
|
|
105
|
+
type: "array",
|
|
106
|
+
},
|
|
107
|
+
peerDependencies: {
|
|
108
|
+
description: "Restricted patterns in peerDependencies",
|
|
92
109
|
items: { type: "string" },
|
|
93
110
|
type: "array",
|
|
94
111
|
},
|
|
@@ -3,7 +3,7 @@ import jsoncParser from "jsonc-eslint-parser"
|
|
|
3
3
|
|
|
4
4
|
import { noRestrictedDependencies } from "./no-restricted-dependencies.js"
|
|
5
5
|
|
|
6
|
-
const createEslint = (
|
|
6
|
+
const createEslint = (options) => {
|
|
7
7
|
return new ESLint({
|
|
8
8
|
overrideConfig: [
|
|
9
9
|
{
|
|
@@ -19,7 +19,7 @@ const createEslint = (packages) => {
|
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
rules: {
|
|
22
|
-
"custom/no-restricted-dependencies": ["error",
|
|
22
|
+
"custom/no-restricted-dependencies": ["error", options],
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
],
|
|
@@ -27,9 +27,19 @@ const createEslint = (packages) => {
|
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const getErrors = async (eslint, packageJson) => {
|
|
31
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
results[0]?.messages.filter((message) => {
|
|
35
|
+
return message.ruleId === "custom/no-restricted-dependencies"
|
|
36
|
+
}) ?? []
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
describe("no-restricted-dependencies", () => {
|
|
31
41
|
test("detects restricted package in dependencies", async () => {
|
|
32
|
-
const eslint = createEslint(["lodash"])
|
|
42
|
+
const eslint = createEslint({ dependencies: ["lodash"] })
|
|
33
43
|
const packageJson = JSON.stringify(
|
|
34
44
|
{
|
|
35
45
|
dependencies: {
|
|
@@ -41,17 +51,14 @@ describe("no-restricted-dependencies", () => {
|
|
|
41
51
|
4,
|
|
42
52
|
)
|
|
43
53
|
|
|
44
|
-
const
|
|
45
|
-
const errors = results[0]?.messages.filter((message) => {
|
|
46
|
-
return message.ruleId === "custom/no-restricted-dependencies"
|
|
47
|
-
})
|
|
54
|
+
const errors = await getErrors(eslint, packageJson)
|
|
48
55
|
|
|
49
|
-
expect(errors
|
|
50
|
-
expect(errors
|
|
56
|
+
expect(errors.length).toBe(1)
|
|
57
|
+
expect(errors[0]?.message).toContain("lodash")
|
|
51
58
|
})
|
|
52
59
|
|
|
53
60
|
test("detects restricted package in devDependencies", async () => {
|
|
54
|
-
const eslint = createEslint(["jest"])
|
|
61
|
+
const eslint = createEslint({ devDependencies: ["jest"] })
|
|
55
62
|
const packageJson = JSON.stringify(
|
|
56
63
|
{
|
|
57
64
|
devDependencies: {
|
|
@@ -63,16 +70,13 @@ describe("no-restricted-dependencies", () => {
|
|
|
63
70
|
4,
|
|
64
71
|
)
|
|
65
72
|
|
|
66
|
-
const
|
|
67
|
-
const errors = results[0]?.messages.filter((message) => {
|
|
68
|
-
return message.ruleId === "custom/no-restricted-dependencies"
|
|
69
|
-
})
|
|
73
|
+
const errors = await getErrors(eslint, packageJson)
|
|
70
74
|
|
|
71
|
-
expect(errors
|
|
75
|
+
expect(errors.length).toBe(1)
|
|
72
76
|
})
|
|
73
77
|
|
|
74
78
|
test("detects pattern with wildcard", async () => {
|
|
75
|
-
const eslint = createEslint(["@types/*"])
|
|
79
|
+
const eslint = createEslint({ devDependencies: ["@types/*"] })
|
|
76
80
|
const packageJson = JSON.stringify(
|
|
77
81
|
{
|
|
78
82
|
devDependencies: {
|
|
@@ -85,16 +89,13 @@ describe("no-restricted-dependencies", () => {
|
|
|
85
89
|
4,
|
|
86
90
|
)
|
|
87
91
|
|
|
88
|
-
const
|
|
89
|
-
const errors = results[0]?.messages.filter((message) => {
|
|
90
|
-
return message.ruleId === "custom/no-restricted-dependencies"
|
|
91
|
-
})
|
|
92
|
+
const errors = await getErrors(eslint, packageJson)
|
|
92
93
|
|
|
93
|
-
expect(errors
|
|
94
|
+
expect(errors.length).toBe(2)
|
|
94
95
|
})
|
|
95
96
|
|
|
96
97
|
test("allows non-restricted packages", async () => {
|
|
97
|
-
const eslint = createEslint(["lodash"])
|
|
98
|
+
const eslint = createEslint({ dependencies: ["lodash"] })
|
|
98
99
|
const packageJson = JSON.stringify(
|
|
99
100
|
{
|
|
100
101
|
dependencies: {
|
|
@@ -106,16 +107,13 @@ describe("no-restricted-dependencies", () => {
|
|
|
106
107
|
4,
|
|
107
108
|
)
|
|
108
109
|
|
|
109
|
-
const
|
|
110
|
-
const errors = results[0]?.messages.filter((message) => {
|
|
111
|
-
return message.ruleId === "custom/no-restricted-dependencies"
|
|
112
|
-
})
|
|
110
|
+
const errors = await getErrors(eslint, packageJson)
|
|
113
111
|
|
|
114
|
-
expect(errors
|
|
112
|
+
expect(errors.length).toBe(0)
|
|
115
113
|
})
|
|
116
114
|
|
|
117
115
|
test("detects multiple restricted packages", async () => {
|
|
118
|
-
const eslint = createEslint(["lodash", "ramda", "underscore"])
|
|
116
|
+
const eslint = createEslint({ dependencies: ["lodash", "ramda", "underscore"] })
|
|
119
117
|
const packageJson = JSON.stringify(
|
|
120
118
|
{
|
|
121
119
|
dependencies: {
|
|
@@ -129,16 +127,13 @@ describe("no-restricted-dependencies", () => {
|
|
|
129
127
|
4,
|
|
130
128
|
)
|
|
131
129
|
|
|
132
|
-
const
|
|
133
|
-
const errors = results[0]?.messages.filter((message) => {
|
|
134
|
-
return message.ruleId === "custom/no-restricted-dependencies"
|
|
135
|
-
})
|
|
130
|
+
const errors = await getErrors(eslint, packageJson)
|
|
136
131
|
|
|
137
|
-
expect(errors
|
|
132
|
+
expect(errors.length).toBe(2)
|
|
138
133
|
})
|
|
139
134
|
|
|
140
|
-
test("handles empty
|
|
141
|
-
const eslint = createEslint(
|
|
135
|
+
test("handles empty options", async () => {
|
|
136
|
+
const eslint = createEslint({})
|
|
142
137
|
const packageJson = JSON.stringify(
|
|
143
138
|
{
|
|
144
139
|
dependencies: {
|
|
@@ -150,11 +145,93 @@ describe("no-restricted-dependencies", () => {
|
|
|
150
145
|
4,
|
|
151
146
|
)
|
|
152
147
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
const errors = await getErrors(eslint, packageJson)
|
|
149
|
+
|
|
150
|
+
expect(errors.length).toBe(0)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("restricts @types/* in dependencies but allows in devDependencies", async () => {
|
|
154
|
+
const eslint = createEslint({ dependencies: ["@types/*"] })
|
|
155
|
+
const packageJson = JSON.stringify(
|
|
156
|
+
{
|
|
157
|
+
dependencies: {
|
|
158
|
+
"@types/node": "^20.0.0",
|
|
159
|
+
},
|
|
160
|
+
devDependencies: {
|
|
161
|
+
"@types/react": "^18.0.0",
|
|
162
|
+
},
|
|
163
|
+
name: "test",
|
|
164
|
+
},
|
|
165
|
+
null,
|
|
166
|
+
4,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const errors = await getErrors(eslint, packageJson)
|
|
170
|
+
|
|
171
|
+
expect(errors.length).toBe(1)
|
|
172
|
+
expect(errors[0]?.message).toContain("@types/node")
|
|
173
|
+
expect(errors[0]?.message).toContain("dependencies")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("restricts different patterns in different dependency types", async () => {
|
|
177
|
+
const eslint = createEslint({
|
|
178
|
+
dependencies: ["@types/*", "lodash"],
|
|
179
|
+
devDependencies: ["jest"],
|
|
156
180
|
})
|
|
181
|
+
const packageJson = JSON.stringify(
|
|
182
|
+
{
|
|
183
|
+
dependencies: {
|
|
184
|
+
"@types/node": "^20.0.0",
|
|
185
|
+
lodash: "^4.0.0",
|
|
186
|
+
},
|
|
187
|
+
devDependencies: {
|
|
188
|
+
"@types/react": "^18.0.0",
|
|
189
|
+
jest: "^29.0.0",
|
|
190
|
+
},
|
|
191
|
+
name: "test",
|
|
192
|
+
},
|
|
193
|
+
null,
|
|
194
|
+
4,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const errors = await getErrors(eslint, packageJson)
|
|
198
|
+
|
|
199
|
+
expect(errors.length).toBe(3)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test("restricts in peerDependencies", async () => {
|
|
203
|
+
const eslint = createEslint({ peerDependencies: ["react"] })
|
|
204
|
+
const packageJson = JSON.stringify(
|
|
205
|
+
{
|
|
206
|
+
name: "test",
|
|
207
|
+
peerDependencies: {
|
|
208
|
+
react: "^18.0.0",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
null,
|
|
212
|
+
4,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
const errors = await getErrors(eslint, packageJson)
|
|
216
|
+
|
|
217
|
+
expect(errors.length).toBe(1)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test("restricts in optionalDependencies", async () => {
|
|
221
|
+
const eslint = createEslint({ optionalDependencies: ["fsevents"] })
|
|
222
|
+
const packageJson = JSON.stringify(
|
|
223
|
+
{
|
|
224
|
+
name: "test",
|
|
225
|
+
optionalDependencies: {
|
|
226
|
+
fsevents: "^2.0.0",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
null,
|
|
230
|
+
4,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const errors = await getErrors(eslint, packageJson)
|
|
157
234
|
|
|
158
|
-
expect(errors
|
|
235
|
+
expect(errors.length).toBe(1)
|
|
159
236
|
})
|
|
160
237
|
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
function getPropertyPath(node) {
|
|
2
|
+
const parts = []
|
|
3
|
+
let current = node
|
|
4
|
+
|
|
5
|
+
while (current?.type === "JSONProperty") {
|
|
6
|
+
if (current.key.type === "JSONLiteral" && typeof current.key.value === "string") {
|
|
7
|
+
parts.unshift(current.key.value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
current = current.parent?.parent
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return parts.join(".")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const requireProperties = {
|
|
17
|
+
create(context) {
|
|
18
|
+
const options = context.options[0] || {}
|
|
19
|
+
const properties = options.properties || []
|
|
20
|
+
|
|
21
|
+
if (properties.length === 0) {
|
|
22
|
+
return {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const foundPaths = new Set()
|
|
26
|
+
let programNode = null
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
JSONExpressionStatement(node) {
|
|
30
|
+
programNode = node
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
JSONProperty(node) {
|
|
34
|
+
const path = getPropertyPath(node)
|
|
35
|
+
|
|
36
|
+
if (path) {
|
|
37
|
+
foundPaths.add(path)
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"Program:exit"() {
|
|
42
|
+
for (const property of properties) {
|
|
43
|
+
if (!foundPaths.has(property)) {
|
|
44
|
+
context.report({
|
|
45
|
+
data: { property },
|
|
46
|
+
messageId: "missingProperty",
|
|
47
|
+
node: programNode,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
meta: {
|
|
55
|
+
docs: {
|
|
56
|
+
description: "Require specified properties in package.json",
|
|
57
|
+
},
|
|
58
|
+
messages: {
|
|
59
|
+
missingProperty: `Missing required property "{{property}}" in package.json.`,
|
|
60
|
+
},
|
|
61
|
+
schema: [
|
|
62
|
+
{
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
properties: {
|
|
65
|
+
properties: {
|
|
66
|
+
description: "List of required property paths (supports dot notation)",
|
|
67
|
+
items: { type: "string" },
|
|
68
|
+
type: "array",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
type: "object",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
type: "problem",
|
|
75
|
+
},
|
|
76
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ESLint } from "eslint"
|
|
2
|
+
import jsoncParser from "jsonc-eslint-parser"
|
|
3
|
+
|
|
4
|
+
import { requireProperties } from "./require-properties.js"
|
|
5
|
+
|
|
6
|
+
const createEslint = (properties) => {
|
|
7
|
+
return new ESLint({
|
|
8
|
+
overrideConfig: [
|
|
9
|
+
{
|
|
10
|
+
files: ["**/package.json"],
|
|
11
|
+
languageOptions: {
|
|
12
|
+
parser: jsoncParser,
|
|
13
|
+
},
|
|
14
|
+
plugins: {
|
|
15
|
+
custom: {
|
|
16
|
+
rules: {
|
|
17
|
+
"require-properties": requireProperties,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
"custom/require-properties": ["error", { properties }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
overrideConfigFile: true,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("require-properties", () => {
|
|
31
|
+
test("passes when all required properties exist", async () => {
|
|
32
|
+
const eslint = createEslint(["name", "version"])
|
|
33
|
+
const packageJson = JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
name: "test",
|
|
36
|
+
version: "1.0.0",
|
|
37
|
+
},
|
|
38
|
+
null,
|
|
39
|
+
4,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
43
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
44
|
+
return message.ruleId === "custom/require-properties"
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(errors?.length).toBe(0)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("fails when a required property is missing", async () => {
|
|
51
|
+
const eslint = createEslint(["name", "version"])
|
|
52
|
+
const packageJson = JSON.stringify(
|
|
53
|
+
{
|
|
54
|
+
name: "test",
|
|
55
|
+
},
|
|
56
|
+
null,
|
|
57
|
+
4,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
61
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
62
|
+
return message.ruleId === "custom/require-properties"
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(errors?.length).toBe(1)
|
|
66
|
+
expect(errors?.[0]?.message).toContain("version")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("fails with multiple missing properties", async () => {
|
|
70
|
+
const eslint = createEslint(["name", "version", "description"])
|
|
71
|
+
const packageJson = JSON.stringify(
|
|
72
|
+
{
|
|
73
|
+
name: "test",
|
|
74
|
+
},
|
|
75
|
+
null,
|
|
76
|
+
4,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
80
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
81
|
+
return message.ruleId === "custom/require-properties"
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(errors?.length).toBe(2)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("passes with empty properties config", async () => {
|
|
88
|
+
const eslint = createEslint([])
|
|
89
|
+
const packageJson = JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
name: "test",
|
|
92
|
+
},
|
|
93
|
+
null,
|
|
94
|
+
4,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
98
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
99
|
+
return message.ruleId === "custom/require-properties"
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(errors?.length).toBe(0)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("supports dot notation for nested properties", async () => {
|
|
106
|
+
const eslint = createEslint(["volta.node"])
|
|
107
|
+
const packageJson = JSON.stringify(
|
|
108
|
+
{
|
|
109
|
+
name: "test",
|
|
110
|
+
volta: {
|
|
111
|
+
node: "24.0.0",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
null,
|
|
115
|
+
4,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
119
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
120
|
+
return message.ruleId === "custom/require-properties"
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(errors?.length).toBe(0)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("fails when nested property is missing", async () => {
|
|
127
|
+
const eslint = createEslint(["volta.node"])
|
|
128
|
+
const packageJson = JSON.stringify(
|
|
129
|
+
{
|
|
130
|
+
name: "test",
|
|
131
|
+
volta: {},
|
|
132
|
+
},
|
|
133
|
+
null,
|
|
134
|
+
4,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
138
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
139
|
+
return message.ruleId === "custom/require-properties"
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(errors?.length).toBe(1)
|
|
143
|
+
expect(errors?.[0]?.message).toContain("volta.node")
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test("fails when parent of nested property is missing", async () => {
|
|
147
|
+
const eslint = createEslint(["volta.node"])
|
|
148
|
+
const packageJson = JSON.stringify(
|
|
149
|
+
{
|
|
150
|
+
name: "test",
|
|
151
|
+
},
|
|
152
|
+
null,
|
|
153
|
+
4,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
157
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
158
|
+
return message.ruleId === "custom/require-properties"
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect(errors?.length).toBe(1)
|
|
162
|
+
expect(errors?.[0]?.message).toContain("volta.node")
|
|
163
|
+
})
|
|
164
|
+
})
|