From 3017e837f08c8e95bf04d11adaad1198e7148cb4 Mon Sep 17 00:00:00 2001 From: Icy <185012025+ic4l4s9c@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:57:14 +0300 Subject: [PATCH 1/2] feat(valid-scripts): add valid-scripts rule (#839) This change adds a new `valid-scripts` rule, which is also included in the `recommended` config. --- docs/rules/valid-scripts.md | 54 +++++++++++++++++++++ src/plugin.ts | 2 + src/rules/valid-scripts.ts | 69 +++++++++++++++++++++++++++ src/tests/rules/valid-scripts.test.ts | 61 +++++++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 docs/rules/valid-scripts.md create mode 100644 src/rules/valid-scripts.ts create mode 100644 src/tests/rules/valid-scripts.test.ts diff --git a/docs/rules/valid-scripts.md b/docs/rules/valid-scripts.md new file mode 100644 index 00000000..49cb33f9 --- /dev/null +++ b/docs/rules/valid-scripts.md @@ -0,0 +1,54 @@ +# valid-scripts + +💼 This rule is enabled in the ✅ `recommended` config. + + + +This rule applies two validations to each property in `"scripts"` field: + +- It must be a string rather than any other data type +- It should not be empty + +Example of **incorrect** code for this rule: + +```json +{ + "scripts": null +} +``` + +```json +{ + "scripts": {} +} +``` + +```json +{ + "scripts": { + "test": "" + } +} +``` + +Example of **correct** code for this rule: + +```json +{ + "scripts": { + "build": "tsup", + "format": "prettier \"**/*\" --ignore-unknown", + "lint": "eslint . --max-warnings 0", + "lint:eslint-docs": "pnpm update:eslint-docs --check", + "lint:knip": "knip", + "lint:md": "markdownlint \"**/*.md\" \".github/**/*.md\"", + "lint:packages": "pnpm dedupe --check", + "lint:spelling": "cspell \"**\" \".github/**/*\"", + "prepare": "husky", + "should-semantic-release": "should-semantic-release --verbose", + "test": "vitest", + "tsc": "tsc", + "update:eslint-docs": "eslint-doc-generator --rule-doc-title-format name" + } +} +``` diff --git a/src/plugin.ts b/src/plugin.ts index 082027a0..48fdd547 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -13,6 +13,7 @@ import { rule as validLocalDependency } from "./rules/valid-local-dependency.js" import { rule as validName } from "./rules/valid-name.js"; import { rule as validPackageDefinition } from "./rules/valid-package-definition.js"; import { rule as validRepositoryDirectory } from "./rules/valid-repository-directory.js"; +import { rule as validScripts } from "./rules/valid-scripts.js"; import { rule as validVersion } from "./rules/valid-version.js"; const require = createRequire(import.meta.url || __filename); @@ -34,6 +35,7 @@ const rules: Record = { "valid-name": validName, "valid-package-definition": validPackageDefinition, "valid-repository-directory": validRepositoryDirectory, + "valid-scripts": validScripts, "valid-version": validVersion, /** @deprecated use 'valid-package-definition' instead */ diff --git a/src/rules/valid-scripts.ts b/src/rules/valid-scripts.ts new file mode 100644 index 00000000..28999d93 --- /dev/null +++ b/src/rules/valid-scripts.ts @@ -0,0 +1,69 @@ +import type { AST as JsonAST } from "jsonc-eslint-parser"; + +import * as ESTree from "estree"; + +import { createRule } from "../createRule.js"; + +export const rule = createRule({ + create(context) { + return { + "Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=scripts]"( + node: JsonAST.JSONProperty, + ) { + const { value } = node; + if (value.type !== "JSONObjectExpression") { + context.report({ + messageId: "incorrectTypeScriptsField", + node: node.value as unknown as ESTree.Node, + }); + return; + } + if (value.properties.length === 0) { + context.report({ + messageId: "emptyScriptsField", + node: node.value as unknown as ESTree.Node, + }); + return; + } + for (const prop of value.properties) { + const { value } = prop; // key, + if ( + value.type !== "JSONLiteral" || + typeof value.value !== "string" + ) { + context.report({ + messageId: "incorrectTypeScript", + node: node.value as unknown as ESTree.Node, + }); + return; + } else if (value.value.length === 0) { + context.report({ + messageId: "emptyScript", + node: node.value as unknown as ESTree.Node, + }); + return; + } + } + }, + }; + }, + + meta: { + docs: { + category: "Best Practices", + description: "Enforce that package scripts are valid commands", + recommended: true, + }, + messages: { + emptyScript: + 'values contained in "scripts" object should not be empty', + emptyScriptsField: '"scripts" field should not be empty.', + incorrectTypeScript: + 'values contained in "scripts" object should strings', + incorrectTypeScriptsField: + '"scripts" field should contain an object.', + }, + schema: [], + type: "suggestion", + }, +}); diff --git a/src/tests/rules/valid-scripts.test.ts b/src/tests/rules/valid-scripts.test.ts new file mode 100644 index 00000000..316ec3c2 --- /dev/null +++ b/src/tests/rules/valid-scripts.test.ts @@ -0,0 +1,61 @@ +import { rule } from "../../rules/valid-scripts.js"; +import { ruleTester } from "./ruleTester.js"; + +ruleTester.run("valid-scripts", rule, { + invalid: [ + { + code: `{ "scripts": null }`, + errors: [{ messageId: "incorrectTypeScriptsField" }], + }, + { + code: `{ "scripts": 123 }`, + errors: [{ messageId: "incorrectTypeScriptsField" }], + }, + { + code: `{ "scripts": "" }`, + errors: [{ messageId: "incorrectTypeScriptsField" }], + }, + { + code: `{ "scripts": [] }`, + errors: [{ messageId: "incorrectTypeScriptsField" }], + }, + { + code: `{ "scripts": "{}" }`, + errors: [{ messageId: "incorrectTypeScriptsField" }], + }, + { + code: `{ "scripts": {} }`, + errors: [{ messageId: "emptyScriptsField" }], + }, + { + code: `{ "scripts": { + "test": "" +} }`, + errors: [{ messageId: "emptyScript" }], + }, + { + code: `{ "scripts": { "test": null }}`, + errors: [{ messageId: "incorrectTypeScript" }], + }, + { + code: `{ "scripts": { "test": undefined }}`, + errors: [{ messageId: "incorrectTypeScript" }], + }, + { + code: `{ "scripts": { "test": {} }}`, + errors: [{ messageId: "incorrectTypeScript" }], + }, + { + code: `{ "scripts": { "test": 678 }}`, + errors: [{ messageId: "incorrectTypeScript" }], + }, + ], + valid: [ + `{ + "scripts": { + "commitlint": "commitlint --edit", + "prepare": "husky" + } +}`, + ], +}); From fddb7223c28d74fd2d3d43de6944dc86a70e175c Mon Sep 17 00:00:00 2001 From: Icy <185012025+ic4l4s9c@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:35:30 +0300 Subject: [PATCH 2/2] feat(valid-scripts): add valid-scripts to README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a866d80..e95b4b07 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ The default settings don't conflict, and Prettier plugins can quickly fix up ord ❌ Deprecated. | Name                       | Description | 💼 | 🔧 | 💡 | ❌ | -| :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | :- | :- | :- | :- | +| :--------------------------------------------------------------------- |:--------------------------------------------------------------------------------------------------| :- | :- | :- | :- | | [no-empty-fields](docs/rules/no-empty-fields.md) | Reports on unnecessary empty arrays and objects. | ✅ | | 💡 | | | [no-redundant-files](docs/rules/no-redundant-files.md) | Prevents adding unnecessary / redundant files. | | | 💡 | | | [order-properties](docs/rules/order-properties.md) | Package properties must be declared in standard order | ✅ | 🔧 | | | @@ -138,11 +138,13 @@ The default settings don't conflict, and Prettier plugins can quickly fix up ord | [valid-package-def](docs/rules/valid-package-def.md) | Enforce that package.json has all properties required by the npm spec | | | | ❌ | | [valid-package-definition](docs/rules/valid-package-definition.md) | Enforce that package.json has all properties required by the npm spec | ✅ | | | | | [valid-repository-directory](docs/rules/valid-repository-directory.md) | Enforce that if repository directory is specified, it matches the path to the package.json file | ✅ | | 💡 | | +| [valid-scripts](docs/rules/valid-scripts.md) | Enforce that package scripts properties are valid | ✅ | | | | | [valid-version](docs/rules/valid-version.md) | Enforce that package versions are valid semver specifiers | ✅ | | | | +valid semver specifiers These rules only run on `package.json` files; they will ignore all other files being linted. They can lint `package.json` files at project root and in any subfolder of the project, making this plugin great for monorepos.