Skip to content

Commit

Permalink
feat: support flat config files in bin
Browse files Browse the repository at this point in the history
  • Loading branch information
JoostKersjes committed Apr 12, 2024
1 parent 878e5d5 commit 3897283
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 40 deletions.
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.`
)
87 changes: 63 additions & 24 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 @@ -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: [
Expand All @@ -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')
Expand All @@ -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')`)
}

Expand Down Expand Up @@ -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`
Expand Down

0 comments on commit 3897283

Please sign in to comment.