Skip to content
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

Add options to generate a Flat Config #25

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand All @@ -108,7 +145,7 @@ try {
vueVersion = anwsers.vueVersion
}

// 3. Choose a style guide
// 4. Choose a style guide
// - Error Prevention (ESLint Recommended)
// - Standard
// - Airbnb
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
Expand All @@ -266,6 +303,8 @@ const { needsPrettier } = await prompt({

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

styleGuide,
hasTypeScript,
needsPrettier,
Expand All @@ -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'
Expand All @@ -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
Expand Up @@ -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') {
Expand All @@ -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')`
Expand Down Expand Up @@ -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.
Expand All @@ -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')
Expand All @@ -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'
}
}
Expand All @@ -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]) {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading