-
-
Notifications
You must be signed in to change notification settings - Fork 24
feat!: support flat config #81
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
Merged
Merged
Changes from 36 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
cc4d8a6
feat!: support flat config
aladdin-add fcde6c3
fix: do not support multi configs
aladdin-add 8ddf82a
fix: helpercontent
aladdin-add 1199d6e
fix: review suggestions
aladdin-add 222e432
fix: rm unused dev deps
aladdin-add 4602713
fix: rm unused dev deps
aladdin-add a46bdf7
fix: add missing @eslint/eslintrc
aladdin-add 893522d
chore: clean fixtures
aladdin-add 595b5b7
fix: styleguide tests
aladdin-add 9fd59b0
feat: allow styleguide to be string (type = flat)
aladdin-add f994131
fix: sourceType: script
aladdin-add ed29ad7
fix: indent
aladdin-add 0cc0c41
fix: use `[].concat()` only for shared configs
aladdin-add d8d81d5
chore: c8 -> v8
aladdin-add 74e83ab
refactor: do not throw in constructor
aladdin-add ec2642f
fix: rm deps debug
aladdin-add 640d0d5
fix: shortname for scoped packages
aladdin-add 9a8affb
chore: put env in .npmrc
aladdin-add 399a0f7
fix: change cwd to the `package.json` located dir
aladdin-add 85a6fb2
fix: review suggestions
aladdin-add 07479e1
fix: generage config for sub-dir
aladdin-add d08ec84
Update lib/config-generator.js
aladdin-add 6fe2d6a
chore: add tests for sub dir
aladdin-add bd4232b
Update lib/config-generator.js
aladdin-add 082044d
Update lib/config-generator.js
aladdin-add bb8e042
fix: module => moduleType
aladdin-add 8354c9f
fix: only apply sourceType to js files
aladdin-add ca75216
fix: problem => problems
aladdin-add 71d5767
fix: eslint bin
aladdin-add 2f61590
fix: update snapshots
aladdin-add 1a6fe9d
chore: refactor export content
aladdin-add 5b83cf0
fix: review suggestions
aladdin-add 36ad1ec
Update README.md
aladdin-add c8011ce
Update lib/config-generator.js
aladdin-add a1eae82
Update README.md
aladdin-add 0c19e32
chore: update snapshots
aladdin-add cdc4430
feat: offical vue supports
aladdin-add 0551e86
fix: revert re-ordering
aladdin-add 0a4c547
fix: allow skipping installing deps
aladdin-add e7da8c7
Update lib/config-generator.js
aladdin-add fce14fd
chore: update snapshots
aladdin-add File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
package-lock=false | ||
package-lock=false | ||
node-options=--loader=esmock |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,39 @@ | ||
#!/usr/bin/env node | ||
|
||
/** | ||
* @fileoverview Main CLI that is run via the eslint command. | ||
* @author Nicholas C. Zakas | ||
* @fileoverview Main CLI that is run via the `npm init @eslint/config` command. | ||
* @author 唯然<[email protected]> | ||
*/ | ||
|
||
/* eslint no-console:off -- CLI */ | ||
import { initializeConfig } from "../lib/init/config-initializer.js"; | ||
initializeConfig(); | ||
import { ConfigGenerator } from "../lib/config-generator.js"; | ||
import { findPackageJson } from "../lib/utils/npm-utils.js"; | ||
import process from "process"; | ||
|
||
|
||
const cwd = process.cwd(); | ||
const packageJsonPath = findPackageJson(cwd); | ||
|
||
if (packageJsonPath === null) { | ||
throw new Error("A package.json file is necessary to initialize ESLint. Run `npm init` to create a package.json file and try again."); | ||
} | ||
|
||
const argv = process.argv; | ||
const sharedConfigIndex = process.argv.indexOf("--config"); | ||
|
||
if (sharedConfigIndex === -1) { | ||
const generator = new ConfigGenerator({ cwd, packageJsonPath }); | ||
|
||
await generator.prompt(); | ||
generator.calc(); | ||
await generator.output(); | ||
} else { | ||
|
||
// passed "--config" | ||
const packageName = argv[sharedConfigIndex + 1]; | ||
const type = argv.includes("--eslintrc") ? "eslintrc" : "flat"; | ||
const answers = { purpose: "style", moduleType: "module", styleguide: { packageName, type } }; | ||
const generator = new ConfigGenerator({ cwd, packageJsonPath, answers }); | ||
|
||
generator.calc(); | ||
await generator.output(); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
/** | ||
* @fileoverview to generate config files. | ||
* @author 唯然<[email protected]> | ||
*/ | ||
import process from "process"; | ||
import path from "path"; | ||
import { spawnSync } from "child_process"; | ||
import { writeFile } from "fs/promises"; | ||
import enquirer from "enquirer"; | ||
import { isPackageTypeModule, installSyncSaveDev, fetchPeerDependencies, findPackageJson } from "./utils/npm-utils.js"; | ||
import { getShorthandName } from "./utils/naming.js"; | ||
import * as log from "./utils/logging.js"; | ||
|
||
// TODO: need to specify the package version - they may export flat configs in the future. | ||
const jsStyleGuides = [ | ||
{ message: "Airbnb: https://github.com/airbnb/javascript", name: "airbnb", value: { packageName: "eslint-config-airbnb", type: "eslintrc" } }, | ||
{ message: "Standard: https://github.com/standard/standard", name: "standard", value: { packageName: "eslint-config-standard", type: "eslintrc" } }, | ||
{ message: "XO: https://github.com/xojs/eslint-config-xo", name: "xo", value: { packageName: "eslint-config-xo", type: "eslintrc" } } | ||
]; | ||
const tsStyleGuides = [ | ||
{ message: "Standard: https://github.com/standard/eslint-config-standard-with-typescript", name: "standard", value: { packageName: "eslint-config-standard-with-typescript", type: "eslintrc" } }, | ||
{ message: "XO: https://github.com/xojs/eslint-config-xo-typescript", name: "xo", value: { packageName: "eslint-config-xo-typescript", type: "eslintrc" } } | ||
]; | ||
|
||
/** | ||
* Class representing a ConfigGenerator. | ||
*/ | ||
export class ConfigGenerator { | ||
|
||
/** | ||
* Create a ConfigGenerator. | ||
* @param {Object} options The options for the ConfigGenerator. | ||
* @param {string} options.cwd The current working directory. | ||
* @param {Object} options.answers The answers provided by the user. | ||
* @returns {ConfigGenerator} The ConfigGenerator instance. | ||
*/ | ||
constructor(options) { | ||
this.cwd = options.cwd; | ||
this.packageJsonPath = options.packageJsonPath || findPackageJson(this.cwd); | ||
this.answers = options.answers || {}; | ||
this.result = { | ||
devDependencies: ["eslint"], | ||
configFilename: "eslint.config.js", | ||
configContent: "" | ||
}; | ||
} | ||
|
||
/** | ||
* Prompt the user for input. | ||
* @returns {void} | ||
*/ | ||
async prompt() { | ||
const questions = [ | ||
{ | ||
type: "select", | ||
name: "purpose", | ||
message: "How would you like to use ESLint?", | ||
initial: 1, | ||
choices: [ | ||
{ message: "To check syntax only", name: "syntax" }, | ||
{ message: "To check syntax and find problems", name: "problems" }, | ||
{ message: "To check syntax, find problems, and enforce code style", name: "style" } | ||
] | ||
}, | ||
{ | ||
type: "select", | ||
name: "moduleType", | ||
message: "What type of modules does your project use?", | ||
initial: 0, | ||
choices: [ | ||
{ message: "JavaScript modules (import/export)", name: "esm" }, | ||
{ message: "CommonJS (require/exports)", name: "commonjs" }, | ||
{ message: "None of these", name: "script" } | ||
] | ||
}, | ||
aladdin-add marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
type: "select", | ||
name: "framework", | ||
message: "Which framework does your project use?", | ||
initial: 0, | ||
choices: [ | ||
{ message: "React", name: "react" }, | ||
{ message: "Vue.js", name: "vue" }, | ||
{ message: "None of these", name: "none" } | ||
] | ||
}, | ||
{ | ||
type: "select", | ||
name: "language", | ||
message: "Does your project use TypeScript?", | ||
choices: [ | ||
{ message: "No", name: "javascript" }, | ||
{ message: "Yes", name: "typescript" } | ||
], | ||
initial: 0 | ||
}, | ||
{ | ||
type: "multiselect", | ||
name: "env", | ||
message: "Where does your code run?", | ||
hint: "(Press <space> to select, <a> to toggle all, <i> to invert selection)", | ||
initial: 0, | ||
choices: [ | ||
{ message: "Browser", name: "browser" }, | ||
{ message: "Node", name: "node" } | ||
] | ||
} | ||
]; | ||
|
||
const answers = await enquirer.prompt(questions); | ||
|
||
Object.assign(this.answers, answers); | ||
|
||
if (answers.purpose === "style") { | ||
const choices = this.answers.language === "javascript" ? jsStyleGuides : tsStyleGuides; | ||
const styleguideAnswer = await enquirer.prompt({ | ||
type: "select", | ||
name: "styleguide", | ||
message: "Which style guide do you want to follow?", | ||
choices, | ||
result: choice => choices.find(it => it.name === choice).value | ||
}); | ||
|
||
Object.assign(this.answers, styleguideAnswer); | ||
} | ||
} | ||
|
||
/** | ||
* Calculate the configuration based on the user's answers. | ||
* @returns {void} | ||
*/ | ||
calc() { | ||
const isESMModule = isPackageTypeModule(this.packageJsonPath); | ||
|
||
this.result.configFilename = isESMModule ? "eslint.config.js" : "eslint.config.mjs"; | ||
|
||
let importContent = ""; | ||
const helperContent = `import path from "path"; | ||
import { fileURLToPath } from "url"; | ||
import { FlatCompat } from "@eslint/eslintrc"; | ||
import pluginJs from "@eslint/js"; | ||
|
||
// mimic CommonJS variables -- not needed if using CommonJS | ||
const __filename = fileURLToPath(import.meta.url); | ||
const __dirname = path.dirname(__filename); | ||
const compat = new FlatCompat({baseDirectory: __dirname, recommendedConfig: pluginJs.configs.recommended}); | ||
`; | ||
let exportContent = ""; | ||
let needCompatHelper = false; | ||
|
||
if (this.answers.moduleType === "commonjs" || this.answers.moduleType === "script") { | ||
exportContent += ` {files: ["**/*.js"], languageOptions: {sourceType: "${this.answers.moduleType}"}},\n`; | ||
} | ||
|
||
if (this.answers.purpose === "syntax") { | ||
|
||
// no need to install any plugin | ||
} else if (this.answers.purpose === "problems") { | ||
this.result.devDependencies.push("@eslint/js"); | ||
importContent += "import pluginJs from \"@eslint/js\";\n"; | ||
exportContent += " pluginJs.configs.recommended,\n"; | ||
} else if (this.answers.purpose === "style") { | ||
const styleguide = typeof this.answers.styleguide === "string" | ||
? { packageName: this.answers.styleguide, type: "flat" } | ||
: this.answers.styleguide; | ||
|
||
this.result.devDependencies.push(styleguide.packageName); | ||
|
||
// install peer dependencies - it's needed for most eslintrc-style shared configs. | ||
const peers = fetchPeerDependencies(styleguide.packageName); | ||
|
||
if (peers !== null) { | ||
this.result.devDependencies.push(...peers); | ||
} | ||
|
||
if (styleguide.type === "flat" || styleguide.type === void 0) { | ||
importContent += `import styleGuide from "${styleguide.packageName}";\n`; | ||
exportContent += " ...[].concat(styleGuide),\n"; | ||
} else if (styleguide.type === "eslintrc") { | ||
needCompatHelper = true; | ||
|
||
const shorthandName = getShorthandName(styleguide.packageName, "eslint-config"); | ||
|
||
exportContent += ` ...compat.extends("${shorthandName}"),\n`; | ||
} | ||
} | ||
|
||
if (this.answers.env?.length > 0) { | ||
this.result.devDependencies.push("globals"); | ||
importContent += "import globals from \"globals\";\n"; | ||
const envContent = { | ||
browser: "globals: globals.browser", | ||
node: "globals: globals.node", | ||
"browser,node": "globals: {...globals.browser, ...globals.node}" | ||
}; | ||
|
||
exportContent += ` {languageOptions: { ${envContent[this.answers.env.join(",")]} }},\n`; | ||
} | ||
|
||
|
||
if (this.answers.language === "typescript") { | ||
this.result.devDependencies.push("typescript-eslint"); | ||
importContent += "import tseslint from \"typescript-eslint\";\n"; | ||
exportContent += " ...tseslint.configs.recommended,\n"; | ||
} | ||
|
||
if (this.answers.framework === "vue") { | ||
|
||
this.result.devDependencies.push("eslint-plugin-vue"); | ||
|
||
// importContent += "import pluginVue from \"eslint-plugin-vue\";\n"; | ||
|
||
// // TODO: there is a wip for flat support - https://github.com/vuejs/eslint-plugin-vue/pull/2319 | ||
// exportContent += " pluginVue.configs[\"flat/essential\"],\n"; | ||
needCompatHelper = true; | ||
exportContent += " ...compat.extends(\"plugin:vue/vue3-essential\").map(config => ({files: [\"**/*.vue\"], ...config})),\n"; | ||
} | ||
|
||
if (this.answers.framework === "react") { | ||
this.result.devDependencies.push("eslint-plugin-react"); | ||
importContent += "import pluginReactConfig from \"eslint-plugin-react/configs/recommended.js\";\n"; | ||
exportContent += " pluginReactConfig,\n"; | ||
} | ||
|
||
if (needCompatHelper) { | ||
this.result.devDependencies.push("@eslint/eslintrc", "@eslint/js"); | ||
} | ||
this.result.configContent = `${importContent} | ||
${needCompatHelper ? helperContent : ""} | ||
export default [\n${exportContent}];`; | ||
} | ||
|
||
/** | ||
* Output the configuration. | ||
* @returns {void} | ||
*/ | ||
async output() { | ||
const packageManager = (await enquirer.prompt({ | ||
type: "select", | ||
name: "packageManager", | ||
message: "Which package manager do you want to use?", | ||
initial: 0, | ||
choices: ["npm", "yarn", "pnpm", "bun"] | ||
})).packageManager; | ||
|
||
installSyncSaveDev(this.result.devDependencies, packageManager); | ||
|
||
const configPath = path.join(this.cwd, this.result.configFilename); | ||
|
||
await writeFile(configPath, this.result.configContent); | ||
aladdin-add marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// import("eslint") won't work in some cases. | ||
// refs: https://github.com/eslint/create-config/issues/8, https://github.com/eslint/create-config/issues/12 | ||
const eslintBin = path.join(this.packageJsonPath, "../node_modules/eslint/bin/eslint.js"); | ||
const result = spawnSync(process.execPath, [eslintBin, "--fix", "--quiet", configPath], { encoding: "utf8" }); | ||
|
||
if (result.error || result.status !== 0) { | ||
log.error("A config file was generated, but the config file itself may not follow your linting rules."); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.