Skip to content

Commit 8be6218

Browse files
authored
Refactor create-rule script (#2832)
1 parent 8deb085 commit 8be6218

File tree

8 files changed

+164
-118
lines changed

8 files changed

+164
-118
lines changed

eslint.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const config = [
6363
'func-names': 'off',
6464
'@stylistic/function-paren-newline': 'off',
6565
'@stylistic/curly-newline': 'off',
66+
// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2833
67+
'unicorn/template-indent': ['error', {indent: '\t'}],
6668
},
6769
},
6870
{

eslint.dogfooding.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const config = [
3737
'unicorn/consistent-function-scoping': 'off',
3838
// Annoying
3939
'unicorn/no-keyword-prefix': 'off',
40+
// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2833
41+
'unicorn/template-indent': ['error', {indent: '\t'}],
4042
},
4143
},
4244
{

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,12 @@
9696
"eslint-remote-tester-repositories": "^2.0.2",
9797
"espree": "^10.4.0",
9898
"listr2": "^9.0.5",
99-
"lodash-es": "^4.17.21",
10099
"markdownlint-cli": "^0.45.0",
101100
"nano-spawn": "^2.0.0",
102101
"node-style-text": "^2.1.2",
103102
"npm-package-json-lint": "^9.0.0",
104103
"npm-run-all2": "^8.0.4",
105-
"open-editor": "^5.1.0",
104+
"open-editor": "^6.0.0",
106105
"outdent": "^0.8.0",
107106
"pretty-ms": "^9.3.0",
108107
"typescript": "^5.9.3",

scripts/create-rule.js

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
#!/usr/bin/env node
22
import fs from 'node:fs';
3-
import path from 'node:path';
4-
import {fileURLToPath} from 'node:url';
53
import enquirer from 'enquirer';
6-
import {template} from 'lodash-es';
74
import openEditor from 'open-editor';
85
import spawn from 'nano-spawn';
6+
import styleText from 'node-style-text';
97

10-
const dirname = path.dirname(fileURLToPath(import.meta.url));
11-
const ROOT = path.join(dirname, '..');
8+
const PROJECT_ROOT = new URL('../', import.meta.url);
9+
const TEMPLATE_DIRECTORY = new URL('template/', import.meta.url);
1210

1311
function checkFiles(ruleId) {
1412
const files = [
@@ -20,20 +18,29 @@ function checkFiles(ruleId) {
2018
];
2119

2220
for (const file of files) {
23-
if (fs.existsSync(path.join(ROOT, file))) {
21+
if (fs.existsSync(new URL(file, PROJECT_ROOT))) {
2422
throw new Error(`“${file}” already exists.`);
2523
}
2624
}
2725
}
2826

29-
function renderTemplate({source, target, data}) {
30-
const templateFile = path.join(dirname, `template/${source}`);
31-
const targetFile = path.join(ROOT, target);
32-
const templateContent = fs.readFileSync(templateFile, 'utf8');
27+
async function renderTemplate({source, target}, data) {
28+
const sourceUrl = new URL(source, TEMPLATE_DIRECTORY);
29+
const targetUrl = new URL(target, PROJECT_ROOT);
3330

34-
const compiled = template(templateContent);
35-
const content = compiled(data);
36-
return fs.writeFileSync(targetFile, content);
31+
if (source.endsWith('.template.txt')) {
32+
await fs.promises.copyFile(sourceUrl, targetUrl);
33+
} else if (source.endsWith('.template.js')) {
34+
const {default: render} = await import(new URL(source, TEMPLATE_DIRECTORY));
35+
const content = render(data);
36+
await fs.promises.writeFile(targetUrl, content);
37+
} else {
38+
throw new Error(`Unknown template file '${source}'.`);
39+
}
40+
41+
console.log(`File ${styleText.underline.blue(target)} created.`);
42+
43+
return target;
3744
}
3845

3946
async function getData() {
@@ -101,36 +108,41 @@ const data = await getData();
101108
const {id} = data;
102109

103110
checkFiles(id);
104-
renderTemplate({
105-
source: 'documentation.md.jst',
106-
target: `docs/rules/${id}.md`,
107-
data,
108-
});
109-
renderTemplate({
110-
source: 'rule.js.jst',
111-
target: `rules/${id}.js`,
112-
data,
113-
});
114-
renderTemplate({
115-
source: 'test.js.jst',
116-
target: `test/${id}.js`,
117-
data,
111+
112+
const files = await Promise.all(
113+
[
114+
{
115+
source: 'documentation.md.template.txt',
116+
target: `docs/rules/${id}.md`,
117+
},
118+
{
119+
source: 'rule.js.template.js',
120+
target: `rules/${id}.js`,
121+
},
122+
{
123+
source: 'test.js.template.txt',
124+
target: `test/${id}.js`,
125+
},
126+
].map(template => renderTemplate(template, data)),
127+
);
128+
129+
const shouldOpenFiles = await enquirer.prompt({
130+
type: 'confirm',
131+
message: 'Open files in editor?',
132+
initial: true,
118133
});
119134

120-
const filesToOpen = [
121-
`docs/rules/${id}.md`,
122-
`rules/${id}.js`,
123-
`test/${id}.js`,
124-
];
125-
try {
126-
await openEditor(filesToOpen);
127-
} catch {
128-
// https://github.com/sindresorhus/open-editor/issues/15
135+
if (shouldOpenFiles) {
129136
try {
130-
await spawn('code', [
131-
'--new-window',
132-
'.',
133-
...filesToOpen,
134-
], {cwd: ROOT});
135-
} catch {}
137+
await openEditor(files.map(file => new URL(file, PROJECT_ROOT)));
138+
} catch {
139+
// https://github.com/sindresorhus/open-editor/issues/15
140+
try {
141+
await spawn('code', [
142+
'--new-window',
143+
'.',
144+
...files,
145+
], {cwd: PROJECT_ROOT});
146+
} catch {}
147+
}
136148
}

scripts/template/rule.js.jst

Lines changed: 0 additions & 74 deletions
This file was deleted.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {outdent} from 'outdent';
2+
import indentString from 'indent-string';
3+
4+
const indent = (string, count) => indentString(string, count, {indent: '\t'});
5+
6+
const imports = outdent`
7+
import {} from './ast/index.js';
8+
import {} from './fix/index.js';
9+
import {} from './utils/index.js';
10+
`;
11+
12+
const typeImports = outdent`
13+
/**
14+
@import {TSESTree as ESTree} from '@typescript-eslint/types';
15+
@import * as ESLint from 'eslint';
16+
*/
17+
`;
18+
19+
const createMessages = data =>
20+
data.hasSuggestions
21+
? outdent`
22+
const MESSAGE_ID_ERROR = '${data.id}/error';
23+
const MESSAGE_ID_SUGGESTION = '${data.id}/suggestion';
24+
const messages = {
25+
[MESSAGE_ID_ERROR]: 'Prefer \`{{replacement}}\` over \`{{value}}\`.',
26+
[MESSAGE_ID_SUGGESTION]: 'Replace \`{{value}}\` with \`{{replacement}}\`.',
27+
};
28+
`
29+
: outdent`
30+
const MESSAGE_ID = '${data.id}';
31+
const messages = {
32+
[MESSAGE_ID]: 'Prefer \`{{replacement}}\` over \`{{value}}\`.',
33+
};
34+
`;
35+
36+
const fix = outdent`
37+
/** @param {ESLint.Rule.RuleFixer} fixer */
38+
fix: fixer => fixer.replaceText(node, '\\'🦄\\''),
39+
`;
40+
41+
const suggestion = outdent`
42+
suggest: [
43+
{
44+
messageId: MESSAGE_ID_SUGGESTION,
45+
data: {
46+
value: 'unicorn',
47+
replacement: '🦄',
48+
},
49+
${indent(fix, 2)}
50+
},
51+
],
52+
`;
53+
54+
const createRuleCreateFunction = data => outdent`
55+
/** @param {ESLint.Rule.RuleContext} context */
56+
const create = context => {
57+
context.on('Literal', node => {
58+
if (node.value !== 'unicorn') {
59+
return;
60+
}
61+
62+
return {
63+
node,
64+
messageId: ${data.hasSuggestions ? 'MESSAGE_ID_ERROR' : 'MESSAGE_ID'},
65+
data: {
66+
value: 'unicorn',
67+
replacement: '🦄',
68+
},
69+
${data.fixableType ? indent(fix, 3) : ''}
70+
${data.hasSuggestions ? indent(suggestion, 3) : ''}
71+
};
72+
});
73+
};
74+
`;
75+
76+
const createConfig = data => outdent`
77+
/** @type {ESLint.Rule.RuleModule} */
78+
const config = {
79+
create,
80+
meta: {
81+
type: '${data.type}',
82+
docs: {
83+
description: '${data.description}',
84+
recommended: 'unopinionated',
85+
},
86+
${data.fixableType ? `fixable: '${data.fixableType}',` : ''}
87+
${data.hasSuggestions ? 'hasSuggestions: true,' : ''}
88+
messages,
89+
},
90+
};
91+
`;
92+
93+
function renderRuleTemplate(data) {
94+
return [
95+
imports,
96+
typeImports,
97+
createMessages,
98+
createRuleCreateFunction,
99+
createConfig,
100+
'export default config;',
101+
].map(part => typeof part === 'function' ? part(data) : part).join('\n\n')
102+
+ '\n';
103+
}
104+
105+
export default renderRuleTemplate;
File renamed without changes.

0 commit comments

Comments
 (0)