diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintGenerator.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintGenerator.spec.js index 5d23158404..481a9ccf43 100644 --- a/packages/@vue/cli-plugin-eslint/__tests__/eslintGenerator.spec.js +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintGenerator.spec.js @@ -1,4 +1,5 @@ const generateWithPlugin = require('@vue/cli-test-utils/generateWithPlugin') +const create = require('@vue/cli-test-utils/createTestProject') test('base', async () => { const { pkg } = await generateWithPlugin({ @@ -135,3 +136,30 @@ test('lint on commit', async () => { lintOnSave: false }) }) + +test('generate .editorconfig for new projects', async () => { + const { files } = await generateWithPlugin({ + id: 'eslint', + apply: require('../generator'), + options: { + config: 'airbnb' + } + }) + expect(files['.editorconfig']).toBeTruthy() +}) + +test('append to existing .editorconfig', async () => { + const { dir, read, write } = await create('eslint-editorconfig', { + plugins: { + '@vue/cli-plugin-eslint': {} + } + }, null, true) + await write('.editorconfig', 'root = true\n') + + const invoke = require('@vue/cli/lib/invoke') + await invoke(`eslint`, { config: 'airbnb' }, dir) + + const editorconfig = await read('.editorconfig') + expect(editorconfig).toMatch('root = true') + expect(editorconfig).toMatch('[*.{js,jsx,ts,tsx,vue}]') +}) diff --git a/packages/@vue/cli-plugin-eslint/generator.js b/packages/@vue/cli-plugin-eslint/generator/index.js similarity index 75% rename from packages/@vue/cli-plugin-eslint/generator.js rename to packages/@vue/cli-plugin-eslint/generator/index.js index 4001e4446a..ad846d59da 100644 --- a/packages/@vue/cli-plugin-eslint/generator.js +++ b/packages/@vue/cli-plugin-eslint/generator/index.js @@ -1,9 +1,12 @@ +const fs = require('fs') +const path = require('path') + module.exports = (api, { config, lintOn = [] }, _, invoking) => { if (typeof lintOn === 'string') { lintOn = lintOn.split(',') } - const eslintConfig = require('./eslintOptions').config(api) + const eslintConfig = require('../eslintOptions').config(api) const pkg = { scripts: { @@ -20,21 +23,40 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => { } } + const injectEditorConfig = (config) => { + const filePath = api.resolve('.editorconfig') + if (fs.existsSync(filePath)) { + // Append to existing .editorconfig + api.render(files => { + const configPath = path.resolve(__dirname, `./template/${config}/_editorconfig`) + const editorconfig = fs.readFileSync(configPath, 'utf-8') + + files['.editorconfig'] += `\n${editorconfig}` + }) + } else { + api.render(`./template/${config}`) + } + } + if (config === 'airbnb') { eslintConfig.extends.push('@vue/airbnb') Object.assign(pkg.devDependencies, { '@vue/eslint-config-airbnb': '^3.0.5' }) + injectEditorConfig('airbnb') } else if (config === 'standard') { eslintConfig.extends.push('@vue/standard') Object.assign(pkg.devDependencies, { '@vue/eslint-config-standard': '^3.0.5' }) + injectEditorConfig('standard') } else if (config === 'prettier') { eslintConfig.extends.push('@vue/prettier') Object.assign(pkg.devDependencies, { '@vue/eslint-config-prettier': '^3.0.5' }) + // prettier & default config do not have any style rules + // so no need to generate an editorconfig file } else { // default eslintConfig.extends.push('eslint:recommended') @@ -80,7 +102,7 @@ module.exports = (api, { config, lintOn = [] }, _, invoking) => { // lint & fix after create to ensure files adhere to chosen config if (config && config !== 'base') { api.onCreateComplete(() => { - require('./lint')({ silent: true }, api) + require('../lint')({ silent: true }, api) }) } } diff --git a/packages/@vue/cli-plugin-eslint/generator/template/airbnb/_editorconfig b/packages/@vue/cli-plugin-eslint/generator/template/airbnb/_editorconfig new file mode 100644 index 0000000000..c24743d006 --- /dev/null +++ b/packages/@vue/cli-plugin-eslint/generator/template/airbnb/_editorconfig @@ -0,0 +1,7 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 100 diff --git a/packages/@vue/cli-plugin-eslint/generator/template/standard/_editorconfig b/packages/@vue/cli-plugin-eslint/generator/template/standard/_editorconfig new file mode 100644 index 0000000000..7053c49a04 --- /dev/null +++ b/packages/@vue/cli-plugin-eslint/generator/template/standard/_editorconfig @@ -0,0 +1,5 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/packages/@vue/cli-test-utils/createTestProject.js b/packages/@vue/cli-test-utils/createTestProject.js index 8cc1e6fe57..eccca7317b 100644 --- a/packages/@vue/cli-test-utils/createTestProject.js +++ b/packages/@vue/cli-test-utils/createTestProject.js @@ -3,6 +3,8 @@ const path = require('path') const execa = require('execa') module.exports = function createTestProject (name, preset, cwd, initGit) { + delete process.env.VUE_CLI_SKIP_WRITE + cwd = cwd || path.resolve(__dirname, '../../test') const projectRoot = path.resolve(cwd, name) diff --git a/scripts/buildEditorConfig.js b/scripts/buildEditorConfig.js new file mode 100644 index 0000000000..4845cc09fd --- /dev/null +++ b/scripts/buildEditorConfig.js @@ -0,0 +1,124 @@ +// Generate editorconfig templates from built-in eslint configs. + +// Supported rules: +// indent_style +// indent_size +// end_of_line +// trim_trailing_whitespace +// insert_final_newline +// max_line_length + +const fs = require('fs') +const path = require('path') +const CLIEngine = require('eslint').CLIEngine + +// Convert eslint rules to editorconfig rules. +function convertRules (config) { + const result = {} + + const eslintRules = new CLIEngine({ + useEslintrc: false, + baseConfig: { + extends: [require.resolve(`@vue/eslint-config-${config}`)] + } + }).getConfigForFile().rules + + const getRuleOptions = (ruleName, defaultOptions = []) => { + const ruleConfig = eslintRules[ruleName] + + if (!ruleConfig || ruleConfig === 0 || ruleConfig === 'off') { + return + } + + if (Array.isArray(ruleConfig) && (ruleConfig[0] === 0 || ruleConfig[0] === 'off')) { + return + } + + if (Array.isArray(ruleConfig) && ruleConfig.length > 1) { + return ruleConfig.slice(1) + } + + return defaultOptions + } + + // https://eslint.org/docs/rules/indent + const indent = getRuleOptions('indent', [4]) + if (indent) { + result.indent_style = indent[0] === 'tab' ? 'tab' : 'space' + + if (typeof indent[0] === 'number') { + result.indent_size = indent[0] + } + } + + // https://eslint.org/docs/rules/linebreak-style + const linebreakStyle = getRuleOptions('linebreak-style', ['unix']) + if (linebreakStyle) { + result.end_of_line = linebreakStyle[0] === 'unix' ? 'lf' : 'crlf' + } + + // https://eslint.org/docs/rules/no-trailing-spaces + const noTrailingSpaces = getRuleOptions('no-trailing-spaces', [{ skipBlankLines: false, ignoreComments: false }]) + if (noTrailingSpaces) { + if (!noTrailingSpaces[0].skipBlankLines && !noTrailingSpaces[0].ignoreComments) { + result.trim_trailing_whitespace = true + } + } + + // https://eslint.org/docs/rules/eol-last + const eolLast = getRuleOptions('eol-last', ['always']) + if (eolLast) { + result.insert_final_newline = eolLast[0] !== 'never' + } + + // https://eslint.org/docs/rules/max-len + const maxLen = getRuleOptions('max-len', [{ code: 80 }]) + if (maxLen) { + // To simplify the implementation logic, we only read from the `code` option. + + // `max-len` has an undocumented array-style configuration, + // where max code length specified directly as integers + // (used by `eslint-config-airbnb`). + + if (typeof maxLen[0] === 'number') { + result.max_line_length = maxLen[0] + } else { + result.max_line_length = maxLen[0].code + } + } + + return result +} + +exports.buildEditorConfig = function buildEditorConfig () { + console.log('Building EditorConfig files...') + // Get built-in eslint configs + const configList = fs.readdirSync(path.resolve(__dirname, '../packages/@vue/')) + .map(name => { + const matched = /eslint-config-(\w+)/.exec(name) + return matched && matched[1] + }) + .filter(x => x) + + configList.forEach(config => { + let content = '[*.{js,jsx,ts,tsx,vue}]\n' + + const editorconfig = convertRules(config) + + // `eslint-config-prettier` & `eslint-config-typescript` do not have any style rules + if (!Object.keys(editorconfig).length) { + return + } + + for (const [key, value] of Object.entries(editorconfig)) { + content += `${key} = ${value}\n` + } + + const templateDir = path.resolve(__dirname, `../packages/@vue/cli-plugin-eslint/generator/template/${config}`) + if (!fs.existsSync(templateDir)) { + fs.mkdirSync(templateDir) + } + fs.writeFileSync(`${templateDir}/_editorconfig`, content) + }) + console.log('EditorConfig files up-to-date.') +} diff --git a/scripts/release.js b/scripts/release.js index 810e704727..aa0745e149 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -35,6 +35,7 @@ const execa = require('execa') const semver = require('semver') const inquirer = require('inquirer') const { syncDeps } = require('./syncDeps') +const { buildEditorConfig } = require('./buildEditorConfig') const curVersion = require('../lerna.json').version @@ -79,6 +80,9 @@ const release = async () => { skipPrompt: true }) delete process.env.PREFIX + + buildEditorConfig() + await execa('git', ['add', '-A'], { stdio: 'inherit' }) await execa('git', ['commit', '-m', 'chore: pre release sync'], { stdio: 'inherit' }) }