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

feat!: support flat config #81

Merged
merged 41 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
cc4d8a6
feat!: support flat config
aladdin-add Jan 10, 2024
fcde6c3
fix: do not support multi configs
aladdin-add Feb 28, 2024
8ddf82a
fix: helpercontent
aladdin-add Feb 28, 2024
1199d6e
fix: review suggestions
aladdin-add Feb 28, 2024
222e432
fix: rm unused dev deps
aladdin-add Feb 28, 2024
4602713
fix: rm unused dev deps
aladdin-add Feb 29, 2024
a46bdf7
fix: add missing @eslint/eslintrc
aladdin-add Feb 29, 2024
893522d
chore: clean fixtures
aladdin-add Feb 29, 2024
595b5b7
fix: styleguide tests
aladdin-add Feb 29, 2024
9fd59b0
feat: allow styleguide to be string (type = flat)
aladdin-add Feb 29, 2024
f994131
fix: sourceType: script
aladdin-add Feb 29, 2024
ed29ad7
fix: indent
aladdin-add Feb 29, 2024
0cc0c41
fix: use `[].concat()` only for shared configs
aladdin-add Mar 1, 2024
d8d81d5
chore: c8 -> v8
aladdin-add Mar 1, 2024
74e83ab
refactor: do not throw in constructor
aladdin-add Mar 5, 2024
ec2642f
fix: rm deps debug
aladdin-add Mar 7, 2024
640d0d5
fix: shortname for scoped packages
aladdin-add Mar 7, 2024
9a8affb
chore: put env in .npmrc
aladdin-add Mar 12, 2024
399a0f7
fix: change cwd to the `package.json` located dir
aladdin-add Mar 12, 2024
85a6fb2
fix: review suggestions
aladdin-add Mar 13, 2024
07479e1
fix: generage config for sub-dir
aladdin-add Mar 14, 2024
d08ec84
Update lib/config-generator.js
aladdin-add Mar 14, 2024
6fe2d6a
chore: add tests for sub dir
aladdin-add Mar 15, 2024
bd4232b
Update lib/config-generator.js
aladdin-add Mar 18, 2024
082044d
Update lib/config-generator.js
aladdin-add Mar 18, 2024
bb8e042
fix: module => moduleType
aladdin-add Mar 19, 2024
8354c9f
fix: only apply sourceType to js files
aladdin-add Mar 19, 2024
ca75216
fix: problem => problems
aladdin-add Mar 20, 2024
71d5767
fix: eslint bin
aladdin-add Mar 20, 2024
2f61590
fix: update snapshots
aladdin-add Mar 20, 2024
1a6fe9d
chore: refactor export content
aladdin-add Mar 21, 2024
5b83cf0
fix: review suggestions
aladdin-add Mar 22, 2024
36ad1ec
Update README.md
aladdin-add Mar 22, 2024
c8011ce
Update lib/config-generator.js
aladdin-add Mar 24, 2024
a1eae82
Update README.md
aladdin-add Mar 25, 2024
0c19e32
chore: update snapshots
aladdin-add Mar 25, 2024
cdc4430
feat: offical vue supports
aladdin-add Mar 28, 2024
0551e86
fix: revert re-ordering
aladdin-add Mar 29, 2024
0a4c547
fix: allow skipping installing deps
aladdin-add Apr 3, 2024
e7da8c7
Update lib/config-generator.js
aladdin-add Apr 4, 2024
fce14fd
chore: update snapshots
aladdin-add Apr 4, 2024
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
3 changes: 2 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
package-lock=false
package-lock=false
node-options=--loader=esmock
18 changes: 4 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,12 @@ npx @eslint/create-config
If you want to use a specific shareable config that is hosted on npm, you can use the `--config` option and specify the package name:

```bash
# use `eslint-config-semistandard` shared config

# npm 7+
npm init @eslint/config -- --config semistandard

# or (`eslint-config` prefix is optional)
npm init @eslint/config -- --config eslint-config-semistandard

# ⚠️ npm 6.x no extra double-dash:
npm init @eslint/config --config semistandard
# use `eslint-config-standard` shared config
npm init @eslint/config -- --config eslint-config-standard
```

The `--config` flag also supports passing in arrays:
To use an eslintrc-style (legacy) shared config:

```bash
npm init @eslint/config -- --config semistandard,standard
# or
npm init @eslint/config -- --config semistandard --config standard
npm init @eslint/config -- --eslintrc --config eslint-config-standard
```
39 changes: 34 additions & 5 deletions bin/create-config.js
100644 → 100755
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 唯然<weiran.zsd@outlook.com>
*/

/* 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 });

aladdin-add marked this conversation as resolved.
Show resolved Hide resolved
generator.calc();
await generator.output();
}
11 changes: 1 addition & 10 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import eslintConfigESLint from "eslint-config-eslint";
import globals from "globals";

export default [
{
Expand All @@ -8,13 +7,5 @@ export default [
"tests/fixtures/"
]
},
...eslintConfigESLint,
{
files: ["tests/**"],
languageOptions: {
globals: {
...globals.mocha
}
}
}
...eslintConfigESLint
];
278 changes: 278 additions & 0 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/**
* @fileoverview to generate config files.
* @author 唯然<weiran.zsd@outlook.com>
*/
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.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.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.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";
exportContent += " ...pluginVue.configs[\"flat/essential\"],\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() {

log.info("The config that you've selected requires the following dependencies:\n");
log.info(this.result.devDependencies.join(", "));

const installDevDeps = (await enquirer.prompt({
type: "toggle",
name: "executeInstallation",
message: "Would you like to install them now?",
enabled: "Yes",
disabled: "No",
initial: 1
})).executeInstallation;

const configPath = path.join(this.cwd, this.result.configFilename);

if (installDevDeps === true) {
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;

log.info("☕️Installing...");
installSyncSaveDev(this.result.devDependencies, packageManager);
await writeFile(configPath, this.result.configContent);

// 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.");
} else {
log.info(`Successfully created ${configPath} file.`);
}
} else {
await writeFile(configPath, this.result.configContent);

log.info(`Successfully created ${configPath} file.`);
log.warn("You will need to install the dependencies yourself.");
}
}
}
Loading
Loading