Skip to content

feat(valid-scripts): add valid-scripts rule (#839) #903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ✅ | 🔧 | | |
Expand All @@ -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 | ✅ | | | |

<!-- end auto-generated rules list -->
<!-- prettier-ignore-end -->

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.

Expand Down
54 changes: 54 additions & 0 deletions docs/rules/valid-scripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# valid-scripts

💼 This rule is enabled in the ✅ `recommended` config.

<!-- end auto-generated rule header -->

This rule applies two validations to each property in `"scripts"` field:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Docs] Just nitpicking a little bit for clarity, WDYT?

Suggested change
This rule applies two validations to each property in `"scripts"` field:
This rule checks that if a `"scripts"` property exists, it is a non-empty object.
Additionally, for each of its script properties:


- 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"
}
}
```
2 changes: 2 additions & 0 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -34,6 +35,7 @@ const rules: Record<string, PackageJsonRuleModule> = {
"valid-name": validName,
"valid-package-definition": validPackageDefinition,
"valid-repository-directory": validRepositoryDirectory,
"valid-scripts": validScripts,
"valid-version": validVersion,

/** @deprecated use 'valid-package-definition' instead */
Expand Down
69 changes: 69 additions & 0 deletions src/rules/valid-scripts.ts
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Feature] Fun fact, it is technically valid JSON to have duplicate keys:

"scripts": {
  "this-is": "fine",
  "this-is": "fine"
}

cc @michaelfaith to check me here, but IMO this should be reported on by valid-scripts.

Original file line number Diff line number Diff line change
@@ -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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Question] Why return;? If multiple scripts are incorrect, we'd want to get reports on all of them I think.

E.g. here I'd expect the values of both script1 and script3 to have complaints:

"scripts": {
    "script1": "",
    "script2": "...",
    "script3": "",
}

(here and the other 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:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Praise] Haha, "TypeScript". I thought this was cute. 🙂

'values contained in "scripts" object should strings',
incorrectTypeScriptsField:
'"scripts" field should contain an object.',
},
schema: [],
type: "suggestion",
},
});
61 changes: 61 additions & 0 deletions src/tests/rules/valid-scripts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { rule } from "../../rules/valid-scripts.js";
import { ruleTester } from "./ruleTester.js";

ruleTester.run("valid-scripts", rule, {
invalid: [
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Testing] Some more invalid cases to think of:

  • 1 incorrect value + 2 correct values
  • 2 incorrect values
  • duplicate values
  • 2 incorrect values + 1 correct value

{
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" }],
},
Comment on lines +40 to +43
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Testing] No need, undefined isn't valid JSON.

Suggested change
{
code: `{ "scripts": { "test": undefined }}`,
errors: [{ messageId: "incorrectTypeScript" }],
},

I'm actually surprised this doesn't cause any "hey you have invalid stuff" complaint in the tester...

{
code: `{ "scripts": { "test": {} }}`,
errors: [{ messageId: "incorrectTypeScript" }],
},
{
code: `{ "scripts": { "test": 678 }}`,
errors: [{ messageId: "incorrectTypeScript" }],
},
],
valid: [
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Testing] Some more valid cases to think of:

  • No "scripts" key at all
  • Empty "scripts"
  • Just one value in "scripts"

`{
"scripts": {
"commitlint": "commitlint --edit",
"prepare": "husky"
}
}`,
],
});
Loading