Skip to content

Commit 686c642

Browse files
committed
feat: generate .editorconfig file for each specific eslint config
closes #905
1 parent 83c5efa commit 686c642

File tree

6 files changed

+192
-2
lines changed

6 files changed

+192
-2
lines changed

packages/@vue/cli-plugin-eslint/__tests__/eslintGenerator.spec.js

+28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const generateWithPlugin = require('@vue/cli-test-utils/generateWithPlugin')
2+
const create = require('@vue/cli-test-utils/createTestProject')
23

34
test('base', async () => {
45
const { pkg } = await generateWithPlugin({
@@ -135,3 +136,30 @@ test('lint on commit', async () => {
135136
lintOnSave: false
136137
})
137138
})
139+
140+
test('generate .editorconfig for new projects', async () => {
141+
const { files } = await generateWithPlugin({
142+
id: 'eslint',
143+
apply: require('../generator'),
144+
options: {
145+
config: 'airbnb'
146+
}
147+
})
148+
expect(files['.editorconfig']).toBeTruthy()
149+
})
150+
151+
test('append to existing .editorconfig', async () => {
152+
const { dir, read, write } = await create('eslint-editorconfig', {
153+
plugins: {
154+
'@vue/cli-plugin-eslint': {}
155+
}
156+
}, null, true)
157+
await write('.editorconfig', 'root = true\n')
158+
159+
const invoke = require('@vue/cli/lib/invoke')
160+
await invoke(`eslint`, { config: 'airbnb' }, dir)
161+
162+
const editorconfig = await read('.editorconfig')
163+
expect(editorconfig).toMatch('root = true')
164+
expect(editorconfig).toMatch('[*.{js,jsx,ts,tsx,vue}]')
165+
})

packages/@vue/cli-plugin-eslint/generator.js renamed to packages/@vue/cli-plugin-eslint/generator/index.js

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
14
module.exports = (api, { config, lintOn = [] }, _, invoking) => {
25
if (typeof lintOn === 'string') {
36
lintOn = lintOn.split(',')
47
}
58

6-
const eslintConfig = require('./eslintOptions').config(api)
9+
const eslintConfig = require('../eslintOptions').config(api)
710

811
const pkg = {
912
scripts: {
@@ -20,21 +23,40 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => {
2023
}
2124
}
2225

26+
const injectEditorConfig = (config) => {
27+
const filePath = api.resolve('.editorconfig')
28+
if (fs.existsSync(filePath)) {
29+
// Append to existing .editorconfig
30+
api.render(files => {
31+
const configPath = path.resolve(__dirname, `./template/${config}/_editorconfig`)
32+
const editorconfig = fs.readFileSync(configPath, 'utf-8')
33+
34+
files['.editorconfig'] += `\n${editorconfig}`
35+
})
36+
} else {
37+
api.render(`./template/${config}`)
38+
}
39+
}
40+
2341
if (config === 'airbnb') {
2442
eslintConfig.extends.push('@vue/airbnb')
2543
Object.assign(pkg.devDependencies, {
2644
'@vue/eslint-config-airbnb': '^3.0.5'
2745
})
46+
injectEditorConfig('airbnb')
2847
} else if (config === 'standard') {
2948
eslintConfig.extends.push('@vue/standard')
3049
Object.assign(pkg.devDependencies, {
3150
'@vue/eslint-config-standard': '^3.0.5'
3251
})
52+
injectEditorConfig('standard')
3353
} else if (config === 'prettier') {
3454
eslintConfig.extends.push('@vue/prettier')
3555
Object.assign(pkg.devDependencies, {
3656
'@vue/eslint-config-prettier': '^3.0.5'
3757
})
58+
// prettier & default config do not have any style rules
59+
// so no need to generate an editorconfig file
3860
} else {
3961
// default
4062
eslintConfig.extends.push('eslint:recommended')
@@ -80,7 +102,7 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => {
80102
// lint & fix after create to ensure files adhere to chosen config
81103
if (config && config !== 'base') {
82104
api.onCreateComplete(() => {
83-
require('./lint')({ silent: true }, api)
105+
require('../lint')({ silent: true }, api)
84106
})
85107
}
86108
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[*.{js,jsx,ts,tsx,vue}]
2+
indent_style = space
3+
indent_size = 2
4+
end_of_line = lf
5+
trim_trailing_whitespace = true
6+
insert_final_newline = true
7+
max_line_length = 100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[*.{js,jsx,ts,tsx,vue}]
2+
indent_style = space
3+
indent_size = 2
4+
trim_trailing_whitespace = true
5+
insert_final_newline = true

scripts/buildEditorConfig.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Generate editorconfig templates from built-in eslint configs.
2+
3+
// Supported rules:
4+
// indent_style
5+
// indent_size
6+
// end_of_line
7+
// trim_trailing_whitespace
8+
// insert_final_newline
9+
// max_line_length
10+
11+
const fs = require('fs')
12+
const path = require('path')
13+
const CLIEngine = require('eslint').CLIEngine
14+
15+
// Convert eslint rules to editorconfig rules.
16+
function convertRules (config) {
17+
const result = {}
18+
19+
const eslintRules = new CLIEngine({
20+
useEslintrc: false,
21+
baseConfig: {
22+
extends: [require.resolve(`@vue/eslint-config-${config}`)]
23+
}
24+
}).getConfigForFile().rules
25+
26+
const getRuleOptions = (ruleName, defaultOptions = []) => {
27+
const ruleConfig = eslintRules[ruleName]
28+
29+
if (!ruleConfig || ruleConfig === 0 || ruleConfig === 'off') {
30+
return
31+
}
32+
33+
if (Array.isArray(ruleConfig) && (ruleConfig[0] === 0 || ruleConfig[0] === 'off')) {
34+
return
35+
}
36+
37+
if (Array.isArray(ruleConfig) && ruleConfig.length > 1) {
38+
return ruleConfig.slice(1)
39+
}
40+
41+
return defaultOptions
42+
}
43+
44+
// https://eslint.org/docs/rules/indent
45+
const indent = getRuleOptions('indent', [4])
46+
if (indent) {
47+
result.indent_style = indent[0] === 'tab' ? 'tab' : 'space'
48+
49+
if (typeof indent[0] === 'number') {
50+
result.indent_size = indent[0]
51+
}
52+
}
53+
54+
// https://eslint.org/docs/rules/linebreak-style
55+
const linebreakStyle = getRuleOptions('linebreak-style', ['unix'])
56+
if (linebreakStyle) {
57+
result.end_of_line = linebreakStyle[0] === 'unix' ? 'lf' : 'crlf'
58+
}
59+
60+
// https://eslint.org/docs/rules/no-trailing-spaces
61+
const noTrailingSpaces = getRuleOptions('no-trailing-spaces', [{ skipBlankLines: false, ignoreComments: false }])
62+
if (noTrailingSpaces) {
63+
if (!noTrailingSpaces[0].skipBlankLines && !noTrailingSpaces[0].ignoreComments) {
64+
result.trim_trailing_whitespace = true
65+
}
66+
}
67+
68+
// https://eslint.org/docs/rules/eol-last
69+
const eolLast = getRuleOptions('eol-last', ['always'])
70+
if (eolLast) {
71+
result.insert_final_newline = eolLast[0] !== 'never'
72+
}
73+
74+
// https://eslint.org/docs/rules/max-len
75+
const maxLen = getRuleOptions('max-len', [{ code: 80 }])
76+
if (maxLen) {
77+
// To simplify the implementation logic, we only read from the `code` option.
78+
79+
// `max-len` has an undocumented array-style configuration,
80+
// where max code length specified directly as integers
81+
// (used by `eslint-config-airbnb`).
82+
83+
if (typeof maxLen[0] === 'number') {
84+
result.max_line_length = maxLen[0]
85+
} else {
86+
result.max_line_length = maxLen[0].code
87+
}
88+
}
89+
90+
return result
91+
}
92+
93+
exports.buildEditorConfig = function buildEditorConfig () {
94+
console.log('Building EditorConfig files...')
95+
// Get built-in eslint configs
96+
const configList = fs.readdirSync(path.resolve(__dirname, '../packages/@vue/'))
97+
.map(name => {
98+
const matched = /eslint-config-(\w+)/.exec(name)
99+
return matched && matched[1]
100+
})
101+
.filter(x => x)
102+
103+
configList.forEach(config => {
104+
let content = '[*.{js,jsx,ts,tsx,vue}]\n'
105+
106+
const editorconfig = convertRules(config)
107+
108+
// `eslint-config-prettier` & `eslint-config-typescript` do not have any style rules
109+
if (!Object.keys(editorconfig).length) {
110+
return
111+
}
112+
113+
for (const [key, value] of Object.entries(editorconfig)) {
114+
content += `${key} = ${value}\n`
115+
}
116+
117+
const templateDir = path.resolve(__dirname, `../packages/@vue/cli-plugin-eslint/generator/template/${config}`)
118+
if (!fs.existsSync(templateDir)) {
119+
fs.mkdirSync(templateDir)
120+
}
121+
fs.writeFileSync(`${templateDir}/_editorconfig`, content)
122+
})
123+
console.log('EditorConfig files up-to-date.')
124+
}

scripts/release.js

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const execa = require('execa')
3535
const semver = require('semver')
3636
const inquirer = require('inquirer')
3737
const { syncDeps } = require('./syncDeps')
38+
const { buildEditorConfig } = require('./buildEditorConfig')
3839

3940
const curVersion = require('../lerna.json').version
4041

@@ -79,6 +80,9 @@ const release = async () => {
7980
skipPrompt: true
8081
})
8182
delete process.env.PREFIX
83+
84+
buildEditorConfig()
85+
8286
await execa('git', ['add', '-A'], { stdio: 'inherit' })
8387
await execa('git', ['commit', '-m', 'chore: pre release sync'], { stdio: 'inherit' })
8488
}

0 commit comments

Comments
 (0)