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

Closed
wants to merge 3 commits into from
Closed
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: support flat config files in bin
  • Loading branch information
JoostKersjes committed Apr 12, 2024
commit 38972836ccc6eb1fba9349811fb4ba87b1fcecf6
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.`
)
87 changes: 63 additions & 24 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')`
@@ -72,17 +76,15 @@ export default function createConfig ({
addDependency('eslint')
addDependency('eslint-plugin-vue')

if (configFormat === 'flat') {
addDependency('@eslint/eslintrc')
addDependency('@eslint/js')
} else 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 flatConfigExtends = []
const flatConfigImports = []
const eslintrcConfig = {
root: true,
extends: [
@@ -96,6 +98,20 @@ export default function createConfig ({
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':
eslintrcConfig.extends.push('eslint:recommended')
@@ -107,41 +123,53 @@ export default function createConfig ({
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}`)
}

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

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

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

const flatConfigEntry = {
files: filePatterns
}
deepMerge(flatConfigEntry, additionalConfig)
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')
eslintrcConfig.extends.push('@vue/eslint-config-prettier/skip-formatting')
needsFlatCompat = true
flatConfigExtends.push(`...compat.extends('@vue/eslint-config-prettier/skip-formatting')`)
}

@@ -174,27 +202,38 @@ export default function createConfig ({

// eslint.config.js | .eslintrc.cjs
if (configFormat === 'flat') {
files['eslint.config.js'] += "import path from 'node:path'\n"
files['eslint.config.js'] += "import { fileURLToPath } from 'node:url'\n\n"
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
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\n'
files['eslint.config.js'] += '})\n\n'
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).split('{')
const [, ...keep] = stringifyJS([flatConfigEntry], styleGuide, "flat").split('{')
files['eslint.config.js'] += ` {${keep.join('{')}\n`
} else {
files['.eslintrc.cjs'] += `module.exports = ${stringifyJS(eslintrcConfig, styleGuide)}\n`