@dvukovic/style-guide 0.3.118 → 0.3.119
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 +3 -0
- 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.119",
|
|
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
|
},
|
|
@@ -35,6 +37,7 @@ export const packageJson = {
|
|
|
35
37
|
],
|
|
36
38
|
},
|
|
37
39
|
],
|
|
40
|
+
"dvukovic/require-properties": ["error", { properties: ["volta.node"] }],
|
|
38
41
|
"dvukovic/valid-engines-node": ["error", { versions: ["24"] }],
|
|
39
42
|
"package-json/bin-name-casing": "error",
|
|
40
43
|
"package-json/no-empty-fields": "error",
|
|
@@ -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
|
+
})
|