@dvukovic/style-guide 0.3.116 → 0.3.117
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 +7 -10
- package/src/eslint/configs/package-json.js +17 -0
- package/src/eslint/configs/package-json.test.js +50 -0
- package/src/eslint/index.js +1 -0
- package/src/eslint/plugins/package-json.js +97 -0
- package/src/eslint/rules/no-restricted-dependencies/no-restricted-dependencies.js +101 -0
- package/src/eslint/rules/no-restricted-dependencies/no-restricted-dependencies.test.js +160 -0
- package/src/eslint/rules/valid-engines-node/valid-engines-node.js +86 -0
- package/src/eslint/rules/valid-engines-node/valid-engines-node.test.js +154 -0
- package/src/package-json/configs/core.js +0 -8
- package/src/package-json/plugins/package-json.js +0 -81
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dvukovic/style-guide",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.117",
|
|
4
4
|
"description": "My own style guide",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
|
33
|
-
"@eslint/compat": "
|
|
33
|
+
"@eslint/compat": "2.0.0",
|
|
34
34
|
"@next/eslint-plugin-next": "16.1.0",
|
|
35
35
|
"@prettier/plugin-xml": "3.4.2",
|
|
36
36
|
"@rimac-technology/eslint-plugin": "1.5.0",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"eslint-plugin-jest": "29.4.0",
|
|
43
43
|
"eslint-plugin-mobx": "0.0.13",
|
|
44
44
|
"eslint-plugin-n": "17.23.1",
|
|
45
|
+
"eslint-plugin-package-json": "0.88.1",
|
|
45
46
|
"eslint-plugin-playwright": "2.4.0",
|
|
46
47
|
"eslint-plugin-promise": "7.2.1",
|
|
47
48
|
"eslint-plugin-react": "7.37.5",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"eslint-plugin-unicorn": "62.0.0",
|
|
57
58
|
"eslint-plugin-unused-imports": "4.3.0",
|
|
58
59
|
"globals": "16.5.0",
|
|
60
|
+
"jsonc-eslint-parser": "2.4.2",
|
|
59
61
|
"prettier-plugin-embed": "0.5.1",
|
|
60
62
|
"prettier-plugin-jsdoc": "1.8.0",
|
|
61
63
|
"prettier-plugin-packagejson": "2.5.20",
|
|
@@ -77,19 +79,17 @@
|
|
|
77
79
|
"cspell": "9.4.0",
|
|
78
80
|
"eslint": "9.39.2",
|
|
79
81
|
"jest": "30.2.0",
|
|
80
|
-
"npm-package-json-lint": "9.0.0",
|
|
81
82
|
"prettier": "3.7.4",
|
|
82
83
|
"react": "19.2.3",
|
|
83
84
|
"release-it": "19.1.0",
|
|
84
|
-
"storybook": "
|
|
85
|
+
"storybook": "10.1.10",
|
|
85
86
|
"stylelint": "16.26.1",
|
|
86
87
|
"typescript": "5.9.3",
|
|
87
|
-
"vitest": "
|
|
88
|
+
"vitest": "4.0.16"
|
|
88
89
|
},
|
|
89
90
|
"peerDependencies": {
|
|
90
91
|
"cspell": "9",
|
|
91
92
|
"eslint": "^9",
|
|
92
|
-
"npm-package-json-lint": "9",
|
|
93
93
|
"prettier": "3",
|
|
94
94
|
"stylelint": "16"
|
|
95
95
|
},
|
|
@@ -100,9 +100,6 @@
|
|
|
100
100
|
"eslint": {
|
|
101
101
|
"optional": true
|
|
102
102
|
},
|
|
103
|
-
"npm-package-json-lint": {
|
|
104
|
-
"optional": true
|
|
105
|
-
},
|
|
106
103
|
"prettier": {
|
|
107
104
|
"optional": true
|
|
108
105
|
},
|
|
@@ -112,7 +109,7 @@
|
|
|
112
109
|
},
|
|
113
110
|
"packageManager": "yarn@4.12.0",
|
|
114
111
|
"engines": {
|
|
115
|
-
"node": ">=
|
|
112
|
+
"node": ">=24.0.0"
|
|
116
113
|
},
|
|
117
114
|
"publishConfig": {
|
|
118
115
|
"access": "public",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { packageJson as packageJsonRules } from "../plugins/package-json.js"
|
|
2
|
+
|
|
3
|
+
export const packageJsonConfigs = [packageJsonRules]
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Package.json ESLint configuration
|
|
7
|
+
*
|
|
8
|
+
* @param {import("@eslint/config-helpers").ConfigWithExtends} [config]
|
|
9
|
+
* @returns {import("@eslint/config-helpers").ConfigWithExtends}
|
|
10
|
+
*/
|
|
11
|
+
export function packageJson(config) {
|
|
12
|
+
return {
|
|
13
|
+
extends: [...packageJsonConfigs, ...(config?.extends ?? [])],
|
|
14
|
+
files: ["**/package.json"],
|
|
15
|
+
...config,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ESLint } from "eslint"
|
|
2
|
+
|
|
3
|
+
import { packageJsonConfigs } from "./package-json.js"
|
|
4
|
+
|
|
5
|
+
const eslint = new ESLint({
|
|
6
|
+
overrideConfig: [...packageJsonConfigs, { files: ["**/package.json"] }],
|
|
7
|
+
overrideConfigFile: true,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe("package-json", () => {
|
|
11
|
+
test("loads without errors", async () => {
|
|
12
|
+
const validPackageJson = JSON.stringify(
|
|
13
|
+
{
|
|
14
|
+
author: "Test Author",
|
|
15
|
+
description: "A test package",
|
|
16
|
+
engines: {
|
|
17
|
+
node: ">=20.0.0",
|
|
18
|
+
},
|
|
19
|
+
license: "MIT",
|
|
20
|
+
name: "test-package",
|
|
21
|
+
repository: {
|
|
22
|
+
type: "git",
|
|
23
|
+
url: "https://github.com/test/test",
|
|
24
|
+
},
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
},
|
|
27
|
+
null,
|
|
28
|
+
4,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const results = await eslint.lintText(validPackageJson, { filePath: "package.json" })
|
|
32
|
+
|
|
33
|
+
expect(results).toBeDefined()
|
|
34
|
+
expect(results[0]?.fatalErrorCount).toBe(0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("detects missing required fields", async () => {
|
|
38
|
+
const invalidPackageJson = JSON.stringify(
|
|
39
|
+
{
|
|
40
|
+
name: "test-package",
|
|
41
|
+
},
|
|
42
|
+
null,
|
|
43
|
+
4,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const results = await eslint.lintText(invalidPackageJson, { filePath: "package.json" })
|
|
47
|
+
|
|
48
|
+
expect(results[0]?.errorCount).toBeGreaterThan(0)
|
|
49
|
+
})
|
|
50
|
+
})
|
package/src/eslint/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export * from "./configs/jest.js"
|
|
|
30
30
|
export * from "./configs/mobx.js"
|
|
31
31
|
export * from "./configs/next.js"
|
|
32
32
|
export * from "./configs/node.js"
|
|
33
|
+
export * from "./configs/package-json.js"
|
|
33
34
|
export * from "./configs/playwright.js"
|
|
34
35
|
export * from "./configs/react.js"
|
|
35
36
|
export * from "./configs/typescript.js"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import plugin from "eslint-plugin-package-json"
|
|
2
|
+
import jsoncParser from "jsonc-eslint-parser"
|
|
3
|
+
|
|
4
|
+
import { noRestrictedDependencies } from "../rules/no-restricted-dependencies/no-restricted-dependencies.js"
|
|
5
|
+
import { validEnginesNode } from "../rules/valid-engines-node/valid-engines-node.js"
|
|
6
|
+
|
|
7
|
+
/** @type {import("@eslint/config-helpers").Config} */
|
|
8
|
+
export const packageJson = {
|
|
9
|
+
languageOptions: {
|
|
10
|
+
parser: jsoncParser,
|
|
11
|
+
},
|
|
12
|
+
plugins: {
|
|
13
|
+
dvukovic: {
|
|
14
|
+
rules: {
|
|
15
|
+
"no-restricted-dependencies": noRestrictedDependencies,
|
|
16
|
+
"valid-engines-node": validEnginesNode,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
"package-json": plugin,
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
"dvukovic/no-restricted-dependencies": [
|
|
23
|
+
"error",
|
|
24
|
+
{
|
|
25
|
+
packages: [
|
|
26
|
+
"@types/*",
|
|
27
|
+
"cspell",
|
|
28
|
+
"eslint",
|
|
29
|
+
"lodash",
|
|
30
|
+
"npm-package-json-lint",
|
|
31
|
+
"prettier",
|
|
32
|
+
"ramda",
|
|
33
|
+
"stylelint",
|
|
34
|
+
"typeorm",
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
"dvukovic/valid-engines-node": ["error", { versions: ["24"] }],
|
|
39
|
+
"package-json/bin-name-casing": "error",
|
|
40
|
+
"package-json/no-empty-fields": "error",
|
|
41
|
+
"package-json/no-redundant-files": "error",
|
|
42
|
+
"package-json/no-redundant-publishConfig": "error",
|
|
43
|
+
"package-json/order-properties": "error",
|
|
44
|
+
"package-json/repository-shorthand": "error",
|
|
45
|
+
"package-json/require-author": "error",
|
|
46
|
+
"package-json/require-description": "error",
|
|
47
|
+
"package-json/require-engines": "error",
|
|
48
|
+
"package-json/require-license": "error",
|
|
49
|
+
"package-json/require-name": "error",
|
|
50
|
+
"package-json/require-repository": "error",
|
|
51
|
+
"package-json/require-version": "error",
|
|
52
|
+
"package-json/restrict-dependency-ranges": [
|
|
53
|
+
"error",
|
|
54
|
+
{
|
|
55
|
+
forDependencyTypes: ["dependencies", "devDependencies", "optionalDependencies"],
|
|
56
|
+
rangeType: "pin",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
"package-json/scripts-name-casing": "error",
|
|
60
|
+
"package-json/sort-collections": "error",
|
|
61
|
+
"package-json/specify-peers-locally": "error",
|
|
62
|
+
"package-json/unique-dependencies": "error",
|
|
63
|
+
"package-json/valid-author": "error",
|
|
64
|
+
"package-json/valid-bin": "error",
|
|
65
|
+
"package-json/valid-bundleDependencies": "error",
|
|
66
|
+
"package-json/valid-config": "error",
|
|
67
|
+
"package-json/valid-contributors": "error",
|
|
68
|
+
"package-json/valid-cpu": "error",
|
|
69
|
+
"package-json/valid-dependencies": "error",
|
|
70
|
+
"package-json/valid-description": "error",
|
|
71
|
+
"package-json/valid-devDependencies": "error",
|
|
72
|
+
"package-json/valid-directories": "error",
|
|
73
|
+
"package-json/valid-engines": "error",
|
|
74
|
+
"package-json/valid-exports": "error",
|
|
75
|
+
"package-json/valid-files": "error",
|
|
76
|
+
"package-json/valid-homepage": "error",
|
|
77
|
+
"package-json/valid-keywords": "error",
|
|
78
|
+
"package-json/valid-license": "error",
|
|
79
|
+
"package-json/valid-main": "error",
|
|
80
|
+
"package-json/valid-man": "error",
|
|
81
|
+
"package-json/valid-module": "error",
|
|
82
|
+
"package-json/valid-name": "error",
|
|
83
|
+
"package-json/valid-optionalDependencies": "error",
|
|
84
|
+
"package-json/valid-os": "error",
|
|
85
|
+
"package-json/valid-package-definition": "error",
|
|
86
|
+
"package-json/valid-peerDependencies": "error",
|
|
87
|
+
"package-json/valid-private": "error",
|
|
88
|
+
"package-json/valid-publishConfig": "error",
|
|
89
|
+
"package-json/valid-repository": "error",
|
|
90
|
+
"package-json/valid-repository-directory": "error",
|
|
91
|
+
"package-json/valid-scripts": "error",
|
|
92
|
+
"package-json/valid-sideEffects": "error",
|
|
93
|
+
"package-json/valid-type": "error",
|
|
94
|
+
"package-json/valid-version": "error",
|
|
95
|
+
"package-json/valid-workspaces": "error",
|
|
96
|
+
},
|
|
97
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const DEPENDENCY_TYPES = new Set([
|
|
2
|
+
"dependencies",
|
|
3
|
+
"devDependencies",
|
|
4
|
+
"optionalDependencies",
|
|
5
|
+
"peerDependencies",
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks if a package name matches a restriction pattern. Supports exact matches and glob-like
|
|
10
|
+
* patterns with * wildcard.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} packageName - The package name to check
|
|
13
|
+
* @param {string} pattern - The restriction pattern
|
|
14
|
+
* @returns {boolean} True if the package matches the pattern
|
|
15
|
+
*/
|
|
16
|
+
function matchesPattern(packageName, pattern) {
|
|
17
|
+
if (pattern.includes("*")) {
|
|
18
|
+
const regexPattern = pattern.replaceAll("*", ".*").replaceAll("/", String.raw`\/`)
|
|
19
|
+
// eslint-disable-next-line security-node/non-literal-reg-expr -- Pattern comes from trusted ESLint config
|
|
20
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
21
|
+
|
|
22
|
+
return regex.test(packageName)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return packageName === pattern
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const noRestrictedDependencies = {
|
|
29
|
+
create(context) {
|
|
30
|
+
const options = context.options[0] || {}
|
|
31
|
+
const restricted = options.packages || []
|
|
32
|
+
|
|
33
|
+
if (restricted.length === 0) {
|
|
34
|
+
return {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
JSONProperty(node) {
|
|
39
|
+
if (node.key.type !== "JSONLiteral" || typeof node.key.value !== "string") {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const propertyName = node.key.value
|
|
44
|
+
|
|
45
|
+
if (!DEPENDENCY_TYPES.has(propertyName)) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (node.value.type !== "JSONObjectExpression") {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const dep of node.value.properties) {
|
|
54
|
+
if (dep.key.type !== "JSONLiteral" || typeof dep.key.value !== "string") {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const packageName = dep.key.value
|
|
59
|
+
|
|
60
|
+
for (const pattern of restricted) {
|
|
61
|
+
if (matchesPattern(packageName, pattern)) {
|
|
62
|
+
context.report({
|
|
63
|
+
data: {
|
|
64
|
+
dependencyType: propertyName,
|
|
65
|
+
package: packageName,
|
|
66
|
+
pattern,
|
|
67
|
+
},
|
|
68
|
+
messageId: "restricted",
|
|
69
|
+
node: dep.key,
|
|
70
|
+
})
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
meta: {
|
|
79
|
+
docs: {
|
|
80
|
+
description: "Disallow specified packages from being used as dependencies.",
|
|
81
|
+
},
|
|
82
|
+
messages: {
|
|
83
|
+
restricted: `Package "{{package}}" is restricted (matched pattern: "{{pattern}}") in {{dependencyType}}.`,
|
|
84
|
+
},
|
|
85
|
+
schema: [
|
|
86
|
+
{
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
properties: {
|
|
89
|
+
packages: {
|
|
90
|
+
description:
|
|
91
|
+
"List of restricted package names or patterns (supports * wildcard)",
|
|
92
|
+
items: { type: "string" },
|
|
93
|
+
type: "array",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
type: "object",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
type: "problem",
|
|
100
|
+
},
|
|
101
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ESLint } from "eslint"
|
|
2
|
+
import jsoncParser from "jsonc-eslint-parser"
|
|
3
|
+
|
|
4
|
+
import { noRestrictedDependencies } from "./no-restricted-dependencies.js"
|
|
5
|
+
|
|
6
|
+
const createEslint = (packages) => {
|
|
7
|
+
return new ESLint({
|
|
8
|
+
overrideConfig: [
|
|
9
|
+
{
|
|
10
|
+
files: ["**/package.json"],
|
|
11
|
+
languageOptions: {
|
|
12
|
+
parser: jsoncParser,
|
|
13
|
+
},
|
|
14
|
+
plugins: {
|
|
15
|
+
custom: {
|
|
16
|
+
rules: {
|
|
17
|
+
"no-restricted-dependencies": noRestrictedDependencies,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
"custom/no-restricted-dependencies": ["error", { packages }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
overrideConfigFile: true,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("no-restricted-dependencies", () => {
|
|
31
|
+
test("detects restricted package in dependencies", async () => {
|
|
32
|
+
const eslint = createEslint(["lodash"])
|
|
33
|
+
const packageJson = JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
dependencies: {
|
|
36
|
+
lodash: "^4.0.0",
|
|
37
|
+
},
|
|
38
|
+
name: "test",
|
|
39
|
+
},
|
|
40
|
+
null,
|
|
41
|
+
4,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
45
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
46
|
+
return message.ruleId === "custom/no-restricted-dependencies"
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(errors?.length).toBe(1)
|
|
50
|
+
expect(errors?.[0]?.message).toContain("lodash")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("detects restricted package in devDependencies", async () => {
|
|
54
|
+
const eslint = createEslint(["jest"])
|
|
55
|
+
const packageJson = JSON.stringify(
|
|
56
|
+
{
|
|
57
|
+
devDependencies: {
|
|
58
|
+
jest: "^29.0.0",
|
|
59
|
+
},
|
|
60
|
+
name: "test",
|
|
61
|
+
},
|
|
62
|
+
null,
|
|
63
|
+
4,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
67
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
68
|
+
return message.ruleId === "custom/no-restricted-dependencies"
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(errors?.length).toBe(1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("detects pattern with wildcard", async () => {
|
|
75
|
+
const eslint = createEslint(["@types/*"])
|
|
76
|
+
const packageJson = JSON.stringify(
|
|
77
|
+
{
|
|
78
|
+
devDependencies: {
|
|
79
|
+
"@types/node": "^20.0.0",
|
|
80
|
+
"@types/react": "^18.0.0",
|
|
81
|
+
},
|
|
82
|
+
name: "test",
|
|
83
|
+
},
|
|
84
|
+
null,
|
|
85
|
+
4,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
89
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
90
|
+
return message.ruleId === "custom/no-restricted-dependencies"
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
expect(errors?.length).toBe(2)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("allows non-restricted packages", async () => {
|
|
97
|
+
const eslint = createEslint(["lodash"])
|
|
98
|
+
const packageJson = JSON.stringify(
|
|
99
|
+
{
|
|
100
|
+
dependencies: {
|
|
101
|
+
react: "^18.0.0",
|
|
102
|
+
},
|
|
103
|
+
name: "test",
|
|
104
|
+
},
|
|
105
|
+
null,
|
|
106
|
+
4,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
110
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
111
|
+
return message.ruleId === "custom/no-restricted-dependencies"
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(errors?.length).toBe(0)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("detects multiple restricted packages", async () => {
|
|
118
|
+
const eslint = createEslint(["lodash", "ramda", "underscore"])
|
|
119
|
+
const packageJson = JSON.stringify(
|
|
120
|
+
{
|
|
121
|
+
dependencies: {
|
|
122
|
+
lodash: "^4.0.0",
|
|
123
|
+
ramda: "^0.29.0",
|
|
124
|
+
react: "^18.0.0",
|
|
125
|
+
},
|
|
126
|
+
name: "test",
|
|
127
|
+
},
|
|
128
|
+
null,
|
|
129
|
+
4,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
133
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
134
|
+
return message.ruleId === "custom/no-restricted-dependencies"
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(errors?.length).toBe(2)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("handles empty packages list", async () => {
|
|
141
|
+
const eslint = createEslint([])
|
|
142
|
+
const packageJson = JSON.stringify(
|
|
143
|
+
{
|
|
144
|
+
dependencies: {
|
|
145
|
+
lodash: "^4.0.0",
|
|
146
|
+
},
|
|
147
|
+
name: "test",
|
|
148
|
+
},
|
|
149
|
+
null,
|
|
150
|
+
4,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
154
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
155
|
+
return message.ruleId === "custom/no-restricted-dependencies"
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(errors?.length).toBe(0)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const DEFAULT_ALLOWED_VERSIONS = ["24"]
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the major version number from a node engine string. Handles formats like ">=20",
|
|
5
|
+
* ">=20.0.0", "^20", "20.x", "20", etc.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} engineValue - The engines.node value
|
|
8
|
+
* @returns {string | null} The major version number or null if not parsable
|
|
9
|
+
*/
|
|
10
|
+
function extractMajorVersion(engineValue) {
|
|
11
|
+
const match = /(\d+)/.exec(engineValue)
|
|
12
|
+
|
|
13
|
+
return match?.[1] ?? null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const validEnginesNode = {
|
|
17
|
+
create(context) {
|
|
18
|
+
const options = context.options[0] || {}
|
|
19
|
+
const allowedVersions = options.versions || DEFAULT_ALLOWED_VERSIONS
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
JSONProperty(node) {
|
|
23
|
+
if (node.key.type !== "JSONLiteral" || node.key.value !== "engines") {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.value.type !== "JSONObjectExpression") {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const nodeEngine = node.value.properties.find((prop) => {
|
|
32
|
+
return prop.key.type === "JSONLiteral" && prop.key.value === "node"
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (!nodeEngine) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (nodeEngine.value.type !== "JSONLiteral") {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const engineValue = nodeEngine.value.value
|
|
44
|
+
|
|
45
|
+
if (typeof engineValue !== "string") {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const majorVersion = extractMajorVersion(engineValue)
|
|
50
|
+
|
|
51
|
+
if (!majorVersion || !allowedVersions.includes(majorVersion)) {
|
|
52
|
+
context.report({
|
|
53
|
+
data: {
|
|
54
|
+
allowed: allowedVersions.join(", "),
|
|
55
|
+
value: engineValue,
|
|
56
|
+
},
|
|
57
|
+
messageId: "invalidVersion",
|
|
58
|
+
node: nodeEngine.value,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
meta: {
|
|
65
|
+
docs: {
|
|
66
|
+
description: "Restrict engines.node to specific LTS versions.",
|
|
67
|
+
},
|
|
68
|
+
messages: {
|
|
69
|
+
invalidVersion: `Node engine "{{value}}" is not allowed. Use one of the LTS versions: {{allowed}}.`,
|
|
70
|
+
},
|
|
71
|
+
schema: [
|
|
72
|
+
{
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
properties: {
|
|
75
|
+
versions: {
|
|
76
|
+
description: "List of allowed major Node.js versions",
|
|
77
|
+
items: { type: "string" },
|
|
78
|
+
type: "array",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
type: "object",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
type: "problem",
|
|
85
|
+
},
|
|
86
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ESLint } from "eslint"
|
|
2
|
+
import jsoncParser from "jsonc-eslint-parser"
|
|
3
|
+
|
|
4
|
+
import { validEnginesNode } from "./valid-engines-node.js"
|
|
5
|
+
|
|
6
|
+
const createEslint = (versions) => {
|
|
7
|
+
return new ESLint({
|
|
8
|
+
overrideConfig: [
|
|
9
|
+
{
|
|
10
|
+
files: ["**/package.json"],
|
|
11
|
+
languageOptions: {
|
|
12
|
+
parser: jsoncParser,
|
|
13
|
+
},
|
|
14
|
+
plugins: {
|
|
15
|
+
custom: {
|
|
16
|
+
rules: {
|
|
17
|
+
"valid-engines-node": validEnginesNode,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
"custom/valid-engines-node": ["error", { versions }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
overrideConfigFile: true,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("valid-engines-node", () => {
|
|
31
|
+
test("allows valid LTS version", async () => {
|
|
32
|
+
const eslint = createEslint(["24"])
|
|
33
|
+
const packageJson = JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
engines: {
|
|
36
|
+
node: ">=24.0.0",
|
|
37
|
+
},
|
|
38
|
+
name: "test",
|
|
39
|
+
},
|
|
40
|
+
null,
|
|
41
|
+
4,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
45
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
46
|
+
return message.ruleId === "custom/valid-engines-node"
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(errors?.length).toBe(0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test("allows version with just major number", async () => {
|
|
53
|
+
const eslint = createEslint(["24"])
|
|
54
|
+
const packageJson = JSON.stringify(
|
|
55
|
+
{
|
|
56
|
+
engines: {
|
|
57
|
+
node: ">=24",
|
|
58
|
+
},
|
|
59
|
+
name: "test",
|
|
60
|
+
},
|
|
61
|
+
null,
|
|
62
|
+
4,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
66
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
67
|
+
return message.ruleId === "custom/valid-engines-node"
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(errors?.length).toBe(0)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test("rejects non-LTS version", async () => {
|
|
74
|
+
const eslint = createEslint(["24"])
|
|
75
|
+
const packageJson = JSON.stringify(
|
|
76
|
+
{
|
|
77
|
+
engines: {
|
|
78
|
+
node: ">=18.0.0",
|
|
79
|
+
},
|
|
80
|
+
name: "test",
|
|
81
|
+
},
|
|
82
|
+
null,
|
|
83
|
+
4,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
87
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
88
|
+
return message.ruleId === "custom/valid-engines-node"
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(errors?.length).toBe(1)
|
|
92
|
+
expect(errors?.[0]?.message).toContain("18.0.0")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("rejects odd version numbers", async () => {
|
|
96
|
+
const eslint = createEslint(["24"])
|
|
97
|
+
const packageJson = JSON.stringify(
|
|
98
|
+
{
|
|
99
|
+
engines: {
|
|
100
|
+
node: ">=21.0.0",
|
|
101
|
+
},
|
|
102
|
+
name: "test",
|
|
103
|
+
},
|
|
104
|
+
null,
|
|
105
|
+
4,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
109
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
110
|
+
return message.ruleId === "custom/valid-engines-node"
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(errors?.length).toBe(1)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("handles missing engines field", async () => {
|
|
117
|
+
const eslint = createEslint(["24"])
|
|
118
|
+
const packageJson = JSON.stringify(
|
|
119
|
+
{
|
|
120
|
+
name: "test",
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
4,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
127
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
128
|
+
return message.ruleId === "custom/valid-engines-node"
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(errors?.length).toBe(0)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("handles missing node in engines", async () => {
|
|
135
|
+
const eslint = createEslint(["24"])
|
|
136
|
+
const packageJson = JSON.stringify(
|
|
137
|
+
{
|
|
138
|
+
engines: {
|
|
139
|
+
npm: ">=9",
|
|
140
|
+
},
|
|
141
|
+
name: "test",
|
|
142
|
+
},
|
|
143
|
+
null,
|
|
144
|
+
4,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const results = await eslint.lintText(packageJson, { filePath: "package.json" })
|
|
148
|
+
const errors = results[0]?.messages.filter((message) => {
|
|
149
|
+
return message.ruleId === "custom/valid-engines-node"
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(errors?.length).toBe(0)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/** @type {import("npm-package-json-lint/dist/src/configuration").Config} */
|
|
2
|
-
const plugin = {
|
|
3
|
-
rules: {
|
|
4
|
-
"bin-type": "error",
|
|
5
|
-
"bundledDependencies-type": "error",
|
|
6
|
-
"config-type": "error",
|
|
7
|
-
"cpu-type": "error",
|
|
8
|
-
"dependencies-type": "error",
|
|
9
|
-
"description-format": [
|
|
10
|
-
"error",
|
|
11
|
-
{
|
|
12
|
-
"name-format": "error",
|
|
13
|
-
"no-duplicate-properties": "error",
|
|
14
|
-
requireCapitalFirstLetter: true,
|
|
15
|
-
requireEndingPeriod: false,
|
|
16
|
-
|
|
17
|
-
"version-format": "error",
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
"description-type": "error",
|
|
21
|
-
"devDependencies-type": "error",
|
|
22
|
-
"directories-type": "error",
|
|
23
|
-
"engines-type": "error",
|
|
24
|
-
"files-type": "error",
|
|
25
|
-
"homepage-type": "error",
|
|
26
|
-
"keywords-type": "error",
|
|
27
|
-
"license-type": "error",
|
|
28
|
-
"main-type": "error",
|
|
29
|
-
"man-type": "error",
|
|
30
|
-
"name-type": "error",
|
|
31
|
-
"no-archive-dependencies": "error",
|
|
32
|
-
"no-archive-devDependencies": "error",
|
|
33
|
-
"no-caret-version-dependencies": "error",
|
|
34
|
-
"no-caret-version-devDependencies": "error",
|
|
35
|
-
"no-file-dependencies": "error",
|
|
36
|
-
"no-file-devDependencies": "error",
|
|
37
|
-
"no-git-dependencies": "error",
|
|
38
|
-
"no-git-devDependencies": "error",
|
|
39
|
-
"no-repeated-dependencies": "error",
|
|
40
|
-
"no-restricted-dependencies": [
|
|
41
|
-
"error",
|
|
42
|
-
[
|
|
43
|
-
"typeorm",
|
|
44
|
-
"ramda",
|
|
45
|
-
"lodash",
|
|
46
|
-
"@types/*",
|
|
47
|
-
"eslint",
|
|
48
|
-
"prettier",
|
|
49
|
-
"cspell",
|
|
50
|
-
"npm-package-json-lint",
|
|
51
|
-
"stylelint",
|
|
52
|
-
],
|
|
53
|
-
],
|
|
54
|
-
"no-tilde-version-dependencies": "error",
|
|
55
|
-
"no-tilde-version-devDependencies": "error",
|
|
56
|
-
"optionalDependencies-type": "error",
|
|
57
|
-
"os-type": "error",
|
|
58
|
-
"peerDependencies-type": "error",
|
|
59
|
-
"prefer-absolute-version-devDependencies": "error",
|
|
60
|
-
"prefer-alphabetical-dependencies": "error",
|
|
61
|
-
"preferGlobal-type": "error",
|
|
62
|
-
"private-type": "error",
|
|
63
|
-
"repository-type": "error",
|
|
64
|
-
"require-author": "error",
|
|
65
|
-
"require-description": "error",
|
|
66
|
-
"require-engines": "error",
|
|
67
|
-
"require-license": "error",
|
|
68
|
-
"require-name": "error",
|
|
69
|
-
"require-repository": "error",
|
|
70
|
-
"require-version": "error",
|
|
71
|
-
"scripts-type": "error",
|
|
72
|
-
"type-type": "error",
|
|
73
|
-
"valid-values-engines": [
|
|
74
|
-
"error",
|
|
75
|
-
[{ node: ">=18.0.0" }, { node: ">=20.0.0" }, { node: ">=22.0.0" }],
|
|
76
|
-
],
|
|
77
|
-
"version-type": "error",
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export default plugin
|