Skip to content

Add options to generate a Flat Config #25

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 57 additions & 16 deletions bin/create-eslint-config.js
Original file line number Diff line number Diff line change
@@ -42,14 +42,19 @@ const indent = inferIndent(rawPkgJson)
const pkg = JSON.parse(rawPkgJson)

// 1. check for existing config files
// `.eslintrc.*`, `eslintConfig` in `package.json`
// `.eslintrc.*`, `eslint.config.*` and `eslintConfig` in `package.json`
// ask if wanna overwrite?

// https://eslint.org/docs/latest/user-guide/configuring/configuration-files#configuration-file-formats
// The experimental `eslint.config.js` isn't supported yet
const eslintConfigFormats = ['js', 'cjs', 'yaml', 'yml', 'json']
for (const fmt of eslintConfigFormats) {
const configFileName = `.eslintrc.${fmt}`
const eslintConfigFormats = [
'.eslintrc.js',
'.eslintrc.cjs',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.json',
'eslint.config.js',
'eslint.config.mjs',
'eslint.config.cjs'
]
for (const configFileName of eslintConfigFormats) {
const fullConfigPath = path.resolve(cwd, configFileName)
if (existsSync(fullConfigPath)) {
const { shouldRemove } = await prompt({
@@ -88,7 +93,39 @@ if (pkg.eslintConfig) {
}
}

// 2. Check Vue
// 2. Config format
let configFormat
try {
const eslintVersion = requireInCwd('eslint/package.json').version
console.info(dim(`Detected ESLint version: ${eslintVersion}`))
const [major, minor] = eslintVersion.split('.')
if (parseInt(major) >= 9) {
configFormat = 'flat'
} else if (parseInt(major) === 8 && parseInt(minor) >= 57) {
throw eslintVersion
} else {
configFormat = 'eslintrc'
}
} catch (e) {
const anwsers = await prompt({
type: 'select',
name: 'configFormat',
message: 'Which configuration file format should be used?',
choices: [
{
name: 'flat',
message: 'eslint.config.js (a.k.a. Flat Config, the new default)'
},
{
name: 'eslintrc',
message: `.eslintrc.cjs (deprecated with ESLint v9.0.0)`
},
]
})
configFormat = anwsers.configFormat
}

// 3. Check Vue
// Not detected? Choose from Vue 2 or 3
// TODO: better support for 2.7 and vue-demi
let vueVersion
@@ -108,7 +145,7 @@ try {
vueVersion = anwsers.vueVersion
}

// 3. Choose a style guide
// 4. Choose a style guide
// - Error Prevention (ESLint Recommended)
// - Standard
// - Airbnb
@@ -132,10 +169,10 @@ const { styleGuide } = await prompt({
]
})

// 4. Check TypeScript
// 4.1 Allow JS?
// 4.2 Allow JS in Vue?
// 4.3 Allow JSX (TSX, if answered no in 4.1) in Vue?
// 5. Check TypeScript
// 5.1 Allow JS?
// 5.2 Allow JS in Vue?
// 5.3 Allow JSX (TSX, if answered no in 5.1) in Vue?
let hasTypeScript = false
const additionalConfig = {}
try {
@@ -200,7 +237,7 @@ if (hasTypeScript && styleGuide !== 'default') {
}
}

// 5. If Airbnb && !TypeScript
// 6. If Airbnb && !TypeScript
// Does your project use any path aliases?
// Show [snippet prompts](https://github.com/enquirer/enquirer#snippet-prompt) for the user to input aliases
if (styleGuide === 'airbnb' && !hasTypeScript) {
@@ -255,7 +292,7 @@ if (styleGuide === 'airbnb' && !hasTypeScript) {
}
}

// 6. Do you need Prettier to format your codebase?
// 7. Do you need Prettier to format your codebase?
const { needsPrettier } = await prompt({
type: 'toggle',
disabled: 'No',
@@ -266,6 +303,8 @@ const { needsPrettier } = await prompt({

const { pkg: pkgToExtend, files } = createConfig({
vueVersion,
configFormat,

styleGuide,
hasTypeScript,
needsPrettier,
@@ -291,6 +330,8 @@ for (const [name, content] of Object.entries(files)) {
writeFileSync(fullPath, content, 'utf-8')
}

const configFilename = configFormat === 'flat' ? 'eslint.config.js' : '.eslintrc.cjs'

// Prompt: Run `npm install` or `yarn` or `pnpm install`
const userAgent = process.env.npm_config_user_agent ?? ''
const packageManager = /pnpm/.test(userAgent) ? 'pnpm' : /yarn/.test(userAgent) ? 'yarn' : 'npm'
@@ -300,7 +341,7 @@ const lintCommand = packageManager === 'npm' ? 'npm run lint' : `${packageManage

console.info(
'\n' +
`${bold(yellow('package.json'))} and ${bold(blue('.eslintrc.cjs'))} have been updated.\n` +
`${bold(yellow('package.json'))} and ${bold(blue(configFilename))} have been updated.\n` +
`Now please run ${bold(green(installCommand))} to re-install the dependencies.\n` +
`Then you can run ${bold(green(lintCommand))} to lint your files.`
)
131 changes: 116 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import versionMap from './versionMap.cjs'
const CREATE_ALIAS_SETTING_PLACEHOLDER = 'CREATE_ALIAS_SETTING_PLACEHOLDER'
export { CREATE_ALIAS_SETTING_PLACEHOLDER }

function stringifyJS (value, styleGuide) {
function stringifyJS (value, styleGuide, configFormat) {
// eslint-disable-next-line no-shadow
const result = stringify(value, (val, indent, stringify, key) => {
if (key === 'CREATE_ALIAS_SETTING_PLACEHOLDER') {
@@ -18,6 +18,10 @@ function stringifyJS (value, styleGuide) {
return stringify(val)
}, 2)

if (configFormat === 'flat') {
return result.replace('CREATE_ALIAS_SETTING_PLACEHOLDER: ', '...createAliasSetting')
}

return result.replace(
'CREATE_ALIAS_SETTING_PLACEHOLDER: ',
`...require('@vue/eslint-config-${styleGuide}/createAliasSetting')`
@@ -52,9 +56,10 @@ export function deepMerge (target, obj) {
// This is also used in `create-vue`
export default function createConfig ({
vueVersion = '3.x', // '2.x' | '3.x' (TODO: 2.7 / vue-demi)
configFormat = 'eslintrc', // eslintrc | flat

styleGuide = 'default', // default | airbnb | typescript
hasTypeScript = false, // js | ts
styleGuide = 'default', // default | airbnb | standard
hasTypeScript = false, // true | false
needsPrettier = false, // true | false

additionalConfig = {}, // e.g. Cypress, createAliasSetting for Airbnb, etc.
@@ -69,13 +74,16 @@ export default function createConfig ({
addDependency('eslint')
addDependency('eslint-plugin-vue')

if (styleGuide !== 'default' || hasTypeScript || needsPrettier) {
addDependency('@rushstack/eslint-patch')
if (
configFormat === "eslintrc" &&
(styleGuide !== "default" || hasTypeScript || needsPrettier)
) {
addDependency("@rushstack/eslint-patch");
}

const language = hasTypeScript ? 'typescript' : 'javascript'

const eslintConfig = {
const eslintrcConfig = {
root: true,
extends: [
vueVersion.startsWith('2')
@@ -85,49 +93,105 @@ export default function createConfig ({
}
const addDependencyAndExtend = (name) => {
addDependency(name)
eslintConfig.extends.push(name)
eslintrcConfig.extends.push(name)
}

let needsFlatCompat = false
const flatConfigExtends = []
const flatConfigImports = []
flatConfigImports.push(`import pluginVue from 'eslint-plugin-vue'`)
flatConfigExtends.push(
vueVersion.startsWith('2')
? `...pluginVue.configs['flat/vue2-essential']`
: `...pluginVue.configs['flat/essential']`
)

if (configFormat === 'flat' && styleGuide === 'default') {
addDependency('@eslint/js')
}

switch (`${styleGuide}-${language}`) {
case 'default-javascript':
eslintConfig.extends.push('eslint:recommended')
eslintrcConfig.extends.push('eslint:recommended')
flatConfigImports.push(`import js from '@eslint/js'`)
flatConfigExtends.push('js.configs.recommended')
break
case 'default-typescript':
eslintConfig.extends.push('eslint:recommended')
eslintrcConfig.extends.push('eslint:recommended')
flatConfigImports.push(`import js from '@eslint/js'`)
flatConfigExtends.push('js.configs.recommended')
addDependencyAndExtend('@vue/eslint-config-typescript')
needsFlatCompat = true
flatConfigExtends.push(`...compat.extends('@vue/eslint-config-typescript')`)
break
case 'airbnb-javascript':
case 'standard-javascript':
addDependencyAndExtend(`@vue/eslint-config-${styleGuide}`)
needsFlatCompat = true
flatConfigExtends.push(`...compat.extends('@vue/eslint-config-${styleGuide}')`)
break
case 'airbnb-typescript':
case 'standard-typescript':
addDependencyAndExtend(`@vue/eslint-config-${styleGuide}-with-typescript`)
needsFlatCompat = true
flatConfigExtends.push(`...compat.extends('@vue/eslint-config-${styleGuide}-with-typescript')`)
break
default:
throw new Error(`unexpected combination of styleGuide and language: ${styleGuide}-${language}`)
}

deepMerge(pkg.devDependencies, additionalDependencies)
deepMerge(eslintConfig, additionalConfig)
deepMerge(eslintrcConfig, additionalConfig)

if (additionalConfig?.extends) {
needsFlatCompat = true
additionalConfig.extends.forEach((pkgName) => {
flatConfigExtends.push(`...compat.extends('${pkgName}')`)
})
}

const flatConfigEntry = {
files: language === 'javascript'
? ['**/*.vue','**/*.js','**/*.jsx','**/*.cjs','**/*.mjs']
: ['**/*.vue','**/*.js','**/*.jsx','**/*.cjs','**/*.mjs','**/*.ts','**/*.tsx','**/*.cts','**/*.mts']
}
if (additionalConfig?.settings?.[CREATE_ALIAS_SETTING_PLACEHOLDER]) {
flatConfigImports.push(
`import createAliasSetting from '@vue/eslint-config-${styleGuide}/createAliasSetting'`
)
flatConfigEntry.settings = {
[CREATE_ALIAS_SETTING_PLACEHOLDER]:
additionalConfig.settings[CREATE_ALIAS_SETTING_PLACEHOLDER]
}
}

if (needsPrettier) {
addDependency('prettier')
addDependency('@vue/eslint-config-prettier')
eslintConfig.extends.push('@vue/eslint-config-prettier/skip-formatting')
eslintrcConfig.extends.push('@vue/eslint-config-prettier/skip-formatting')
needsFlatCompat = true
flatConfigExtends.push(`...compat.extends('@vue/eslint-config-prettier/skip-formatting')`)
}

const configFilename = configFormat === 'flat'
? 'eslint.config.js'
: '.eslintrc.cjs'
const files = {
'.eslintrc.cjs': ''
[configFilename]: ''
}

if (styleGuide === 'default') {
// Both Airbnb & Standard have already set `env: node`
files['.eslintrc.cjs'] += '/* eslint-env node */\n'
if (configFormat === 'eslintrc') {
files['.eslintrc.cjs'] += '/* eslint-env node */\n'
}

// Both Airbnb & Standard have already set `ecmaVersion`
// The default in eslint-plugin-vue is 2020, which doesn't support top-level await
eslintConfig.parserOptions = {
eslintrcConfig.parserOptions = {
ecmaVersion: 'latest'
}
flatConfigEntry.languageOptions = {
ecmaVersion: 'latest'
}
}
@@ -136,7 +200,44 @@ export default function createConfig ({
files['.eslintrc.cjs'] += "require('@rushstack/eslint-patch/modern-module-resolution')\n\n"
}

files['.eslintrc.cjs'] += `module.exports = ${stringifyJS(eslintConfig, styleGuide)}\n`
// eslint.config.js | .eslintrc.cjs
if (configFormat === 'flat') {
if (needsFlatCompat) {
files['eslint.config.js'] += "import path from 'node:path'\n"
files['eslint.config.js'] += "import { fileURLToPath } from 'node:url'\n\n"

addDependency('@eslint/eslintrc')
files['eslint.config.js'] += "import { FlatCompat } from '@eslint/eslintrc'\n"
}

// imports
flatConfigImports.forEach((pkgImport) => {
files['eslint.config.js'] += `${pkgImport}\n`
})
files['eslint.config.js'] += '\n'

// neccesary for compatibility until all packages support flat config
if (needsFlatCompat) {
files['eslint.config.js'] += 'const __filename = fileURLToPath(import.meta.url)\n'
files['eslint.config.js'] += 'const __dirname = path.dirname(__filename)\n'
files['eslint.config.js'] += 'const compat = new FlatCompat({\n'
files['eslint.config.js'] += ' baseDirectory: __dirname'
if (pkg.devDependencies['@vue/eslint-config-typescript']) {
files['eslint.config.js'] += ',\n recommendedConfig: js.configs.recommended'
}
files['eslint.config.js'] += '\n})\n\n'
}

files['eslint.config.js'] += 'export default [\n'
flatConfigExtends.forEach((usage) => {
files['eslint.config.js'] += ` ${usage},\n`
})

const [, ...keep] = stringifyJS([flatConfigEntry], styleGuide, "flat").split('{')
files['eslint.config.js'] += ` {${keep.join('{')}\n`
} else {
files['.eslintrc.cjs'] += `module.exports = ${stringifyJS(eslintrcConfig, styleGuide)}\n`
}

// .editorconfig & .prettierrc.json
if (editorconfigs[styleGuide]) {
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -37,6 +37,8 @@
"kolorist": "^1.8.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.0.2",
"@eslint/js": "^9.0.0",
"@rushstack/eslint-patch": "^1.10.1",
"@vue/eslint-config-airbnb": "^8.0.0",
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
61 changes: 61 additions & 0 deletions pnpm-lock.yaml