-
Notifications
You must be signed in to change notification settings - Fork 28
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: | ||
|
||
- 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" | ||
} | ||
} | ||
``` |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Question] Why E.g. here I'd expect the values of both "scripts": {
"script1": "",
"script2": "...",
"script3": "",
} (here and the other |
||
} 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
}, | ||
}); |
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: [ | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Testing] Some more invalid cases to think of:
|
||||||||||
{ | ||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Testing] No need,
Suggested change
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: [ | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Testing] Some more valid cases to think of:
|
||||||||||
`{ | ||||||||||
"scripts": { | ||||||||||
"commitlint": "commitlint --edit", | ||||||||||
"prepare": "husky" | ||||||||||
} | ||||||||||
}`, | ||||||||||
], | ||||||||||
}); |
There was a problem hiding this comment.
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?