eslint-plugin-package-json 1.5.0 → 1.6.1
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/README.md +107 -106
- package/{lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/definitions/jsonSchema.d.mts → dist/createRule-29pC9RoP.d.mts} +49 -4
- package/dist/experimental/index.d.mts +68 -0
- package/{lib/experimental/plugin.mjs → dist/experimental/index.mjs} +10 -5
- package/dist/index.d.mts +99 -0
- package/{lib → dist}/index.mjs +1 -1
- package/dist/plugin-C-Z8sf5g.mjs +1737 -0
- package/package.json +33 -27
- package/lib/createRule.d.mts +0 -59
- package/lib/createRule.mjs +0 -24
- package/lib/experimental/index.d.mts +0 -34
- package/lib/experimental/index.mjs +0 -7
- package/lib/experimental/plugin.d.mts +0 -40
- package/lib/index.d.mts +0 -49
- package/lib/node_modules/.pnpm/@types_estree@1.0.8/node_modules/@types/estree/index.d.mts +0 -438
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/definitions/deserializationPattern.d.mts +0 -7
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/definitions/fromSchemaOptions.d.mts +0 -20
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/index.d.mts +0 -11
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-options.d.mts +0 -16
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/allOf.d.mts +0 -13
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/anyOf.d.mts +0 -14
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/array.d.mts +0 -58
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/const.d.mts +0 -14
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/deserialize.d.mts +0 -13
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/enum.d.mts +0 -14
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/ifThenElse.d.mts +0 -20
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/index.d.mts +0 -34
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/multipleTypes.d.mts +0 -14
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/not.d.mts +0 -20
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/nullable.d.mts +0 -14
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/object.d.mts +0 -34
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/oneOf.d.mts +0 -14
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/references/external.d.mts +0 -22
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/references/index.d.mts +0 -13
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/references/internal.d.mts +0 -8
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/references/utils.d.mts +0 -12
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/singleType.d.mts +0 -24
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/parse-schema/utils.d.mts +0 -20
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/and.d.mts +0 -4
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/extends.d.mts +0 -4
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/get.d.mts +0 -4
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/join.d.mts +0 -4
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/not.d.mts +0 -4
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/pop.d.mts +0 -4
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/split.d.mts +0 -7
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/tail.d.mts +0 -4
- package/lib/node_modules/.pnpm/json-schema-to-ts@3.1.1/node_modules/json-schema-to-ts/lib/types/type-utils/writable.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/any.d.mts +0 -20
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/array.d.mts +0 -28
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/const.d.mts +0 -25
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/enum.d.mts +0 -25
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/any.d.mts +0 -16
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/array.d.mts +0 -20
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/const.d.mts +0 -28
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/enum.d.mts +0 -23
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/index.d.mts +0 -24
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/object.d.mts +0 -44
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/primitive.d.mts +0 -16
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/tuple.d.mts +0 -43
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/union.d.mts +0 -13
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/exclusion/utils.d.mts +0 -35
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/any.d.mts +0 -23
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/array.d.mts +0 -24
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/const.d.mts +0 -34
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/enum.d.mts +0 -26
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/index.d.mts +0 -24
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/object.d.mts +0 -25
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/primitive.d.mts +0 -23
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/tuple.d.mts +0 -26
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/union.d.mts +0 -17
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/intersection/utils.d.mts +0 -10
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/never.d.mts +0 -9
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/object.d.mts +0 -49
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/primitive.d.mts +0 -26
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/resolve.d.mts +0 -22
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/tuple.d.mts +0 -36
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/type.d.mts +0 -18
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/union.d.mts +0 -23
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/meta-types/utils.d.mts +0 -7
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/and.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/extends.d.mts +0 -7
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/if.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/intersectUnion.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/isNever.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/merge.d.mts +0 -6
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/not.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/or.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/prettify.d.mts +0 -6
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/tail.d.mts +0 -4
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/unionLast.d.mts +0 -6
- package/lib/node_modules/.pnpm/ts-algebra@2.0.0/node_modules/ts-algebra/lib/utils/unionPop.d.mts +0 -6
- package/lib/plugin.d.mts +0 -56
- package/lib/plugin.mjs +0 -93
- package/lib/rules/bin-name-casing.d.mts +0 -6
- package/lib/rules/bin-name-casing.mjs +0 -41
- package/lib/rules/exports-subpaths-style.d.mts +0 -18
- package/lib/rules/exports-subpaths-style.mjs +0 -71
- package/lib/rules/no-empty-fields.d.mts +0 -20
- package/lib/rules/no-empty-fields.mjs +0 -90
- package/lib/rules/no-local-dependencies.d.mts +0 -17
- package/lib/rules/no-local-dependencies.mjs +0 -47
- package/lib/rules/no-redundant-files.d.mts +0 -6
- package/lib/rules/no-redundant-files.mjs +0 -100
- package/lib/rules/no-redundant-publishConfig.d.mts +0 -6
- package/lib/rules/no-redundant-publishConfig.mjs +0 -50
- package/lib/rules/order-properties.d.mts +0 -25
- package/lib/rules/order-properties.mjs +0 -64
- package/lib/rules/repository-shorthand.d.mts +0 -18
- package/lib/rules/repository-shorthand.mjs +0 -111
- package/lib/rules/require-attribution.d.mts +0 -22
- package/lib/rules/require-attribution.mjs +0 -91
- package/lib/rules/require-properties.d.mts +0 -22
- package/lib/rules/require-properties.mjs +0 -74
- package/lib/rules/restrict-dependency-ranges.d.mts +0 -92
- package/lib/rules/restrict-dependency-ranges.mjs +0 -147
- package/lib/rules/restrict-private-properties.d.mts +0 -20
- package/lib/rules/restrict-private-properties.mjs +0 -53
- package/lib/rules/restrict-top-level-properties.d.mts +0 -39
- package/lib/rules/restrict-top-level-properties.mjs +0 -73
- package/lib/rules/scripts-name-casing.d.mts +0 -6
- package/lib/rules/scripts-name-casing.mjs +0 -44
- package/lib/rules/sort-collections.d.mts +0 -35
- package/lib/rules/sort-collections.mjs +0 -111
- package/lib/rules/specify-peers-locally.d.mts +0 -6
- package/lib/rules/specify-peers-locally.mjs +0 -63
- package/lib/rules/unique-dependencies.d.mts +0 -6
- package/lib/rules/unique-dependencies.mjs +0 -87
- package/lib/rules/valid-author.d.mts +0 -6
- package/lib/rules/valid-author.mjs +0 -59
- package/lib/rules/valid-peerDependenciesMeta-relationship.d.mts +0 -6
- package/lib/rules/valid-peerDependenciesMeta-relationship.mjs +0 -50
- package/lib/rules/valid-properties.d.mts +0 -9
- package/lib/rules/valid-properties.mjs +0 -57
- package/lib/rules/valid-repository-directory.d.mts +0 -6
- package/lib/rules/valid-repository-directory.mjs +0 -73
- package/lib/types/estree.d.mts +0 -8
- package/lib/utils/createSimpleRequirePropertyRule.d.mts +0 -48
- package/lib/utils/createSimpleRequirePropertyRule.mjs +0 -62
- package/lib/utils/createSimpleValidPropertyRule.d.mts +0 -17
- package/lib/utils/createSimpleValidPropertyRule.mjs +0 -57
- package/lib/utils/findPropertyWithKeyValue.d.mts +0 -10
- package/lib/utils/findPropertyWithKeyValue.mjs +0 -6
- package/lib/utils/formatErrors.d.mts +0 -9
- package/lib/utils/formatErrors.mjs +0 -12
- package/lib/utils/git/getGitAuthor.d.mts +0 -8
- package/lib/utils/git/getGitAuthor.mjs +0 -29
- package/lib/utils/git/index.d.mts +0 -2
- package/lib/utils/git/index.mjs +0 -2
- package/lib/utils/predicates/index.d.mts +0 -4
- package/lib/utils/predicates/index.mjs +0 -4
- package/lib/utils/predicates/isJSONStringLiteral.d.mts +0 -6
- package/lib/utils/predicates/isJSONStringLiteral.mjs +0 -6
- package/lib/utils/predicates/isNotNullish.d.mts +0 -4
- package/lib/utils/predicates/isNotNullish.mjs +0 -6
- package/lib/utils/predicates/isPackageJson.d.mts +0 -4
- package/lib/utils/predicates/isPackageJson.mjs +0 -4
|
@@ -0,0 +1,1737 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as parserJsonc from "jsonc-eslint-parser";
|
|
3
|
+
import { kebabCase } from "change-case";
|
|
4
|
+
import { fixRemoveArrayElement, fixRemoveObjectProperty } from "eslint-fix-utils";
|
|
5
|
+
import detectIndent from "detect-indent";
|
|
6
|
+
import { detectNewlineGraceful } from "detect-newline";
|
|
7
|
+
import sortObjectKeys from "sort-object-keys";
|
|
8
|
+
import sortPackageJson, { sortOrder } from "sort-package-json";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import semver from "semver";
|
|
11
|
+
import { validateAuthor, validateBin, validateBrowser, validateBugs, validateBundleDependencies, validateConfig, validateContributors, validateCpu, validateDependencies, validateDescription, validateDevEngines, validateDirectories, validateEngines, validateExports, validateFiles, validateFunding, validateGypfile, validateHomepage, validateKeywords, validateLibc, validateLicense, validateMain, validateMan, validateName, validateOs, validatePackageManager, validatePeerDependenciesMeta, validatePrivate, validatePublishConfig, validateRepository, validateScripts, validateSideEffects, validateType, validateVersion, validateWorkspaces } from "package-json-validator";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { sep } from "node:path/posix";
|
|
14
|
+
import { findRootSync } from "@altano/repository-tools";
|
|
15
|
+
//#region src/utils/predicates/isJSONStringLiteral.ts
|
|
16
|
+
function isJSONStringLiteral(node) {
|
|
17
|
+
return node.type === "JSONLiteral" && typeof node.value === "string";
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/utils/predicates/isNotNullish.ts
|
|
21
|
+
function isNotNullish(value) {
|
|
22
|
+
return value !== null && value !== void 0;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/utils/predicates/isPackageJson.ts
|
|
26
|
+
const isPackageJson = (filePath) => /(?:^|[/\\])package.json$/.test(filePath);
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/createRule.ts
|
|
29
|
+
/**
|
|
30
|
+
* Rule options type is inferred from the JSON schema by [json-schema-to-ts](https://www.npmjs.com/package/json-schema-to-ts).
|
|
31
|
+
* If you're not satisfied with the inferred type, you may specify it manually in the first type parameter.
|
|
32
|
+
*/
|
|
33
|
+
function createRule(rule) {
|
|
34
|
+
const ruleGroup = rule.meta.docs?.ruleGroup;
|
|
35
|
+
return {
|
|
36
|
+
create(context) {
|
|
37
|
+
if (!isPackageJson(context.filename)) return {};
|
|
38
|
+
return rule.create(context);
|
|
39
|
+
},
|
|
40
|
+
meta: {
|
|
41
|
+
...rule.meta,
|
|
42
|
+
docs: {
|
|
43
|
+
...rule.meta.docs,
|
|
44
|
+
url: `https://eslint-plugin-package-json.dev/rules/${ruleGroup ? `${ruleGroup}/` : ""}${rule.name}`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/rules/bin-name-casing.ts
|
|
51
|
+
const rule$18 = createRule({
|
|
52
|
+
create(context) {
|
|
53
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=bin]"(node) {
|
|
54
|
+
if (node.value.type === "JSONObjectExpression") for (const property of node.value.properties) {
|
|
55
|
+
const key = property.key;
|
|
56
|
+
const kebabCaseKey = kebabCase(key.value);
|
|
57
|
+
if (kebabCaseKey !== key.value) context.report({
|
|
58
|
+
data: { property: key.value },
|
|
59
|
+
messageId: "invalidCase",
|
|
60
|
+
node: key,
|
|
61
|
+
suggest: [{
|
|
62
|
+
data: { property: key.value },
|
|
63
|
+
fix: (fixer) => {
|
|
64
|
+
return fixer.replaceText(key, JSON.stringify(kebabCaseKey));
|
|
65
|
+
},
|
|
66
|
+
messageId: "convertToKebabCase"
|
|
67
|
+
}]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} };
|
|
71
|
+
},
|
|
72
|
+
meta: {
|
|
73
|
+
docs: {
|
|
74
|
+
category: "Stylistic",
|
|
75
|
+
description: "Enforce that names for bin properties are in kebab case."
|
|
76
|
+
},
|
|
77
|
+
hasSuggestions: true,
|
|
78
|
+
messages: {
|
|
79
|
+
convertToKebabCase: "Convert {{ property }} to kebab case.",
|
|
80
|
+
invalidCase: "Command name {{ property }} should be in kebab case."
|
|
81
|
+
},
|
|
82
|
+
schema: [],
|
|
83
|
+
type: "suggestion"
|
|
84
|
+
},
|
|
85
|
+
name: "bin-name-casing"
|
|
86
|
+
});
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/rules/exports-subpaths-style.ts
|
|
89
|
+
function isImplicitFormat(node) {
|
|
90
|
+
if (node.type === "JSONLiteral") return true;
|
|
91
|
+
return node.properties.every((property) => !isJSONStringLiteral(property.key) || !property.key.value.startsWith("."));
|
|
92
|
+
}
|
|
93
|
+
const rule$17 = createRule({
|
|
94
|
+
create(context) {
|
|
95
|
+
const [{ prefer = "explicit" } = {}] = context.options;
|
|
96
|
+
function validateForExplicit(node) {
|
|
97
|
+
const { value } = node;
|
|
98
|
+
if (value.type !== "JSONLiteral" && value.type !== "JSONObjectExpression" || !isImplicitFormat(value)) return;
|
|
99
|
+
context.report({
|
|
100
|
+
fix(fixer) {
|
|
101
|
+
const valueText = context.sourceCode.getText(value);
|
|
102
|
+
const fixedValue = JSON.stringify({ ".": JSON.parse(valueText) }, null, 2);
|
|
103
|
+
return fixer.replaceText(value, fixedValue);
|
|
104
|
+
},
|
|
105
|
+
messageId: "preferExplicit",
|
|
106
|
+
node: value
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function validateForImplicit(node) {
|
|
110
|
+
const { value } = node;
|
|
111
|
+
if (value.type !== "JSONObjectExpression") return;
|
|
112
|
+
if (value.properties.length !== 1 || !isJSONStringLiteral(value.properties[0].key) || value.properties[0].key.value !== ".") return;
|
|
113
|
+
const dotProperty = value.properties[0];
|
|
114
|
+
context.report({
|
|
115
|
+
fix(fixer) {
|
|
116
|
+
const valueText = context.sourceCode.getText(dotProperty.value);
|
|
117
|
+
const fixedValue = JSON.stringify(JSON.parse(valueText), null, 2);
|
|
118
|
+
return fixer.replaceText(value, fixedValue);
|
|
119
|
+
},
|
|
120
|
+
messageId: "preferImplicit",
|
|
121
|
+
node: value
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return { JSONProperty(node) {
|
|
125
|
+
if (node.key.type !== "JSONLiteral" || node.key.value !== "exports" || node.parent.parent.parent.type !== "Program") return;
|
|
126
|
+
if (prefer === "explicit") validateForExplicit(node);
|
|
127
|
+
else validateForImplicit(node);
|
|
128
|
+
} };
|
|
129
|
+
},
|
|
130
|
+
meta: {
|
|
131
|
+
defaultOptions: [{ prefer: "explicit" }],
|
|
132
|
+
docs: {
|
|
133
|
+
category: "Stylistic",
|
|
134
|
+
description: "Enforce consistent format for the exports field (implicit or explicit subpaths).",
|
|
135
|
+
recommended: false
|
|
136
|
+
},
|
|
137
|
+
fixable: "code",
|
|
138
|
+
messages: {
|
|
139
|
+
preferExplicit: "Prefer explicit subpaths format with \".\" key for single root export.",
|
|
140
|
+
preferImplicit: "Prefer implicit format without \".\" key for single root export."
|
|
141
|
+
},
|
|
142
|
+
schema: [{
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
properties: { prefer: {
|
|
145
|
+
description: "Specifies which exports format to enforce.",
|
|
146
|
+
enum: ["implicit", "explicit"],
|
|
147
|
+
type: "string"
|
|
148
|
+
} },
|
|
149
|
+
type: "object"
|
|
150
|
+
}],
|
|
151
|
+
type: "suggestion"
|
|
152
|
+
},
|
|
153
|
+
name: "exports-subpaths-style"
|
|
154
|
+
});
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/rules/no-empty-fields.ts
|
|
157
|
+
const getDataAndMessageId = (node) => {
|
|
158
|
+
switch (node.type) {
|
|
159
|
+
case "JSONArrayExpression": return {
|
|
160
|
+
data: { expressionType: "array" },
|
|
161
|
+
messageId: "emptyExpression"
|
|
162
|
+
};
|
|
163
|
+
case "JSONObjectExpression": return {
|
|
164
|
+
data: { expressionType: "object" },
|
|
165
|
+
messageId: "emptyExpression"
|
|
166
|
+
};
|
|
167
|
+
case "JSONProperty": return {
|
|
168
|
+
data: { field: node.key.value },
|
|
169
|
+
messageId: "emptyFields"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const report = (context, node) => {
|
|
174
|
+
const { data, messageId } = getDataAndMessageId(node);
|
|
175
|
+
context.report({
|
|
176
|
+
data,
|
|
177
|
+
messageId,
|
|
178
|
+
node,
|
|
179
|
+
suggest: [{
|
|
180
|
+
fix: node.type === "JSONProperty" ? fixRemoveObjectProperty(context, node) : fixRemoveArrayElement(context, node, node.parent),
|
|
181
|
+
messageId: "remove"
|
|
182
|
+
}]
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
const getNode = (node) => {
|
|
186
|
+
return node.parent.type === "JSONProperty" ? node.parent : node;
|
|
187
|
+
};
|
|
188
|
+
const getTopLevelProperty = (node) => {
|
|
189
|
+
let n = node;
|
|
190
|
+
while (n.parent.parent?.parent?.type !== void 0 && n.parent.parent.parent.type !== "Program") n = n.parent;
|
|
191
|
+
return n.type === "JSONProperty" ? n.key : void 0;
|
|
192
|
+
};
|
|
193
|
+
const defaultIgnoreProperties = ["files"];
|
|
194
|
+
const rule$16 = createRule({
|
|
195
|
+
create(context) {
|
|
196
|
+
const ignoreProperties = new Set(context.options[0]?.ignoreProperties ?? defaultIgnoreProperties);
|
|
197
|
+
return {
|
|
198
|
+
JSONArrayExpression(node) {
|
|
199
|
+
const topLevelProperty = getTopLevelProperty(node);
|
|
200
|
+
if (!topLevelProperty) return;
|
|
201
|
+
if (!node.elements.length) {
|
|
202
|
+
const topLevelPropertyName = topLevelProperty.value;
|
|
203
|
+
if (!ignoreProperties.has(topLevelPropertyName)) report(context, getNode(node));
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
JSONObjectExpression(node) {
|
|
207
|
+
const topLevelProperty = getTopLevelProperty(node);
|
|
208
|
+
if (!topLevelProperty) return;
|
|
209
|
+
if (!node.properties.length) {
|
|
210
|
+
const topLevelPropertyName = topLevelProperty.value;
|
|
211
|
+
if (!ignoreProperties.has(topLevelPropertyName)) report(context, getNode(node));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
meta: {
|
|
217
|
+
defaultOptions: [{ ignoreProperties: defaultIgnoreProperties }],
|
|
218
|
+
docs: {
|
|
219
|
+
category: "Best Practices",
|
|
220
|
+
description: "Reports on unnecessary empty arrays and objects.",
|
|
221
|
+
recommended: true
|
|
222
|
+
},
|
|
223
|
+
hasSuggestions: true,
|
|
224
|
+
messages: {
|
|
225
|
+
emptyExpression: "This {{ expressionType }} does nothing and can be removed.",
|
|
226
|
+
emptyFields: "The field '{{ field }}' does nothing and can be removed.",
|
|
227
|
+
remove: "Remove this empty field."
|
|
228
|
+
},
|
|
229
|
+
schema: [{
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
properties: { ignoreProperties: {
|
|
232
|
+
description: "Array of top-level properties to ignore.",
|
|
233
|
+
items: { type: "string" },
|
|
234
|
+
type: "array"
|
|
235
|
+
} },
|
|
236
|
+
type: "object"
|
|
237
|
+
}],
|
|
238
|
+
type: "suggestion"
|
|
239
|
+
},
|
|
240
|
+
name: "no-empty-fields"
|
|
241
|
+
});
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/rules/no-local-dependencies.ts
|
|
244
|
+
const isLocalDependency = (value) => value.startsWith("file:") || value.startsWith("link:") || value.startsWith("./") || value.startsWith("../") || value.startsWith(".\\") || value.startsWith("..\\");
|
|
245
|
+
const rule$15 = createRule({
|
|
246
|
+
create(context) {
|
|
247
|
+
const ignorePrivate = context.options[0]?.ignorePrivate ?? true;
|
|
248
|
+
let isPrivate = false;
|
|
249
|
+
let dependencyNodes = [];
|
|
250
|
+
return {
|
|
251
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.type=JSONLiteral][value.type=JSONLiteral][key.value=private]"(node) {
|
|
252
|
+
if (node.value.value === true) isPrivate = true;
|
|
253
|
+
},
|
|
254
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.type=JSONLiteral][value.type=JSONObjectExpression][key.value=dependencies]"(node) {
|
|
255
|
+
dependencyNodes = node.value.properties;
|
|
256
|
+
},
|
|
257
|
+
"Program:exit"() {
|
|
258
|
+
if (ignorePrivate && isPrivate) return;
|
|
259
|
+
for (const dependencyPropertyNode of dependencyNodes) {
|
|
260
|
+
const dependencyValue = dependencyPropertyNode.value;
|
|
261
|
+
if (isJSONStringLiteral(dependencyValue) && isLocalDependency(dependencyValue.value)) context.report({
|
|
262
|
+
data: { name: dependencyValue.value },
|
|
263
|
+
messageId: "localDependencyFound",
|
|
264
|
+
node: dependencyValue
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
meta: {
|
|
271
|
+
defaultOptions: [{ ignorePrivate: true }],
|
|
272
|
+
docs: { description: "Requires that dependencies do not use local file paths, which will likely result in errors when installing from a registry." },
|
|
273
|
+
messages: { localDependencyFound: "Local dependency \"{{ name }}\" is not allowed." },
|
|
274
|
+
schema: [{
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
properties: { ignorePrivate: {
|
|
277
|
+
description: "Determines if this rule should be enforced when the package's `private` property is `true`.",
|
|
278
|
+
type: "boolean"
|
|
279
|
+
} },
|
|
280
|
+
type: "object"
|
|
281
|
+
}],
|
|
282
|
+
type: "problem"
|
|
283
|
+
},
|
|
284
|
+
name: "no-local-dependencies"
|
|
285
|
+
});
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/rules/no-redundant-files.ts
|
|
288
|
+
const defaultFiles = [
|
|
289
|
+
/^(\.\/)?LICEN(C|S)E(\.|$)/i,
|
|
290
|
+
/^(\.\/)?README(\.|$)/i,
|
|
291
|
+
/^(\.\/)?package\.json$/i
|
|
292
|
+
];
|
|
293
|
+
const wildcardsRegex = /[*?[\]{}]/;
|
|
294
|
+
const cachedRegex = /* @__PURE__ */ new Map();
|
|
295
|
+
const getCachedLocalFileRegex = (filename) => {
|
|
296
|
+
if (wildcardsRegex.test(filename)) return null;
|
|
297
|
+
const baseFilename = filename.replace("./", "");
|
|
298
|
+
let regex = cachedRegex.get(baseFilename);
|
|
299
|
+
if (regex) return regex;
|
|
300
|
+
else {
|
|
301
|
+
regex = new RegExp(`^(./)?${baseFilename}$`, "i");
|
|
302
|
+
cachedRegex.set(baseFilename, regex);
|
|
303
|
+
return regex;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const rule$14 = createRule({
|
|
307
|
+
create(context) {
|
|
308
|
+
const entryCache = {
|
|
309
|
+
bin: [],
|
|
310
|
+
files: []
|
|
311
|
+
};
|
|
312
|
+
const report = (elements, index, messageId) => {
|
|
313
|
+
const element = elements[index];
|
|
314
|
+
if (isNotNullish(element) && isJSONStringLiteral(element)) context.report({
|
|
315
|
+
data: { file: element.value },
|
|
316
|
+
messageId,
|
|
317
|
+
node: element,
|
|
318
|
+
suggest: [{
|
|
319
|
+
fix: fixRemoveArrayElement(context, index, elements),
|
|
320
|
+
messageId: "remove"
|
|
321
|
+
}]
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=bin]"(node) {
|
|
326
|
+
const binValue = node.value;
|
|
327
|
+
if (isJSONStringLiteral(binValue)) entryCache.bin.push(binValue.value);
|
|
328
|
+
else if (binValue.type === "JSONObjectExpression") {
|
|
329
|
+
for (const prop of binValue.properties) if (isJSONStringLiteral(prop.value)) entryCache.bin.push(prop.value.value);
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=files]"(node) {
|
|
333
|
+
if (node.value.type !== "JSONArrayExpression") return;
|
|
334
|
+
const seen = /* @__PURE__ */ new Set();
|
|
335
|
+
const elements = node.value.elements;
|
|
336
|
+
entryCache.files = elements;
|
|
337
|
+
for (const [index, element] of elements.entries()) if (isNotNullish(element) && isJSONStringLiteral(element)) {
|
|
338
|
+
if (seen.has(element.value)) report(elements, index, "duplicate");
|
|
339
|
+
else seen.add(element.value);
|
|
340
|
+
for (const defaultFile of defaultFiles) if (defaultFile.test(element.value)) report(elements, index, "unnecessaryDefault");
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=main]"(node) {
|
|
344
|
+
if (isJSONStringLiteral(node.value)) entryCache.main = node.value.value;
|
|
345
|
+
},
|
|
346
|
+
"Program:exit"() {
|
|
347
|
+
const files = entryCache.files;
|
|
348
|
+
if (files.length === 0) return;
|
|
349
|
+
const validations = [{
|
|
350
|
+
files: entryCache.main ? [entryCache.main] : [],
|
|
351
|
+
messageId: "unnecessaryMain"
|
|
352
|
+
}, {
|
|
353
|
+
files: entryCache.bin,
|
|
354
|
+
messageId: "unnecessaryBin"
|
|
355
|
+
}];
|
|
356
|
+
for (const validation of validations) for (const fileToCheck of validation.files) for (const [index, fileEntry] of files.entries()) if (isNotNullish(fileEntry) && isJSONStringLiteral(fileEntry)) {
|
|
357
|
+
if (getCachedLocalFileRegex(fileEntry.value)?.test(fileToCheck)) report(files, index, validation.messageId);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
meta: {
|
|
363
|
+
docs: {
|
|
364
|
+
category: "Best Practices",
|
|
365
|
+
description: "Prevents adding unnecessary / redundant files.",
|
|
366
|
+
recommended: true
|
|
367
|
+
},
|
|
368
|
+
hasSuggestions: true,
|
|
369
|
+
messages: {
|
|
370
|
+
duplicate: "Files has more than one entry for \"{{file}}\".",
|
|
371
|
+
remove: "Remove this redundant entry.",
|
|
372
|
+
unnecessaryBin: `Explicitly declaring "{{file}}" in "files" is unnecessary; it's included in "bin".`,
|
|
373
|
+
unnecessaryDefault: `Explicitly declaring "{{file}}" in "files" is unnecessary; it's included by default.`,
|
|
374
|
+
unnecessaryMain: `Explicitly declaring "{{file}}" in "files" is unnecessary; it's the "main" entry.`
|
|
375
|
+
},
|
|
376
|
+
schema: [],
|
|
377
|
+
type: "suggestion"
|
|
378
|
+
},
|
|
379
|
+
name: "no-redundant-files"
|
|
380
|
+
});
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/rules/no-redundant-publishConfig.ts
|
|
383
|
+
const rule$13 = createRule({
|
|
384
|
+
create(context) {
|
|
385
|
+
let packageName;
|
|
386
|
+
let publishConfigAccessProperty;
|
|
387
|
+
return {
|
|
388
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=name]"(node) {
|
|
389
|
+
if (isJSONStringLiteral(node.value)) packageName = node.value.value;
|
|
390
|
+
},
|
|
391
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=publishConfig]"(node) {
|
|
392
|
+
if (node.value.type !== "JSONObjectExpression") return;
|
|
393
|
+
for (const property of node.value.properties) if (isJSONStringLiteral(property.key) && property.key.value === "access") {
|
|
394
|
+
publishConfigAccessProperty = property;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
"Program:exit"() {
|
|
399
|
+
if (!packageName || !publishConfigAccessProperty) return;
|
|
400
|
+
if (!packageName.startsWith("@")) context.report({
|
|
401
|
+
messageId: "redundantAccess",
|
|
402
|
+
node: publishConfigAccessProperty,
|
|
403
|
+
suggest: [{
|
|
404
|
+
fix: fixRemoveObjectProperty(context, publishConfigAccessProperty),
|
|
405
|
+
messageId: "removeAccess"
|
|
406
|
+
}]
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
meta: {
|
|
412
|
+
docs: {
|
|
413
|
+
category: "Best Practices",
|
|
414
|
+
description: "Warns when publishConfig.access is used in unscoped packages.",
|
|
415
|
+
recommended: true
|
|
416
|
+
},
|
|
417
|
+
hasSuggestions: true,
|
|
418
|
+
messages: {
|
|
419
|
+
redundantAccess: "'publishConfig.access' is redundant for unscoped packages - they are always public.",
|
|
420
|
+
removeAccess: "Remove the redundant 'access' field."
|
|
421
|
+
},
|
|
422
|
+
schema: [],
|
|
423
|
+
type: "suggestion"
|
|
424
|
+
},
|
|
425
|
+
name: "no-redundant-publishConfig"
|
|
426
|
+
});
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/rules/order-properties.ts
|
|
429
|
+
const rule$12 = createRule({
|
|
430
|
+
create(context) {
|
|
431
|
+
return { "Program:exit"() {
|
|
432
|
+
const { ast, text } = context.sourceCode;
|
|
433
|
+
const { order = "sort-package-json" } = context.options[0] ?? {};
|
|
434
|
+
const requiredOrder = order === "sort-package-json" ? sortOrder : order;
|
|
435
|
+
const json = JSON.parse(text);
|
|
436
|
+
const orderedNonStandardKeys = Object.keys(json).filter((key) => !requiredOrder.includes(key)).sort();
|
|
437
|
+
const orderedSource = sortObjectKeys(json, [...requiredOrder, ...orderedNonStandardKeys]);
|
|
438
|
+
const orderedKeys = Object.keys(orderedSource);
|
|
439
|
+
const { properties } = ast.body[0].expression;
|
|
440
|
+
for (let i = 0; i < properties.length; i += 1) {
|
|
441
|
+
const { value } = properties[i].key;
|
|
442
|
+
if (value === orderedKeys[i]) continue;
|
|
443
|
+
context.report({
|
|
444
|
+
data: { property: value },
|
|
445
|
+
fix(fixer) {
|
|
446
|
+
const { indent, type } = detectIndent(text);
|
|
447
|
+
const endCharacters = text.endsWith("\n") ? "\n" : "";
|
|
448
|
+
const newline = detectNewlineGraceful(text);
|
|
449
|
+
let result = JSON.stringify(orderedSource, null, type === "tab" ? " " : indent) + endCharacters;
|
|
450
|
+
if (newline === "\r\n") result = result.replace(/\n/g, newline);
|
|
451
|
+
return fixer.replaceText(context.sourceCode.ast, result);
|
|
452
|
+
},
|
|
453
|
+
loc: properties[i].loc,
|
|
454
|
+
messageId: "incorrectOrder"
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
} };
|
|
458
|
+
},
|
|
459
|
+
meta: {
|
|
460
|
+
defaultOptions: [{ order: "sort-package-json" }],
|
|
461
|
+
docs: {
|
|
462
|
+
category: "Stylistic",
|
|
463
|
+
description: "Enforces that package properties are declared in a consistent order."
|
|
464
|
+
},
|
|
465
|
+
fixable: "code",
|
|
466
|
+
messages: { incorrectOrder: "Top-level property `{{property}}` is not ordered in the standard way." },
|
|
467
|
+
schema: [{
|
|
468
|
+
additionalProperties: false,
|
|
469
|
+
properties: { order: {
|
|
470
|
+
anyOf: [{
|
|
471
|
+
enum: ["sort-package-json"],
|
|
472
|
+
type: ["string"]
|
|
473
|
+
}, {
|
|
474
|
+
items: { type: ["string"] },
|
|
475
|
+
type: ["array"]
|
|
476
|
+
}],
|
|
477
|
+
description: "Specifies the sorting order of top-level properties."
|
|
478
|
+
} },
|
|
479
|
+
type: "object"
|
|
480
|
+
}],
|
|
481
|
+
type: "layout"
|
|
482
|
+
},
|
|
483
|
+
name: "order-properties"
|
|
484
|
+
});
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/utils/findPropertyWithKeyValue.ts
|
|
487
|
+
function findPropertyWithKeyValue(properties, value) {
|
|
488
|
+
return properties.find((property) => property.key.type === "JSONLiteral" && property.key.value === value);
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/rules/repository-shorthand.ts
|
|
492
|
+
const providerRegexes = {
|
|
493
|
+
bitbucket: /^(?:git\+)?(?:ssh:\/\/git@|http?s:\/\/)?(?:www\.)?bitbucket\.org\//,
|
|
494
|
+
gist: /^(?:git\+)?(?:ssh:\/\/git@|http?s:\/\/)?(?:www\.)?gist\.github\.com\//,
|
|
495
|
+
github: /^(?:git\+)?(?:ssh:\/\/git@|http?s:\/\/)?(?:www\.)?github\.com\//,
|
|
496
|
+
gitlab: /^(?:git\+)?(?:ssh:\/\/git@|http?s:\/\/)?(?:www\.)?gitlab\.com\//
|
|
497
|
+
};
|
|
498
|
+
const providerUrls = {
|
|
499
|
+
bitbucket: "https://bitbucket.org/",
|
|
500
|
+
gist: "https://gist.github.com/",
|
|
501
|
+
github: "https://github.com/",
|
|
502
|
+
gitlab: "https://gitlab.com/"
|
|
503
|
+
};
|
|
504
|
+
const providers = Object.keys(providerRegexes);
|
|
505
|
+
const isProvider = (value) => value in providerRegexes;
|
|
506
|
+
const cleanUrl = (url, provider) => url.replace(providerRegexes[provider], "").replace(/\.git$/, "");
|
|
507
|
+
const getProviderFromUrl = (url) => {
|
|
508
|
+
return providers.find((provider) => providerRegexes[provider].test(url));
|
|
509
|
+
};
|
|
510
|
+
const createShorthand = (url, provider) => {
|
|
511
|
+
return `${provider}:${cleanUrl(url, provider)}`;
|
|
512
|
+
};
|
|
513
|
+
const createUrl = (shorthand) => {
|
|
514
|
+
if (shorthand.includes(":")) {
|
|
515
|
+
const [provider, repo] = shorthand.split(":", 2);
|
|
516
|
+
if (isProvider(provider)) return `${providerUrls[provider]}${repo}`;
|
|
517
|
+
}
|
|
518
|
+
return `${providerUrls.github}${shorthand}`;
|
|
519
|
+
};
|
|
520
|
+
const rule$11 = createRule({
|
|
521
|
+
create(context) {
|
|
522
|
+
const [{ form = "object" } = {}] = context.options;
|
|
523
|
+
function validateRepositoryForObject(node) {
|
|
524
|
+
if (isJSONStringLiteral(node.value)) context.report({
|
|
525
|
+
fix(fixer) {
|
|
526
|
+
if (!isJSONStringLiteral(node.value) || node.value.value.split("/").filter(Boolean).length !== 2) return null;
|
|
527
|
+
return fixer.replaceText(node.value, JSON.stringify({
|
|
528
|
+
type: "git",
|
|
529
|
+
url: createUrl(node.value.value)
|
|
530
|
+
}, null, 2));
|
|
531
|
+
},
|
|
532
|
+
messageId: "preferObject",
|
|
533
|
+
node: node.value
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function validateRepositoryForShorthand(node) {
|
|
537
|
+
if (isJSONStringLiteral(node.value)) {
|
|
538
|
+
const { value } = node.value;
|
|
539
|
+
const provider = getProviderFromUrl(value);
|
|
540
|
+
if (provider) context.report({
|
|
541
|
+
fix(fixer) {
|
|
542
|
+
return fixer.replaceText(node.value, JSON.stringify(createShorthand(value, provider)));
|
|
543
|
+
},
|
|
544
|
+
messageId: "preferShorthand",
|
|
545
|
+
node: node.value
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (node.value.type !== "JSONObjectExpression") return;
|
|
550
|
+
const { properties } = node.value;
|
|
551
|
+
if (findPropertyWithKeyValue(properties, "directory")) return;
|
|
552
|
+
const typeProperty = findPropertyWithKeyValue(properties, "type");
|
|
553
|
+
if (typeProperty?.value.type !== "JSONLiteral" || typeProperty.value.value !== "git") return;
|
|
554
|
+
const urlProperty = findPropertyWithKeyValue(properties, "url");
|
|
555
|
+
if (urlProperty?.value.type !== "JSONLiteral" || typeof urlProperty.value.value !== "string") return;
|
|
556
|
+
const url = urlProperty.value.value;
|
|
557
|
+
const provider = getProviderFromUrl(url);
|
|
558
|
+
if (provider) context.report({
|
|
559
|
+
fix(fixer) {
|
|
560
|
+
return fixer.replaceText(node.value, JSON.stringify(createShorthand(url, provider)));
|
|
561
|
+
},
|
|
562
|
+
messageId: "preferShorthand",
|
|
563
|
+
node: node.value
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return { JSONProperty(node) {
|
|
567
|
+
if (node.key.type !== "JSONLiteral" || node.key.value !== "repository" || node.parent.parent.parent.type !== "Program") return;
|
|
568
|
+
if (form === "shorthand") validateRepositoryForShorthand(node);
|
|
569
|
+
else validateRepositoryForObject(node);
|
|
570
|
+
} };
|
|
571
|
+
},
|
|
572
|
+
meta: {
|
|
573
|
+
defaultOptions: [{ form: "object" }],
|
|
574
|
+
docs: {
|
|
575
|
+
category: "Best Practices",
|
|
576
|
+
description: "Enforce either object or shorthand declaration for repository.",
|
|
577
|
+
recommended: true
|
|
578
|
+
},
|
|
579
|
+
fixable: "code",
|
|
580
|
+
messages: {
|
|
581
|
+
preferObject: "Prefer an object locator for a repository.",
|
|
582
|
+
preferShorthand: "Prefer a shorthand locator for a supported repository provider."
|
|
583
|
+
},
|
|
584
|
+
schema: [{
|
|
585
|
+
additionalProperties: false,
|
|
586
|
+
properties: { form: {
|
|
587
|
+
description: "Specifies which repository form to enforce.",
|
|
588
|
+
enum: ["object", "shorthand"],
|
|
589
|
+
type: ["string"]
|
|
590
|
+
} },
|
|
591
|
+
type: "object"
|
|
592
|
+
}],
|
|
593
|
+
type: "suggestion"
|
|
594
|
+
},
|
|
595
|
+
name: "repository-shorthand"
|
|
596
|
+
});
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/rules/require-attribution.ts
|
|
599
|
+
const rule$10 = createRule({
|
|
600
|
+
create(context) {
|
|
601
|
+
const preferContributorsOnly = context.options[0]?.preferContributorsOnly ?? false;
|
|
602
|
+
const ignorePrivate = context.options[0]?.ignorePrivate ?? true;
|
|
603
|
+
let authorPropertyNode;
|
|
604
|
+
let contributorsPropertyNode;
|
|
605
|
+
let isPrivatePackage = false;
|
|
606
|
+
return {
|
|
607
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=author]"(node) {
|
|
608
|
+
authorPropertyNode = node;
|
|
609
|
+
},
|
|
610
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=contributors]"(node) {
|
|
611
|
+
contributorsPropertyNode = node;
|
|
612
|
+
const contributorsValue = node.value;
|
|
613
|
+
if (contributorsValue.type !== "JSONArrayExpression" || contributorsValue.elements.every((element) => !element)) context.report({
|
|
614
|
+
messageId: "noContributors",
|
|
615
|
+
node
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=private]"(node) {
|
|
619
|
+
if (node.value.type === "JSONLiteral" && node.value.value === true) isPrivatePackage = true;
|
|
620
|
+
},
|
|
621
|
+
"Program:exit"() {
|
|
622
|
+
if (ignorePrivate && isPrivatePackage) return;
|
|
623
|
+
if (preferContributorsOnly) {
|
|
624
|
+
if (authorPropertyNode) context.report({
|
|
625
|
+
messageId: "contributorsOnly",
|
|
626
|
+
node: authorPropertyNode,
|
|
627
|
+
suggest: [{
|
|
628
|
+
fix: fixRemoveObjectProperty(context, authorPropertyNode),
|
|
629
|
+
messageId: "removeAuthor"
|
|
630
|
+
}]
|
|
631
|
+
});
|
|
632
|
+
if (!contributorsPropertyNode) context.report({
|
|
633
|
+
loc: {
|
|
634
|
+
column: 0,
|
|
635
|
+
line: 1
|
|
636
|
+
},
|
|
637
|
+
messageId: "missingContributor"
|
|
638
|
+
});
|
|
639
|
+
} else if (!authorPropertyNode && !contributorsPropertyNode) context.report({
|
|
640
|
+
loc: {
|
|
641
|
+
column: 0,
|
|
642
|
+
line: 1
|
|
643
|
+
},
|
|
644
|
+
messageId: "missing"
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
},
|
|
649
|
+
meta: {
|
|
650
|
+
defaultOptions: [{
|
|
651
|
+
ignorePrivate: true,
|
|
652
|
+
preferContributorsOnly: false
|
|
653
|
+
}],
|
|
654
|
+
docs: {
|
|
655
|
+
category: "Best Practices",
|
|
656
|
+
description: "Ensures that proper attribution is included, requiring that either `author` or `contributors` is defined, and that if `contributors` is present, it should include at least one contributor.",
|
|
657
|
+
recommended: true
|
|
658
|
+
},
|
|
659
|
+
hasSuggestions: true,
|
|
660
|
+
messages: {
|
|
661
|
+
contributorsOnly: "Only `contributors` should be defined for attribution.",
|
|
662
|
+
missing: "Property attribution is required. Either `author` or `contributors` should be defined.",
|
|
663
|
+
missingContributor: "Property attribution is required. `contributors` should be defined.",
|
|
664
|
+
noContributors: "At least one contributor should be defined.",
|
|
665
|
+
removeAuthor: "Remove `author`."
|
|
666
|
+
},
|
|
667
|
+
schema: [{
|
|
668
|
+
additionalProperties: false,
|
|
669
|
+
properties: {
|
|
670
|
+
ignorePrivate: {
|
|
671
|
+
description: "Skip attribution requirements for packages with `\"private\": true`.",
|
|
672
|
+
type: "boolean"
|
|
673
|
+
},
|
|
674
|
+
preferContributorsOnly: {
|
|
675
|
+
description: "Require that only `contributors` is present, and `author` is not defined.",
|
|
676
|
+
type: "boolean"
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
type: "object"
|
|
680
|
+
}],
|
|
681
|
+
type: "suggestion"
|
|
682
|
+
},
|
|
683
|
+
name: "require-attribution"
|
|
684
|
+
});
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/utils/createSimpleRequirePropertyRule.ts
|
|
687
|
+
/**
|
|
688
|
+
* Given a top-level property name, create a rule that requires that property to be present.
|
|
689
|
+
* Optionally, include it in the recommended config.
|
|
690
|
+
* Note: this will only create a basic require rule, with no options. If you need
|
|
691
|
+
* to create a more complex rule, create it in its own file.
|
|
692
|
+
*/
|
|
693
|
+
const createSimpleRequirePropertyRule = (propertyName, { category, fixValue, ignorePrivateDefault = false, isRecommended } = {}) => {
|
|
694
|
+
const ruleName = `require-${propertyName}`;
|
|
695
|
+
return {
|
|
696
|
+
rule: createRule({
|
|
697
|
+
create(context) {
|
|
698
|
+
const enforceForPrivate = context.settings.packageJson?.enforceForPrivate;
|
|
699
|
+
const ignorePrivate = context.options[0]?.ignorePrivate ?? (typeof enforceForPrivate === "boolean" ? !enforceForPrivate : ignorePrivateDefault);
|
|
700
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression"(node) {
|
|
701
|
+
if (ignorePrivate && node.properties.some((property) => isJSONStringLiteral(property.key) && property.key.value === "private" && property.value.type === "JSONLiteral" && property.value.value === true)) return;
|
|
702
|
+
if (node.properties.every((property) => !(isJSONStringLiteral(property.key) && property.key.value === propertyName))) {
|
|
703
|
+
const resolvedValue = typeof fixValue === "function" ? fixValue() : fixValue;
|
|
704
|
+
context.report({
|
|
705
|
+
data: { property: propertyName },
|
|
706
|
+
fix: resolvedValue === void 0 ? void 0 : function* (fixer) {
|
|
707
|
+
yield fixer.insertTextAfterRange([0, 1], `\n "${propertyName}": ${JSON.stringify(resolvedValue, null, 2).split("\n").join("\n ")}`);
|
|
708
|
+
yield node.properties.length > 0 ? fixer.insertTextAfterRange([0, 1], ",") : fixer.insertTextAfterRange([0, 1], "\n");
|
|
709
|
+
},
|
|
710
|
+
loc: {
|
|
711
|
+
column: 0,
|
|
712
|
+
line: 1
|
|
713
|
+
},
|
|
714
|
+
messageId: "missing"
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
} };
|
|
718
|
+
},
|
|
719
|
+
meta: {
|
|
720
|
+
docs: {
|
|
721
|
+
category,
|
|
722
|
+
description: `Requires the \`${propertyName}\` property to be present.`,
|
|
723
|
+
recommended: isRecommended,
|
|
724
|
+
ruleGroup: "require-properties"
|
|
725
|
+
},
|
|
726
|
+
fixable: fixValue === void 0 ? void 0 : "code",
|
|
727
|
+
messages: { missing: "Property '{{property}}' is required." },
|
|
728
|
+
schema: propertyName === "private" ? [] : [{
|
|
729
|
+
additionalProperties: false,
|
|
730
|
+
properties: { ignorePrivate: {
|
|
731
|
+
default: ignorePrivateDefault,
|
|
732
|
+
description: "Determines if this rule should be enforced when the package's `private` property is `true`.",
|
|
733
|
+
type: "boolean"
|
|
734
|
+
} },
|
|
735
|
+
type: "object"
|
|
736
|
+
}],
|
|
737
|
+
type: "suggestion"
|
|
738
|
+
},
|
|
739
|
+
name: ruleName
|
|
740
|
+
}),
|
|
741
|
+
ruleName
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/utils/git/getGitAuthor.ts
|
|
746
|
+
let cachedAuthor;
|
|
747
|
+
const getGitAuthor = () => {
|
|
748
|
+
if (cachedAuthor === null) return;
|
|
749
|
+
else if (cachedAuthor !== void 0) return cachedAuthor;
|
|
750
|
+
try {
|
|
751
|
+
const name = execFileSync("git", [
|
|
752
|
+
"config",
|
|
753
|
+
"--get",
|
|
754
|
+
"user.name"
|
|
755
|
+
], { encoding: "utf8" }).trim();
|
|
756
|
+
const email = execFileSync("git", [
|
|
757
|
+
"config",
|
|
758
|
+
"--get",
|
|
759
|
+
"user.email"
|
|
760
|
+
], { encoding: "utf8" }).trim();
|
|
761
|
+
cachedAuthor = {
|
|
762
|
+
name,
|
|
763
|
+
...email && { email }
|
|
764
|
+
};
|
|
765
|
+
} catch {
|
|
766
|
+
cachedAuthor = null;
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
return cachedAuthor;
|
|
770
|
+
};
|
|
771
|
+
const rules$2 = Object.fromEntries([
|
|
772
|
+
["author", { fixValue: getGitAuthor }],
|
|
773
|
+
["bin"],
|
|
774
|
+
["browser"],
|
|
775
|
+
["bugs", { ignorePrivateDefault: true }],
|
|
776
|
+
["bundleDependencies"],
|
|
777
|
+
["config"],
|
|
778
|
+
["contributors"],
|
|
779
|
+
["cpu"],
|
|
780
|
+
["dependencies"],
|
|
781
|
+
["description", { isRecommended: true }],
|
|
782
|
+
["devDependencies"],
|
|
783
|
+
["devEngines"],
|
|
784
|
+
["directories"],
|
|
785
|
+
["engines"],
|
|
786
|
+
["exports", {
|
|
787
|
+
ignorePrivateDefault: true,
|
|
788
|
+
isRecommended: true
|
|
789
|
+
}],
|
|
790
|
+
["files", {
|
|
791
|
+
ignorePrivateDefault: true,
|
|
792
|
+
isRecommended: true
|
|
793
|
+
}],
|
|
794
|
+
["funding"],
|
|
795
|
+
["gypfile"],
|
|
796
|
+
["homepage", { ignorePrivateDefault: true }],
|
|
797
|
+
["keywords", { ignorePrivateDefault: true }],
|
|
798
|
+
["libc"],
|
|
799
|
+
["license", {
|
|
800
|
+
ignorePrivateDefault: true,
|
|
801
|
+
isRecommended: true
|
|
802
|
+
}],
|
|
803
|
+
["main"],
|
|
804
|
+
["man"],
|
|
805
|
+
["module"],
|
|
806
|
+
["name", {
|
|
807
|
+
ignorePrivateDefault: true,
|
|
808
|
+
isRecommended: true
|
|
809
|
+
}],
|
|
810
|
+
["optionalDependencies"],
|
|
811
|
+
["os"],
|
|
812
|
+
["packageManager"],
|
|
813
|
+
["peerDependencies"],
|
|
814
|
+
["peerDependenciesMeta"],
|
|
815
|
+
["private", { fixValue: false }],
|
|
816
|
+
["publishConfig", { ignorePrivateDefault: true }],
|
|
817
|
+
["repository", {
|
|
818
|
+
ignorePrivateDefault: true,
|
|
819
|
+
isRecommended: true
|
|
820
|
+
}],
|
|
821
|
+
["scripts"],
|
|
822
|
+
["sideEffects", {
|
|
823
|
+
ignorePrivateDefault: true,
|
|
824
|
+
isRecommended: true
|
|
825
|
+
}],
|
|
826
|
+
["type", {
|
|
827
|
+
fixValue: "commonjs",
|
|
828
|
+
isRecommended: true
|
|
829
|
+
}],
|
|
830
|
+
["types"],
|
|
831
|
+
["version", {
|
|
832
|
+
ignorePrivateDefault: true,
|
|
833
|
+
isRecommended: true
|
|
834
|
+
}]
|
|
835
|
+
].map(([propertyName, options]) => {
|
|
836
|
+
const { rule, ruleName } = createSimpleRequirePropertyRule(propertyName, options);
|
|
837
|
+
return [ruleName, rule];
|
|
838
|
+
}));
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region src/rules/restrict-dependency-ranges.ts
|
|
841
|
+
const DEPENDENCY_TYPES = [
|
|
842
|
+
"dependencies",
|
|
843
|
+
"devDependencies",
|
|
844
|
+
"optionalDependencies",
|
|
845
|
+
"peerDependencies"
|
|
846
|
+
];
|
|
847
|
+
const RANGE_TYPES = [
|
|
848
|
+
"caret",
|
|
849
|
+
"pin",
|
|
850
|
+
"tilde"
|
|
851
|
+
];
|
|
852
|
+
const schemaOptions = {
|
|
853
|
+
additionalProperties: false,
|
|
854
|
+
properties: {
|
|
855
|
+
forDependencyTypes: {
|
|
856
|
+
description: "Apply a range type restriction for an entire group of dependencies by which type of dependencies they belong to.",
|
|
857
|
+
items: { enum: DEPENDENCY_TYPES },
|
|
858
|
+
type: "array"
|
|
859
|
+
},
|
|
860
|
+
forPackages: {
|
|
861
|
+
description: "The exact name of a package, or a regex pattern used to match a group of packages by name.",
|
|
862
|
+
items: { type: "string" },
|
|
863
|
+
type: "array"
|
|
864
|
+
},
|
|
865
|
+
forVersions: {
|
|
866
|
+
description: "Apply a restriction to a specific semver range.",
|
|
867
|
+
type: "string"
|
|
868
|
+
},
|
|
869
|
+
rangeType: {
|
|
870
|
+
description: "Identifies which range type or types you want to apply to packages that match any of the other match options (or all dependencies if no other options are provided).",
|
|
871
|
+
oneOf: [{ enum: RANGE_TYPES }, {
|
|
872
|
+
items: { enum: RANGE_TYPES },
|
|
873
|
+
type: "array"
|
|
874
|
+
}]
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
required: ["rangeType"],
|
|
878
|
+
type: "object"
|
|
879
|
+
};
|
|
880
|
+
const SYMBOLS = {
|
|
881
|
+
caret: "^",
|
|
882
|
+
pin: "",
|
|
883
|
+
tilde: "~"
|
|
884
|
+
};
|
|
885
|
+
/**
|
|
886
|
+
* Given the original version, update it to use the correct range type.
|
|
887
|
+
*/
|
|
888
|
+
const changeVersionRange = (version, rangeType) => {
|
|
889
|
+
if (/^workspace:[~^*]$/.test(version)) switch (rangeType) {
|
|
890
|
+
case "caret": return "workspace:^";
|
|
891
|
+
case "pin": return "workspace:*";
|
|
892
|
+
default: return "workspace:~";
|
|
893
|
+
}
|
|
894
|
+
return version.replace(/^(workspace:)?(\^|~|<=?|>=?)?/, `$1${SYMBOLS[rangeType]}`);
|
|
895
|
+
};
|
|
896
|
+
/**
|
|
897
|
+
* Check if the version is in a form that this rule supports.
|
|
898
|
+
*/
|
|
899
|
+
const isVersionSupported = (version) => {
|
|
900
|
+
if (/^workspace:[*^~]$/.test(version)) return true;
|
|
901
|
+
const rawVersion = version.replace(/^workspace:/, "");
|
|
902
|
+
return !!semver.validRange(rawVersion);
|
|
903
|
+
};
|
|
904
|
+
const capitalize = (str) => {
|
|
905
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
906
|
+
};
|
|
907
|
+
const rule$9 = createRule({
|
|
908
|
+
create(context) {
|
|
909
|
+
if (!context.options[0]) return {};
|
|
910
|
+
const optionsArray = (Array.isArray(context.options[0]) ? context.options[0].toReversed() : [context.options[0]]).map((option) => ({
|
|
911
|
+
...option,
|
|
912
|
+
forPackages: option.forPackages?.map((pattern) => new RegExp(pattern)),
|
|
913
|
+
rangeTypes: Array.isArray(option.rangeType) ? option.rangeType : [option.rangeType]
|
|
914
|
+
}));
|
|
915
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.type=JSONLiteral][value.type=JSONObjectExpression]"(node) {
|
|
916
|
+
const dependencyType = node.key.value;
|
|
917
|
+
if (!DEPENDENCY_TYPES.includes(dependencyType)) return;
|
|
918
|
+
for (const property of node.value.properties) {
|
|
919
|
+
if (!isJSONStringLiteral(property.key) || !isJSONStringLiteral(property.value)) continue;
|
|
920
|
+
const name = property.key.value;
|
|
921
|
+
const version = property.value.value;
|
|
922
|
+
if (!isVersionSupported(version)) continue;
|
|
923
|
+
const isPinned = !!semver.parse(version) || version === "workspace:*";
|
|
924
|
+
const isTildeRange = !!semver.validRange(version) && version.startsWith("~") || version.startsWith("workspace:~");
|
|
925
|
+
const isCaretRange = !!semver.validRange(version) && version.startsWith("^") || version.startsWith("workspace:^");
|
|
926
|
+
for (const options of optionsArray) {
|
|
927
|
+
if (options.forDependencyTypes && !options.forDependencyTypes.includes(dependencyType)) continue;
|
|
928
|
+
if (options.forPackages) {
|
|
929
|
+
if (!options.forPackages.some((packageNameRegex) => packageNameRegex.test(name))) continue;
|
|
930
|
+
}
|
|
931
|
+
if (options.forVersions && (/^workspace:[^~*]?$/.test(version) || version !== "*" && !semver.satisfies(version.replace(/(?:workspace:)?[^~]?/, ""), options.forVersions))) continue;
|
|
932
|
+
const rangeTypes = options.rangeTypes;
|
|
933
|
+
if (version === "*") {
|
|
934
|
+
context.report({
|
|
935
|
+
data: { rangeTypes: rangeTypes.join(", ") },
|
|
936
|
+
messageId: "wrongRangeType",
|
|
937
|
+
node: property.value
|
|
938
|
+
});
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
if (!rangeTypes.some((rangeType) => {
|
|
942
|
+
switch (rangeType) {
|
|
943
|
+
case "caret": return isCaretRange;
|
|
944
|
+
case "pin": return isPinned;
|
|
945
|
+
case "tilde": return isTildeRange;
|
|
946
|
+
}
|
|
947
|
+
})) context.report({
|
|
948
|
+
data: { rangeTypes: rangeTypes.join(", ") },
|
|
949
|
+
messageId: "wrongRangeType",
|
|
950
|
+
node: property.value,
|
|
951
|
+
suggest: rangeTypes.map((rangeType) => ({
|
|
952
|
+
fix(fixer) {
|
|
953
|
+
return fixer.replaceText(property.value, `"${changeVersionRange(version, rangeType)}"`);
|
|
954
|
+
},
|
|
955
|
+
messageId: `changeTo${capitalize(rangeType)}`
|
|
956
|
+
}))
|
|
957
|
+
});
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
} };
|
|
962
|
+
},
|
|
963
|
+
meta: {
|
|
964
|
+
defaultOptions: [[]],
|
|
965
|
+
docs: { description: "Restricts the range of dependencies to allow or disallow specific types of ranges." },
|
|
966
|
+
hasSuggestions: true,
|
|
967
|
+
messages: {
|
|
968
|
+
changeToCaret: "Change to use a caret range.",
|
|
969
|
+
changeToPin: "Pin the version.",
|
|
970
|
+
changeToTilde: "Change to use a tilde range.",
|
|
971
|
+
wrongRangeType: "This dependency is using the wrong range type. Acceptable range type(s): {{rangeTypes}}"
|
|
972
|
+
},
|
|
973
|
+
schema: [{ oneOf: [schemaOptions, {
|
|
974
|
+
description: "Array of configuration options, specifying range requirements.",
|
|
975
|
+
items: schemaOptions,
|
|
976
|
+
type: "array"
|
|
977
|
+
}] }],
|
|
978
|
+
type: "suggestion"
|
|
979
|
+
},
|
|
980
|
+
name: "restrict-dependency-ranges"
|
|
981
|
+
});
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/rules/restrict-private-properties.ts
|
|
984
|
+
const defaultBlockedProperties = ["files", "publishConfig"];
|
|
985
|
+
const rule$8 = createRule({
|
|
986
|
+
create(context) {
|
|
987
|
+
const blockedProperties = context.options[0]?.blockedProperties ?? defaultBlockedProperties;
|
|
988
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression"(node) {
|
|
989
|
+
if (node.properties.every((property) => !(isJSONStringLiteral(property.key) && property.key.value === "private" && property.value.type === "JSONLiteral" && property.value.value === true))) return;
|
|
990
|
+
for (const property of node.properties) if (isJSONStringLiteral(property.key) && blockedProperties.includes(property.key.value)) {
|
|
991
|
+
const isEmpty = property.value.type === "JSONArrayExpression" && property.value.elements.length === 0 || property.value.type === "JSONObjectExpression" && property.value.properties.length === 0;
|
|
992
|
+
context.report({
|
|
993
|
+
data: { property: property.key.value },
|
|
994
|
+
messageId: "unnecessaryProperty",
|
|
995
|
+
node: property,
|
|
996
|
+
...isEmpty ? { fix: fixRemoveObjectProperty(context, property) } : { suggest: [{
|
|
997
|
+
data: { property: property.key.value },
|
|
998
|
+
fix: fixRemoveObjectProperty(context, property),
|
|
999
|
+
messageId: "removePropertySuggestion"
|
|
1000
|
+
}] }
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
} };
|
|
1004
|
+
},
|
|
1005
|
+
meta: {
|
|
1006
|
+
defaultOptions: [{ blockedProperties: defaultBlockedProperties }],
|
|
1007
|
+
docs: {
|
|
1008
|
+
category: "Best Practices",
|
|
1009
|
+
description: "Disallows unnecessary properties in private packages.",
|
|
1010
|
+
recommended: false
|
|
1011
|
+
},
|
|
1012
|
+
fixable: "code",
|
|
1013
|
+
hasSuggestions: true,
|
|
1014
|
+
messages: {
|
|
1015
|
+
removePropertySuggestion: "Remove the '{{ property }}' field.",
|
|
1016
|
+
unnecessaryProperty: "The '{{ property }}' field is unnecessary in private packages and can be removed."
|
|
1017
|
+
},
|
|
1018
|
+
schema: [{
|
|
1019
|
+
additionalProperties: false,
|
|
1020
|
+
properties: { blockedProperties: {
|
|
1021
|
+
description: "Array of property names to disallow in private packages.",
|
|
1022
|
+
items: { type: "string" },
|
|
1023
|
+
type: "array"
|
|
1024
|
+
} },
|
|
1025
|
+
type: "object"
|
|
1026
|
+
}],
|
|
1027
|
+
type: "suggestion"
|
|
1028
|
+
},
|
|
1029
|
+
name: "restrict-private-properties"
|
|
1030
|
+
});
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region src/rules/restrict-top-level-properties.ts
|
|
1033
|
+
const rule$7 = createRule({
|
|
1034
|
+
create(context) {
|
|
1035
|
+
const banList = Object.fromEntries(
|
|
1036
|
+
/* v8 ignore next - not possible to cover due to presence of `defaultOptions` */
|
|
1037
|
+
(context.options[0]?.ban ?? []).map((entry) => typeof entry === "string" ? [entry, ""] : [entry.property, entry.message ?? ""])
|
|
1038
|
+
);
|
|
1039
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression"(node) {
|
|
1040
|
+
for (const property of node.properties) {
|
|
1041
|
+
if (!isJSONStringLiteral(property.key)) continue;
|
|
1042
|
+
const propertyName = property.key.value;
|
|
1043
|
+
if (Object.hasOwn(banList, propertyName)) {
|
|
1044
|
+
const customMessage = banList[propertyName];
|
|
1045
|
+
context.report({
|
|
1046
|
+
data: {
|
|
1047
|
+
customMessage: customMessage ? `: ${customMessage}` : "",
|
|
1048
|
+
property: propertyName
|
|
1049
|
+
},
|
|
1050
|
+
messageId: "bannedProperty",
|
|
1051
|
+
node: property.key,
|
|
1052
|
+
suggest: [{
|
|
1053
|
+
fix: fixRemoveObjectProperty(context, property),
|
|
1054
|
+
messageId: "removePropertySuggestion"
|
|
1055
|
+
}]
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
} };
|
|
1060
|
+
},
|
|
1061
|
+
meta: {
|
|
1062
|
+
defaultOptions: [{ ban: [] }],
|
|
1063
|
+
docs: {
|
|
1064
|
+
category: "Best Practices",
|
|
1065
|
+
description: "Disallows specified top-level properties in package.json.",
|
|
1066
|
+
recommended: false
|
|
1067
|
+
},
|
|
1068
|
+
hasSuggestions: true,
|
|
1069
|
+
messages: {
|
|
1070
|
+
bannedProperty: "The `{{ property }}` property is not allowed{{ customMessage }}",
|
|
1071
|
+
removePropertySuggestion: "Remove the property."
|
|
1072
|
+
},
|
|
1073
|
+
schema: [{
|
|
1074
|
+
additionalProperties: false,
|
|
1075
|
+
properties: { ban: {
|
|
1076
|
+
description: "List of top-level properties to ban. Each entry can be a property name string or an object with a property name and an optional custom message.",
|
|
1077
|
+
items: { oneOf: [{ type: "string" }, {
|
|
1078
|
+
additionalProperties: false,
|
|
1079
|
+
properties: {
|
|
1080
|
+
message: {
|
|
1081
|
+
description: "Custom message to append to the error report.",
|
|
1082
|
+
type: "string"
|
|
1083
|
+
},
|
|
1084
|
+
property: {
|
|
1085
|
+
description: "The top-level property name to ban.",
|
|
1086
|
+
type: "string"
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
required: ["property"],
|
|
1090
|
+
type: "object"
|
|
1091
|
+
}] },
|
|
1092
|
+
type: "array"
|
|
1093
|
+
} },
|
|
1094
|
+
type: "object"
|
|
1095
|
+
}],
|
|
1096
|
+
type: "suggestion"
|
|
1097
|
+
},
|
|
1098
|
+
name: "restrict-top-level-properties"
|
|
1099
|
+
});
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region src/rules/scripts-name-casing.ts
|
|
1102
|
+
const BUILT_IN_SCRIPTS_IN_CAMEL_CASE = /* @__PURE__ */ new Set(["prepublishOnly"]);
|
|
1103
|
+
const rule$6 = createRule({
|
|
1104
|
+
create(context) {
|
|
1105
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=scripts]"(node) {
|
|
1106
|
+
if (node.value.type === "JSONObjectExpression") for (const property of node.value.properties) {
|
|
1107
|
+
const keyNode = property.key;
|
|
1108
|
+
const key = keyNode.value;
|
|
1109
|
+
if (BUILT_IN_SCRIPTS_IN_CAMEL_CASE.has(key)) continue;
|
|
1110
|
+
const kebabCaseKey = key.split(":").map((segment) => kebabCase(segment)).join(":");
|
|
1111
|
+
if (kebabCaseKey !== key) context.report({
|
|
1112
|
+
data: { property: key },
|
|
1113
|
+
messageId: "invalidCase",
|
|
1114
|
+
node: keyNode,
|
|
1115
|
+
suggest: [{
|
|
1116
|
+
data: { property: key },
|
|
1117
|
+
fix: (fixer) => {
|
|
1118
|
+
return fixer.replaceText(keyNode, JSON.stringify(kebabCaseKey));
|
|
1119
|
+
},
|
|
1120
|
+
messageId: "convertToKebabCase"
|
|
1121
|
+
}]
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
} };
|
|
1125
|
+
},
|
|
1126
|
+
meta: {
|
|
1127
|
+
docs: {
|
|
1128
|
+
category: "Stylistic",
|
|
1129
|
+
description: "Enforce that names for `scripts` are in kebab case (optionally separated by colons)."
|
|
1130
|
+
},
|
|
1131
|
+
hasSuggestions: true,
|
|
1132
|
+
messages: {
|
|
1133
|
+
convertToKebabCase: "Convert {{ property }} to kebab case.",
|
|
1134
|
+
invalidCase: "Script name {{ property }} should be in kebab case."
|
|
1135
|
+
},
|
|
1136
|
+
schema: [],
|
|
1137
|
+
type: "suggestion"
|
|
1138
|
+
},
|
|
1139
|
+
name: "scripts-name-casing"
|
|
1140
|
+
});
|
|
1141
|
+
//#endregion
|
|
1142
|
+
//#region src/rules/sort-collections.ts
|
|
1143
|
+
const defaultCollections = /* @__PURE__ */ new Set([
|
|
1144
|
+
"config",
|
|
1145
|
+
"dependencies",
|
|
1146
|
+
"devDependencies",
|
|
1147
|
+
"exports",
|
|
1148
|
+
"optionalDependencies",
|
|
1149
|
+
"overrides",
|
|
1150
|
+
"peerDependencies",
|
|
1151
|
+
"peerDependenciesMeta",
|
|
1152
|
+
"scripts"
|
|
1153
|
+
]);
|
|
1154
|
+
const rule$5 = createRule({
|
|
1155
|
+
create(context) {
|
|
1156
|
+
const toSort = new Map((context.options[0] ?? Array.from(defaultCollections)).map((entry) => typeof entry === "string" ? [entry, null] : [entry.key, entry.order]));
|
|
1157
|
+
return { "JSONProperty:exit"(node) {
|
|
1158
|
+
const { key: nodeKey, value: collection } = node;
|
|
1159
|
+
if (nodeKey.type !== "JSONLiteral" || collection.type !== "JSONObjectExpression") return;
|
|
1160
|
+
const keyPartsReversed = [nodeKey.value];
|
|
1161
|
+
for (let currNode = node.parent; currNode; currNode = currNode.parent) if (currNode.type === "JSONProperty" && currNode.key.type === "JSONLiteral") keyPartsReversed.push(currNode.key.value);
|
|
1162
|
+
else if (currNode.type === "JSONArrayExpression") return;
|
|
1163
|
+
const key = keyPartsReversed.reverse().join(".");
|
|
1164
|
+
const customOrder = toSort.get(key);
|
|
1165
|
+
if (customOrder === void 0) return;
|
|
1166
|
+
const currentOrder = collection.properties;
|
|
1167
|
+
const isScripts = keyPartsReversed.at(-1) === "scripts";
|
|
1168
|
+
let naturalCompare;
|
|
1169
|
+
if (isScripts) {
|
|
1170
|
+
const scriptsSource = context.sourceCode.getText(node);
|
|
1171
|
+
const { scripts: sortedScripts } = sortPackageJson(JSON.parse(`{${scriptsSource}}`));
|
|
1172
|
+
const lifecycleIndex = new Map(Object.keys(sortedScripts).map((k, i) => [k, i]));
|
|
1173
|
+
naturalCompare = (a, b) => (lifecycleIndex.get(a) ?? 0) - (lifecycleIndex.get(b) ?? 0);
|
|
1174
|
+
} else naturalCompare = (a, b) => a > b ? 1 : -1;
|
|
1175
|
+
const orderIndex = new Map((customOrder ?? []).map((k, i) => [k, i]));
|
|
1176
|
+
const rank = (k) => orderIndex.get(k) ?? orderIndex.size;
|
|
1177
|
+
const desiredOrder = currentOrder.toSorted((a, b) => {
|
|
1178
|
+
const aKey = a.key.value;
|
|
1179
|
+
const bKey = b.key.value;
|
|
1180
|
+
const ai = rank(aKey);
|
|
1181
|
+
const bi = rank(bKey);
|
|
1182
|
+
if (ai !== bi) return ai - bi;
|
|
1183
|
+
return naturalCompare(aKey, bKey);
|
|
1184
|
+
});
|
|
1185
|
+
if (currentOrder.some((property, i) => desiredOrder[i] !== property)) context.report({
|
|
1186
|
+
data: { key },
|
|
1187
|
+
fix(fixer) {
|
|
1188
|
+
const { text } = context.sourceCode;
|
|
1189
|
+
const { indent, type } = detectIndent(text);
|
|
1190
|
+
const newline = detectNewlineGraceful(text);
|
|
1191
|
+
const indentUnit = type === "tab" ? " " : indent || " ";
|
|
1192
|
+
const jsonLines = JSON.stringify(desiredOrder.reduce((out, property) => {
|
|
1193
|
+
out[property.key.value] = JSON.parse(context.sourceCode.getText(property.value));
|
|
1194
|
+
return out;
|
|
1195
|
+
}, {}), null, indentUnit).split("\n");
|
|
1196
|
+
const collectionStartLine = collection.loc.start.line;
|
|
1197
|
+
const lineText = context.sourceCode.lines[collectionStartLine - 1];
|
|
1198
|
+
const leadingWhitespaceMatch = /^\s*/.exec(lineText);
|
|
1199
|
+
const leadingWhitespace = leadingWhitespaceMatch ? leadingWhitespaceMatch[0] : "";
|
|
1200
|
+
const result = jsonLines.map((l, i) => i === 0 ? l : leadingWhitespace + l).join(newline);
|
|
1201
|
+
return fixer.replaceText(collection, result);
|
|
1202
|
+
},
|
|
1203
|
+
loc: collection.loc,
|
|
1204
|
+
messageId: customOrder ? "unsortedOrder" : isScripts ? "unsortedScripts" : "unsortedKeys",
|
|
1205
|
+
node
|
|
1206
|
+
});
|
|
1207
|
+
} };
|
|
1208
|
+
},
|
|
1209
|
+
meta: {
|
|
1210
|
+
defaultOptions: [Array.from(defaultCollections)],
|
|
1211
|
+
docs: {
|
|
1212
|
+
category: "Best Practices",
|
|
1213
|
+
description: "Selected collections must be in a consistent order (lexicographical for most; lifecycle-aware for scripts).",
|
|
1214
|
+
recommended: true
|
|
1215
|
+
},
|
|
1216
|
+
fixable: "code",
|
|
1217
|
+
messages: {
|
|
1218
|
+
unsortedKeys: "Entries in '{{ key }}' are not in lexicographical order",
|
|
1219
|
+
unsortedOrder: "Entries in '{{ key }}' are not in the specified order",
|
|
1220
|
+
unsortedScripts: "Entries in 'scripts' are not in lexicographical order and grouped by lifecycles"
|
|
1221
|
+
},
|
|
1222
|
+
schema: [{
|
|
1223
|
+
description: "Array of package properties to require sorting. Provide a string to sort that collection lexicographically (lifecycle-aware for `scripts`), or an object to sort it by a specified order.",
|
|
1224
|
+
items: { anyOf: [{ type: "string" }, {
|
|
1225
|
+
additionalProperties: false,
|
|
1226
|
+
properties: {
|
|
1227
|
+
key: {
|
|
1228
|
+
description: "The collection property to sort.",
|
|
1229
|
+
type: "string"
|
|
1230
|
+
},
|
|
1231
|
+
order: {
|
|
1232
|
+
description: "The order to sort the collection by. Keys not listed are appended in lexicographical order.",
|
|
1233
|
+
items: { type: "string" },
|
|
1234
|
+
type: "array",
|
|
1235
|
+
uniqueItems: true
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
required: ["key", "order"],
|
|
1239
|
+
type: "object"
|
|
1240
|
+
}] },
|
|
1241
|
+
type: "array"
|
|
1242
|
+
}],
|
|
1243
|
+
type: "layout"
|
|
1244
|
+
},
|
|
1245
|
+
name: "sort-collections"
|
|
1246
|
+
});
|
|
1247
|
+
//#endregion
|
|
1248
|
+
//#region src/rules/specify-peers-locally.ts
|
|
1249
|
+
const rule$4 = createRule({
|
|
1250
|
+
create(context) {
|
|
1251
|
+
const devDependencyNames = /* @__PURE__ */ new Set();
|
|
1252
|
+
let devDependenciesObjectNode;
|
|
1253
|
+
const peerDependencyMap = {};
|
|
1254
|
+
return {
|
|
1255
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.type=JSONLiteral][value.type=JSONObjectExpression][key.value=devDependencies]"(node) {
|
|
1256
|
+
devDependenciesObjectNode = node.value;
|
|
1257
|
+
for (const devDependencyPropertyNode of node.value.properties) {
|
|
1258
|
+
const dependencyKey = devDependencyPropertyNode.key;
|
|
1259
|
+
if (isJSONStringLiteral(dependencyKey)) devDependencyNames.add(dependencyKey.value);
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.type=JSONLiteral][value.type=JSONObjectExpression][key.value=peerDependencies]"(node) {
|
|
1263
|
+
for (const peerDependencyPropertyNode of node.value.properties) {
|
|
1264
|
+
const dependencyKey = peerDependencyPropertyNode.key;
|
|
1265
|
+
if (isJSONStringLiteral(dependencyKey)) peerDependencyMap[dependencyKey.value] = peerDependencyPropertyNode;
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
"Program:exit"() {
|
|
1269
|
+
for (const [peerDependencyName, peerDependencyNode] of Object.entries(peerDependencyMap)) if (!devDependencyNames.has(peerDependencyName)) {
|
|
1270
|
+
const peerDependencyValue = peerDependencyNode.value;
|
|
1271
|
+
context.report({
|
|
1272
|
+
data: { name: peerDependencyName },
|
|
1273
|
+
messageId: "devDependencyNotDefined",
|
|
1274
|
+
node: peerDependencyNode,
|
|
1275
|
+
suggest: devDependenciesObjectNode && isJSONStringLiteral(peerDependencyValue) ? [{
|
|
1276
|
+
data: { name: peerDependencyName },
|
|
1277
|
+
fix: (fixer) => {
|
|
1278
|
+
const updatedDevDependencies = {
|
|
1279
|
+
...JSON.parse(context.sourceCode.getText(devDependenciesObjectNode)),
|
|
1280
|
+
[peerDependencyName]: peerDependencyValue.value
|
|
1281
|
+
};
|
|
1282
|
+
const sortedDevDependencies = Object.fromEntries(Object.entries(updatedDevDependencies).sort((a, b) => a[0] > b[0] ? 1 : -1));
|
|
1283
|
+
return fixer.replaceText(devDependenciesObjectNode, JSON.stringify(sortedDevDependencies, null, 2).split("\n").join("\n "));
|
|
1284
|
+
},
|
|
1285
|
+
messageId: "addToDevDependencies"
|
|
1286
|
+
}] : []
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
},
|
|
1292
|
+
meta: {
|
|
1293
|
+
docs: {
|
|
1294
|
+
description: "Requires that all peer dependencies are also declared as dev dependencies",
|
|
1295
|
+
recommended: true
|
|
1296
|
+
},
|
|
1297
|
+
hasSuggestions: true,
|
|
1298
|
+
messages: {
|
|
1299
|
+
addToDevDependencies: "Add \"{{ name }}\" to `devDependencies`",
|
|
1300
|
+
devDependencyNotDefined: "Peer dependency \"{{ name }}\" is not also declared in `devDependencies`."
|
|
1301
|
+
},
|
|
1302
|
+
schema: [],
|
|
1303
|
+
type: "problem"
|
|
1304
|
+
},
|
|
1305
|
+
name: "specify-peers-locally"
|
|
1306
|
+
});
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/rules/unique-dependencies.ts
|
|
1309
|
+
const dependencyPropertyNames = /* @__PURE__ */ new Set([
|
|
1310
|
+
"bundledDependencies",
|
|
1311
|
+
"bundleDependencies",
|
|
1312
|
+
"dependencies",
|
|
1313
|
+
"devDependencies",
|
|
1314
|
+
"optionalDependencies",
|
|
1315
|
+
"overrides",
|
|
1316
|
+
"peerDependencies"
|
|
1317
|
+
]);
|
|
1318
|
+
const rule$3 = createRule({
|
|
1319
|
+
create(context) {
|
|
1320
|
+
const dependenciesCache = {
|
|
1321
|
+
dependencies: [],
|
|
1322
|
+
devDependencies: [],
|
|
1323
|
+
peerDependencies: []
|
|
1324
|
+
};
|
|
1325
|
+
const trackForCrossGroupUniqueness = Object.keys(dependenciesCache);
|
|
1326
|
+
function check(elements, getNodeToRemove) {
|
|
1327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1328
|
+
for (const element of elements.filter(isNotNullish).filter(isJSONStringLiteral).reverse()) if (seen.has(element.value)) report(element);
|
|
1329
|
+
else seen.add(element.value);
|
|
1330
|
+
function report(node) {
|
|
1331
|
+
const removal = getNodeToRemove(node);
|
|
1332
|
+
context.report({
|
|
1333
|
+
messageId: "overridden",
|
|
1334
|
+
node,
|
|
1335
|
+
suggest: [{
|
|
1336
|
+
fix: removal.type === "JSONProperty" ? fixRemoveObjectProperty(context, removal) : fixRemoveArrayElement(context, removal, elements),
|
|
1337
|
+
messageId: "remove"
|
|
1338
|
+
}]
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.type=JSONLiteral]"(node) {
|
|
1344
|
+
if (!dependencyPropertyNames.has(node.key.value)) return;
|
|
1345
|
+
const nodeValueType = node.value.type;
|
|
1346
|
+
if (nodeValueType === "JSONArrayExpression") check(node.value.elements, (element) => element);
|
|
1347
|
+
if (nodeValueType === "JSONObjectExpression") {
|
|
1348
|
+
check(node.value.properties.map((property) => property.key), (property) => property.parent);
|
|
1349
|
+
if (trackForCrossGroupUniqueness.includes(node.key.value)) dependenciesCache[node.key.value] = node.value.properties;
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
"Program:exit"() {
|
|
1353
|
+
const dependencyNames = new Set(dependenciesCache.dependencies.map((node) => node.key).filter(isJSONStringLiteral).map((dependencyNameNode) => dependencyNameNode.value));
|
|
1354
|
+
if (!dependencyNames.size) return;
|
|
1355
|
+
for (const dependencyType of ["devDependencies", "peerDependencies"]) {
|
|
1356
|
+
const otherDependencies = dependenciesCache[dependencyType];
|
|
1357
|
+
for (const otherDependencyNode of otherDependencies) {
|
|
1358
|
+
const otherDependencyKey = otherDependencyNode.key;
|
|
1359
|
+
if (isJSONStringLiteral(otherDependencyKey) && dependencyNames.has(otherDependencyKey.value)) context.report({
|
|
1360
|
+
messageId: "crossGroupDuplicate",
|
|
1361
|
+
node: otherDependencyNode,
|
|
1362
|
+
suggest: [{
|
|
1363
|
+
fix: fixRemoveObjectProperty(context, otherDependencyNode),
|
|
1364
|
+
messageId: "remove"
|
|
1365
|
+
}]
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
},
|
|
1372
|
+
meta: {
|
|
1373
|
+
docs: {
|
|
1374
|
+
category: "Best Practices",
|
|
1375
|
+
description: "Checks a dependency isn't specified more than once (i.e. in `dependencies` and `devDependencies`)",
|
|
1376
|
+
recommended: true
|
|
1377
|
+
},
|
|
1378
|
+
hasSuggestions: true,
|
|
1379
|
+
messages: {
|
|
1380
|
+
crossGroupDuplicate: "Dependency is also declared in \"dependencies\" and is redundant",
|
|
1381
|
+
overridden: "Dependency is overridden by a duplicate entry later on",
|
|
1382
|
+
remove: "Remove this redundant dependency"
|
|
1383
|
+
},
|
|
1384
|
+
schema: [],
|
|
1385
|
+
type: "suggestion"
|
|
1386
|
+
},
|
|
1387
|
+
name: "unique-dependencies"
|
|
1388
|
+
});
|
|
1389
|
+
//#endregion
|
|
1390
|
+
//#region src/rules/valid-author.ts
|
|
1391
|
+
const rule$2 = createRule({
|
|
1392
|
+
create(context) {
|
|
1393
|
+
const createFixer = (propertyName, node) => {
|
|
1394
|
+
const author = getGitAuthor();
|
|
1395
|
+
if (!author) return;
|
|
1396
|
+
let mergedAuthor = author;
|
|
1397
|
+
const originalValue = JSON.parse(context.sourceCode.getText(node));
|
|
1398
|
+
if (originalValue && typeof originalValue === "object" && !Array.isArray(originalValue)) mergedAuthor = {
|
|
1399
|
+
name: author.name,
|
|
1400
|
+
...originalValue
|
|
1401
|
+
};
|
|
1402
|
+
switch (propertyName) {
|
|
1403
|
+
case "author": return (fixer) => fixer.replaceText(node, JSON.stringify(mergedAuthor, null, 2).split("\n").join("\n "));
|
|
1404
|
+
case "email": return (fixer) => fixer.replaceText(node, JSON.stringify(author.email));
|
|
1405
|
+
case "name": return (fixer) => fixer.replaceText(node, JSON.stringify(author.name));
|
|
1406
|
+
default: return;
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
const reportIssues = (result, node, propertyName) => {
|
|
1410
|
+
if (result.errorMessages.length === 0) return;
|
|
1411
|
+
if (result.issues.length) for (const issue of result.issues) context.report({
|
|
1412
|
+
data: { error: issue.message },
|
|
1413
|
+
fix: propertyName === void 0 ? void 0 : createFixer(propertyName, node),
|
|
1414
|
+
messageId: "validationError",
|
|
1415
|
+
node
|
|
1416
|
+
});
|
|
1417
|
+
const childrenWithIssues = result.childResults.filter((childResult) => childResult.errorMessages.length);
|
|
1418
|
+
if (node.type === "JSONObjectExpression" && childrenWithIssues.length) for (const childResult of childrenWithIssues) {
|
|
1419
|
+
const childNode = node.properties[childResult.index];
|
|
1420
|
+
const childPropertyName = childNode.key;
|
|
1421
|
+
reportIssues(childResult, childNode.value, isJSONStringLiteral(childPropertyName) ? childPropertyName.value : void 0);
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=author]"(node) {
|
|
1425
|
+
const valueNode = node.value;
|
|
1426
|
+
const result = validateAuthor(JSON.parse(context.sourceCode.getText(valueNode)));
|
|
1427
|
+
reportIssues(result, valueNode, "author");
|
|
1428
|
+
} };
|
|
1429
|
+
},
|
|
1430
|
+
meta: {
|
|
1431
|
+
docs: {
|
|
1432
|
+
category: "Best Practices",
|
|
1433
|
+
description: "Enforce that the `author` property is valid.",
|
|
1434
|
+
recommended: true,
|
|
1435
|
+
ruleGroup: "valid-properties"
|
|
1436
|
+
},
|
|
1437
|
+
fixable: "code",
|
|
1438
|
+
messages: { validationError: `Invalid author: {{ error }}` },
|
|
1439
|
+
schema: [],
|
|
1440
|
+
type: "problem"
|
|
1441
|
+
},
|
|
1442
|
+
name: "valid-author"
|
|
1443
|
+
});
|
|
1444
|
+
//#endregion
|
|
1445
|
+
//#region src/rules/valid-peerDependenciesMeta-relationship.ts
|
|
1446
|
+
const rule$1 = createRule({
|
|
1447
|
+
create(context) {
|
|
1448
|
+
const peerDependencies = /* @__PURE__ */ new Set();
|
|
1449
|
+
const peerDependenciesMeta = /* @__PURE__ */ new Map();
|
|
1450
|
+
return {
|
|
1451
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=peerDependencies]"(node) {
|
|
1452
|
+
if (node.value.type === "JSONObjectExpression") {
|
|
1453
|
+
for (const property of node.value.properties) if (isJSONStringLiteral(property.key)) peerDependencies.add(property.key.value);
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=peerDependenciesMeta]"(node) {
|
|
1457
|
+
if (node.value.type === "JSONObjectExpression") {
|
|
1458
|
+
for (const property of node.value.properties) if (isJSONStringLiteral(property.key)) peerDependenciesMeta.set(property.key.value, property);
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
"Program:exit"() {
|
|
1462
|
+
for (const [dependencyName, propertyNode] of peerDependenciesMeta) if (!peerDependencies.has(dependencyName)) context.report({
|
|
1463
|
+
data: { dependencyName },
|
|
1464
|
+
messageId: "unnecessaryPeerDependency",
|
|
1465
|
+
node: propertyNode,
|
|
1466
|
+
suggest: [{
|
|
1467
|
+
fix: fixRemoveObjectProperty(context, propertyNode),
|
|
1468
|
+
messageId: "removePeerDependencyMeta"
|
|
1469
|
+
}]
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
},
|
|
1474
|
+
meta: {
|
|
1475
|
+
docs: {
|
|
1476
|
+
category: "Best Practices",
|
|
1477
|
+
description: "Enforces that any dependencies declared in `peerDependenciesMeta` are also defined in the package's `peerDependencies`.",
|
|
1478
|
+
recommended: true
|
|
1479
|
+
},
|
|
1480
|
+
hasSuggestions: true,
|
|
1481
|
+
messages: {
|
|
1482
|
+
removePeerDependencyMeta: "Remove from `peerDependenciesMeta`.",
|
|
1483
|
+
unnecessaryPeerDependency: "Dependency '{{ dependencyName }}' is declared in `peerDependenciesMeta` but not in `peerDependencies`."
|
|
1484
|
+
},
|
|
1485
|
+
schema: [],
|
|
1486
|
+
type: "problem"
|
|
1487
|
+
},
|
|
1488
|
+
name: "valid-peerDependenciesMeta-relationship"
|
|
1489
|
+
});
|
|
1490
|
+
//#endregion
|
|
1491
|
+
//#region src/utils/createSimpleValidPropertyRule.ts
|
|
1492
|
+
/**
|
|
1493
|
+
* Given a top-level property name, and a validation function, create a rule that validates the property using the validation function.
|
|
1494
|
+
* These rules will always be included in the recommended config.
|
|
1495
|
+
* Note: this will only create a basic validation rule, with no options. If you need
|
|
1496
|
+
* to create a more complex rule, create it in its own file.
|
|
1497
|
+
*/
|
|
1498
|
+
const createSimpleValidPropertyRule = (propertyName, validationFunction, aliases = []) => {
|
|
1499
|
+
const ruleName = `valid-${propertyName}`;
|
|
1500
|
+
const propertyNames = [propertyName, ...aliases];
|
|
1501
|
+
return {
|
|
1502
|
+
rule: createRule({
|
|
1503
|
+
create(context) {
|
|
1504
|
+
const reportIssues = (result, node) => {
|
|
1505
|
+
if (result.errorMessages.length === 0) return;
|
|
1506
|
+
if (result.issues.length) for (const issue of result.issues) context.report({
|
|
1507
|
+
data: { error: issue.message },
|
|
1508
|
+
messageId: "validationError",
|
|
1509
|
+
node
|
|
1510
|
+
});
|
|
1511
|
+
const childrenWithIssues = result.childResults.filter((childResult) => childResult.errorMessages.length);
|
|
1512
|
+
if (node.type === "JSONObjectExpression" && childrenWithIssues.length) for (const childResult of childrenWithIssues) {
|
|
1513
|
+
const childNode = node.properties[childResult.index];
|
|
1514
|
+
reportIssues(childResult, childNode.value);
|
|
1515
|
+
}
|
|
1516
|
+
else if (node.type === "JSONArrayExpression" && childrenWithIssues.length) for (const childResult of childrenWithIssues) {
|
|
1517
|
+
const childNode = node.elements[childResult.index];
|
|
1518
|
+
if (childNode) reportIssues(childResult, childNode);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
return propertyNames.reduce((acc, name) => {
|
|
1522
|
+
acc[`Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=${name}]`] = (node) => {
|
|
1523
|
+
const valueNode = node.value;
|
|
1524
|
+
const result = validationFunction(JSON.parse(context.sourceCode.getText(valueNode)));
|
|
1525
|
+
reportIssues(result, valueNode);
|
|
1526
|
+
};
|
|
1527
|
+
return acc;
|
|
1528
|
+
}, {});
|
|
1529
|
+
},
|
|
1530
|
+
meta: {
|
|
1531
|
+
docs: {
|
|
1532
|
+
category: "Best Practices",
|
|
1533
|
+
description: `Enforce that the \`${propertyName}\`${aliases.length ? ` (also ${aliases.map((alias) => `\`${alias}\``).join(", ")})` : ""} property is valid.`,
|
|
1534
|
+
recommended: true,
|
|
1535
|
+
ruleGroup: "valid-properties"
|
|
1536
|
+
},
|
|
1537
|
+
messages: { validationError: `Invalid ${propertyName}: {{ error }}` },
|
|
1538
|
+
schema: [],
|
|
1539
|
+
type: "problem"
|
|
1540
|
+
},
|
|
1541
|
+
name: ruleName
|
|
1542
|
+
}),
|
|
1543
|
+
ruleName
|
|
1544
|
+
};
|
|
1545
|
+
};
|
|
1546
|
+
/** All basic valid- flavor rules */
|
|
1547
|
+
const rules$1 = Object.fromEntries([
|
|
1548
|
+
["bin", validateBin],
|
|
1549
|
+
["browser", validateBrowser],
|
|
1550
|
+
["bugs", validateBugs],
|
|
1551
|
+
["bundleDependencies", {
|
|
1552
|
+
aliases: ["bundledDependencies"],
|
|
1553
|
+
validator: validateBundleDependencies
|
|
1554
|
+
}],
|
|
1555
|
+
["config", validateConfig],
|
|
1556
|
+
["contributors", validateContributors],
|
|
1557
|
+
["cpu", validateCpu],
|
|
1558
|
+
["description", validateDescription],
|
|
1559
|
+
["dependencies", validateDependencies],
|
|
1560
|
+
["devDependencies", validateDependencies],
|
|
1561
|
+
["devEngines", validateDevEngines],
|
|
1562
|
+
["directories", validateDirectories],
|
|
1563
|
+
["engines", validateEngines],
|
|
1564
|
+
["exports", validateExports],
|
|
1565
|
+
["files", validateFiles],
|
|
1566
|
+
["funding", validateFunding],
|
|
1567
|
+
["gypfile", validateGypfile],
|
|
1568
|
+
["homepage", validateHomepage],
|
|
1569
|
+
["keywords", validateKeywords],
|
|
1570
|
+
["libc", validateLibc],
|
|
1571
|
+
["license", validateLicense],
|
|
1572
|
+
["main", validateMain],
|
|
1573
|
+
["man", validateMan],
|
|
1574
|
+
["module", validateMain],
|
|
1575
|
+
["name", validateName],
|
|
1576
|
+
["optionalDependencies", validateDependencies],
|
|
1577
|
+
["os", validateOs],
|
|
1578
|
+
["packageManager", validatePackageManager],
|
|
1579
|
+
["peerDependencies", validateDependencies],
|
|
1580
|
+
["peerDependenciesMeta", validatePeerDependenciesMeta],
|
|
1581
|
+
["private", validatePrivate],
|
|
1582
|
+
["publishConfig", validatePublishConfig],
|
|
1583
|
+
["repository", validateRepository],
|
|
1584
|
+
["scripts", validateScripts],
|
|
1585
|
+
["sideEffects", validateSideEffects],
|
|
1586
|
+
["type", validateType],
|
|
1587
|
+
["version", validateVersion],
|
|
1588
|
+
["workspaces", validateWorkspaces]
|
|
1589
|
+
].map(([propertyName, validationFunctionOrOptions]) => {
|
|
1590
|
+
let validationFunction;
|
|
1591
|
+
let aliases = [];
|
|
1592
|
+
if (typeof validationFunctionOrOptions === "object") {
|
|
1593
|
+
validationFunction = validationFunctionOrOptions.validator;
|
|
1594
|
+
aliases = validationFunctionOrOptions.aliases;
|
|
1595
|
+
} else validationFunction = validationFunctionOrOptions;
|
|
1596
|
+
const { rule, ruleName } = createSimpleValidPropertyRule(propertyName, validationFunction, aliases);
|
|
1597
|
+
return [ruleName, rule];
|
|
1598
|
+
}));
|
|
1599
|
+
//#endregion
|
|
1600
|
+
//#region src/rules/valid-repository-directory.ts
|
|
1601
|
+
/**
|
|
1602
|
+
* Checks if the child path appears at the end of the parent path.
|
|
1603
|
+
* @example '/a/b/c', 'c' => true
|
|
1604
|
+
* @example '/a/b/c', 'b/c' => true
|
|
1605
|
+
* @example '/a/b/c', 'b' => false
|
|
1606
|
+
* @example '/a/b/c', 'd' => false
|
|
1607
|
+
*/
|
|
1608
|
+
const pathEndsWith = (parent, child) => {
|
|
1609
|
+
if (parent === child) return true;
|
|
1610
|
+
const segments = parent.split(path.sep);
|
|
1611
|
+
let pathToCheck = "";
|
|
1612
|
+
return segments.reverse().some((segment) => {
|
|
1613
|
+
pathToCheck = path.join(segment, pathToCheck);
|
|
1614
|
+
if (pathToCheck === child) return true;
|
|
1615
|
+
});
|
|
1616
|
+
};
|
|
1617
|
+
let repoRootCache = null;
|
|
1618
|
+
const rule = createRule({
|
|
1619
|
+
create(context) {
|
|
1620
|
+
return { "Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=repository][value.type=JSONObjectExpression]"(node) {
|
|
1621
|
+
const directoryProperty = findPropertyWithKeyValue(node.value.properties, "directory");
|
|
1622
|
+
if (directoryProperty?.value.type !== "JSONLiteral" || typeof directoryProperty.value.value !== "string") return;
|
|
1623
|
+
const directoryValue = directoryProperty.value.value;
|
|
1624
|
+
const fileDirectory = path.normalize(path.dirname(context.filename));
|
|
1625
|
+
let repositoryRoot;
|
|
1626
|
+
if (repoRootCache && fileDirectory.startsWith(repoRootCache)) repositoryRoot = repoRootCache;
|
|
1627
|
+
else {
|
|
1628
|
+
repositoryRoot = findRootSync(fileDirectory);
|
|
1629
|
+
if (repositoryRoot) repoRootCache = path.normalize(repositoryRoot);
|
|
1630
|
+
}
|
|
1631
|
+
if (repositoryRoot) {
|
|
1632
|
+
const expected = path.relative(repositoryRoot, fileDirectory).replaceAll(path.sep, sep);
|
|
1633
|
+
if (expected !== directoryValue) context.report({
|
|
1634
|
+
messageId: "mismatched",
|
|
1635
|
+
node: directoryProperty.value,
|
|
1636
|
+
suggest: [{
|
|
1637
|
+
data: { expected },
|
|
1638
|
+
fix(fixer) {
|
|
1639
|
+
return fixer.replaceText(directoryProperty.value, `"${expected}"`);
|
|
1640
|
+
},
|
|
1641
|
+
messageId: "replace"
|
|
1642
|
+
}]
|
|
1643
|
+
});
|
|
1644
|
+
} else if (!pathEndsWith(fileDirectory, path.normalize(directoryValue))) context.report({
|
|
1645
|
+
messageId: "mismatched",
|
|
1646
|
+
node: directoryProperty.value
|
|
1647
|
+
});
|
|
1648
|
+
} };
|
|
1649
|
+
},
|
|
1650
|
+
meta: {
|
|
1651
|
+
docs: {
|
|
1652
|
+
category: "Best Practices",
|
|
1653
|
+
description: "Enforce that if repository directory is specified, it matches the path to the package.json file",
|
|
1654
|
+
recommended: true
|
|
1655
|
+
},
|
|
1656
|
+
hasSuggestions: true,
|
|
1657
|
+
messages: {
|
|
1658
|
+
mismatched: "Directory does not match package.json directory.",
|
|
1659
|
+
replace: "Replace with '{{ expected }}'."
|
|
1660
|
+
},
|
|
1661
|
+
schema: [],
|
|
1662
|
+
type: "suggestion"
|
|
1663
|
+
},
|
|
1664
|
+
name: "valid-repository-directory"
|
|
1665
|
+
});
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region src/plugin.ts
|
|
1668
|
+
const require = createRequire(import.meta.url);
|
|
1669
|
+
const rules = {
|
|
1670
|
+
"bin-name-casing": rule$18,
|
|
1671
|
+
"exports-subpaths-style": rule$17,
|
|
1672
|
+
"no-empty-fields": rule$16,
|
|
1673
|
+
"no-local-dependencies": rule$15,
|
|
1674
|
+
"no-redundant-files": rule$14,
|
|
1675
|
+
"no-redundant-publishConfig": rule$13,
|
|
1676
|
+
"order-properties": rule$12,
|
|
1677
|
+
"require-attribution": rule$10,
|
|
1678
|
+
...rules$2,
|
|
1679
|
+
"repository-shorthand": rule$11,
|
|
1680
|
+
"restrict-dependency-ranges": rule$9,
|
|
1681
|
+
"restrict-private-properties": rule$8,
|
|
1682
|
+
"restrict-top-level-properties": rule$7,
|
|
1683
|
+
"scripts-name-casing": rule$6,
|
|
1684
|
+
"sort-collections": rule$5,
|
|
1685
|
+
"specify-peers-locally": rule$4,
|
|
1686
|
+
"unique-dependencies": rule$3,
|
|
1687
|
+
"valid-author": rule$2,
|
|
1688
|
+
...rules$1,
|
|
1689
|
+
"valid-peerDependenciesMeta-relationship": rule$1,
|
|
1690
|
+
"valid-repository-directory": rule
|
|
1691
|
+
};
|
|
1692
|
+
const recommendedRules = { ...Object.fromEntries(Object.entries(rules).filter(([, rule]) => rule.meta.docs?.recommended).map(([name]) => ["package-json/" + name, "error"])) };
|
|
1693
|
+
const recommendedPublishableRules = {
|
|
1694
|
+
...recommendedRules,
|
|
1695
|
+
...Object.fromEntries(Object.entries(rules).filter(([, rule]) => rule.meta.docs?.category === "Publishable").map(([name]) => ["package-json/" + name, "error"]))
|
|
1696
|
+
};
|
|
1697
|
+
const stylisticRules = { ...Object.fromEntries(Object.entries(rules).filter(([, rule]) => rule.meta.docs?.category === "Stylistic").map(([name]) => ["package-json/" + name, "error"])) };
|
|
1698
|
+
const { name, version } = require("../package.json");
|
|
1699
|
+
const plugin = {
|
|
1700
|
+
configs: {
|
|
1701
|
+
recommended: {
|
|
1702
|
+
files: ["**/package.json"],
|
|
1703
|
+
languageOptions: { parser: parserJsonc },
|
|
1704
|
+
name: "package-json/recommended",
|
|
1705
|
+
plugins: { get "package-json"() {
|
|
1706
|
+
return plugin;
|
|
1707
|
+
} },
|
|
1708
|
+
rules: recommendedRules
|
|
1709
|
+
},
|
|
1710
|
+
/** @deprecated use the recommended config instead. this config will be removed in a future major release */
|
|
1711
|
+
"recommended-publishable": {
|
|
1712
|
+
files: ["**/package.json"],
|
|
1713
|
+
languageOptions: { parser: parserJsonc },
|
|
1714
|
+
name: "package-json/recommended-publishable",
|
|
1715
|
+
plugins: { get "package-json"() {
|
|
1716
|
+
return plugin;
|
|
1717
|
+
} },
|
|
1718
|
+
rules: recommendedPublishableRules
|
|
1719
|
+
},
|
|
1720
|
+
stylistic: {
|
|
1721
|
+
files: ["**/package.json"],
|
|
1722
|
+
languageOptions: { parser: parserJsonc },
|
|
1723
|
+
name: "package-json/stylistic",
|
|
1724
|
+
plugins: { get "package-json"() {
|
|
1725
|
+
return plugin;
|
|
1726
|
+
} },
|
|
1727
|
+
rules: stylisticRules
|
|
1728
|
+
}
|
|
1729
|
+
},
|
|
1730
|
+
meta: {
|
|
1731
|
+
name,
|
|
1732
|
+
version
|
|
1733
|
+
},
|
|
1734
|
+
rules
|
|
1735
|
+
};
|
|
1736
|
+
//#endregion
|
|
1737
|
+
export { plugin as t };
|