-
Notifications
You must be signed in to change notification settings - Fork 59
/
Copy pathgenerate-rule.ts
249 lines (208 loc) · 8.25 KB
/
generate-rule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import path from 'path'
import fs from 'fs/promises'
import cp from 'child_process'
import prompts from 'prompts'
import dedent from 'ts-dedent'
const logger = console
// CLI questions
const questions = [
{
type: 'text',
name: 'authorName',
initial: '',
message: 'What is your name? (to be given credit for the rule)',
validate: (name: string) => (name === '' ? "Name can't be empty" : true),
},
{
type: 'text',
name: 'ruleId',
message: dedent(`Time to name your rule! Follow the ESLint rule naming conventions:
- If your rule is disallowing something, prefix it with no- such as no-eval for disallowing eval() and no-debugger for disallowing debugger.
- If your rule is enforcing the inclusion of something, use a short name without a special prefix.
- Use dashes between words.
What is the ID of this new rule?
`),
validate: (rule: string) => (rule === '' ? "Rule can't be empty" : true),
},
{
type: 'text',
name: 'ruleDescription',
message: 'Type a short description of this rule',
validate: (rule: string) => (rule === '' ? "Description can't be empty" : true),
},
{
type: 'confirm',
name: 'isAutoFixable',
message: 'Will this rule contain an autofix?',
initial: true,
},
]
const generateRule = async () => {
logger.log(
'👋 Welcome to the Storybook ESLint rule generator! Please answer a few questions so I can provide everything you need for your new rule.'
)
logger.log()
const { authorName, ruleId, ruleDescription, isAutoFixable } = await prompts(questions, {
onCancel: () => {
logger.log('Process canceled by the user.')
process.exit(0)
},
})
const ruleFile = path.resolve(__dirname, `../lib/rules/${ruleId}.ts`)
const testFile = path.resolve(__dirname, `../tests/lib/rules/${ruleId}.test.ts`)
const docFile = path.resolve(__dirname, `../docs/rules/${ruleId}.md`)
logger.log(`creating lib/rules/${ruleId}.ts`)
await fs.writeFile(
ruleFile,
dedent(`/**
* @fileoverview ${ruleDescription}
* @author ${authorName}
*/
import { TSESTree } from '@typescript-eslint/utils'
import { createStorybookRule } from '../utils/create-storybook-rule'
import { CategoryId } from '../utils/constants'
import { isIdentifier, isVariableDeclaration } from '../utils/ast'
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
export = createStorybookRule({
name: '${ruleId}',
defaultOptions: [],
meta: {
type: 'problem', // \`problem\`, \`suggestion\`, or \`layout\`
severity: 'error', // or 'warn'
docs: {
description: '${ruleDescription}',
// Add the categories that suit this rule.
categories: [CategoryId.RECOMMENDED],
},
messages: {
anyMessageIdHere: 'Fill me in',
},
${isAutoFixable ? "fixable: 'code'," : ''}
${isAutoFixable ? 'hasSuggestions: true,' : ''}
schema: [], // Add a schema if the rule has options. Otherwise remove this
},
create(context) {
// variables should be defined here
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
// any helper functions should go here or else delete this section
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
/**
* 👉 Please read this and then delete this entire comment block.
* This is an example rule that reports an error in case a named export is called 'wrong'.
* Hopefully this will guide you to write your own rules. Make sure to always use the AST utilities and account for all possible cases.
*
* Keep in mind that sometimes AST nodes change when in javascript or typescript format. For example, the type of "declaration" from "export default {}" is ObjectExpression but in "export default {} as SomeType" is TSAsExpression.
*
* Use https://eslint.org/docs/developer-guide/working-with-rules for Eslint API reference
* And check https://astexplorer.net/ to help write rules
* Working with AST is fun. Good luck!
*/
ExportNamedDeclaration: function (node: TSESTree.ExportNamedDeclaration) {
const declaration = node.declaration
if (!declaration) return
// use AST helpers to make sure the nodes are of the right type
if (isVariableDeclaration(declaration)) {
const identifier = declaration.declarations[0]?.id
if (isIdentifier(identifier)) {
const { name } = identifier
if (name === 'wrong') {
context.report({
node,
messageId: 'anyMessageIdHere',
})
}
}
}
},
}
},
})
`)
)
logger.log(`creating tests/lib/rules/${ruleId}.test.ts`)
await fs.writeFile(
testFile,
dedent(`/**
* @fileoverview ${ruleDescription}
* @author ${authorName}
*/
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
import rule from '../../../lib/rules/${ruleId}'
import ruleTester from '../../utils/rule-tester'
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
ruleTester.run('${ruleId}', rule, {
/**
* 👉 Please read this and delete this entire comment block.
* This is an example test for a rule that reports an error in case a named export is called 'wrong'
* Use https://eslint.org/docs/developer-guide/working-with-rules for Eslint API reference
*/
valid: ['export const correct = {}'],
invalid: [
{
code: 'export const wrong = {}',
errors: [
{
messageId: 'anyMessageIdHere', // comes from the rule file
},
],
},
],
})
`)
)
logger.log(`creating docs/rules/${ruleId}.md`)
await fs.writeFile(
docFile,
dedent(`
# ${ruleId}
<!-- RULE-CATEGORIES:START -->
<!-- RULE-CATEGORIES:END -->
## Rule Details
${ruleDescription}.
Examples of **incorrect** code for this rule:
\`\`\`js
// fill me in
\`\`\`
Examples of **correct** code for this rule:
\`\`\`js
// fill me in
\`\`\`
### Options
If there are any options, describe them here. Otherwise, delete this section.
## When Not To Use It
Give a short description of when it would be appropriate to turn off this rule. If not applicable, delete this section.
## Further Reading
If there are other links that describe the issue this rule addresses, please include them here in a bulleted list. Otherwise, delete this section.
`)
)
const { shouldOpenInVSCode } = await prompts({
type: 'confirm',
name: 'shouldOpenInVSCode',
message: 'Do you want to open the newly generated files in VS Code?',
initial: false,
})
if (shouldOpenInVSCode) {
cp.execSync(`code "${ruleFile}"`)
cp.execSync(`code "${testFile}"`)
cp.execSync(`code "${docFile}"`)
}
logger.log(
'\n🚀 All done! Make sure to run `pnpm run test` as you write the rule and `pnpm run update-all` when you are done.'
)
logger.log(`❤️ Thanks for helping this plugin get better, ${authorName.split(' ')[0]}!`)
}
generateRule().catch((error) => {
logger.error('An error occurred while generating the rule:', error)
process.exit(1)
})