diff --git a/lib/config-generator.js b/lib/config-generator.js index 339f437..6ce67a4 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -13,10 +13,11 @@ import { spawnSync } from "node:child_process"; import { writeFile } from "node:fs/promises"; import enquirer from "enquirer"; import semverGreaterThanRange from "semver/ranges/gtr.js"; +import semverLessThan from "semver/functions/lt.js"; import { isPackageTypeModule, installSyncSaveDev, fetchPeerDependencies, findPackageJson } from "./utils/npm-utils.js"; import { getShorthandName } from "./utils/naming.js"; import * as log from "./utils/logging.js"; -import { langQuestions, jsQuestions, mdQuestions, installationQuestions } from "./questions.js"; +import { langQuestions, jsQuestions, mdQuestions, installationQuestions, addJitiQuestion } from "./questions.js"; //----------------------------------------------------------------------------- // Helpers @@ -127,6 +128,16 @@ export class ConfigGenerator { if (this.answers.languages.includes("md")) { Object.assign(this.answers, await enquirer.prompt(mdQuestions)); } + + if (this.answers.configFileLanguage === "ts") { + const nodeVersion = process.versions.node; + + // Node.js v24.3.0 removed the experimental warning from type stripping. + if (semverLessThan(nodeVersion, "24.3.0")) { + log.info("Jiti is required for Node.js <24.3.0 to read TypeScript configuration files."); + Object.assign(this.answers, await enquirer.prompt(addJitiQuestion)); + } + } } /** @@ -136,7 +147,14 @@ export class ConfigGenerator { async calc() { const isESMModule = isPackageTypeModule(this.packageJsonPath); - this.result.configFilename = isESMModule ? "eslint.config.js" : "eslint.config.mjs"; + let configExt = isESMModule ? "js" : "mjs"; + + if (this.answers.configFileLanguage === "ts") { + configExt = isESMModule ? "ts" : "mts"; + } + + this.result.configFilename = `eslint.config.${configExt}`; + this.answers.config = typeof this.answers.config === "string" ? { packageName: this.answers.config, type: "flat" } : this.answers.config; @@ -320,6 +338,11 @@ export class ConfigGenerator { if (needCompatHelper) { this.result.devDependencies.push("@eslint/eslintrc", "@eslint/js"); } + + if (this.answers.addJiti) { + this.result.devDependencies.push("jiti"); + } + this.result.configContent = `${importContent} ${needCompatHelper ? helperContent : ""} export default defineConfig([\n${exportContent || " {}\n"}]);\n`; // defaults to `[{}]` to avoid empty config warning diff --git a/lib/questions.js b/lib/questions.js index fb26c48..9e3bb8f 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -54,6 +54,8 @@ export const jsQuestions = [ type: "toggle", name: "useTs", message: "Does your project use TypeScript?", + disabled: "No", + enabled: "Yes", initial: 0 }, { @@ -66,6 +68,19 @@ export const jsQuestions = [ { message: "Browser", name: "browser" }, { message: "Node", name: "node" } ] + }, + { + type: "select", + name: "configFileLanguage", + message: "Which language do you want your configuration file be written in?", + initial: 0, + choices: [ + { message: "JavaScript", name: "js" }, + { message: "TypeScript", name: "ts" } + ], + skip() { + return !this.state.answers.useTs; + } } ]; @@ -99,3 +114,12 @@ export const installationQuestions = [ } } ]; + +export const addJitiQuestion = { + type: "toggle", + name: "addJiti", + message: "Would you like to add Jiti as a devDependency?", + disabled: "No", + enabled: "Yes", + initial: 1 +}; diff --git a/tests/__snapshots__/cjs-configfile-js b/tests/__snapshots__/cjs-configfile-js new file mode 100644 index 0000000..962004f --- /dev/null +++ b/tests/__snapshots__/cjs-configfile-js @@ -0,0 +1,24 @@ +{ + "configContent": "import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, + { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, + tseslint.configs.recommended, +]); +", + "configFilename": "eslint.config.mjs", + "devDependencies": [ + "eslint", + "@eslint/js", + "globals", + "typescript-eslint", + ], + "installFlags": [ + "-D", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/cjs-configfile-ts b/tests/__snapshots__/cjs-configfile-ts new file mode 100644 index 0000000..7cb46b7 --- /dev/null +++ b/tests/__snapshots__/cjs-configfile-ts @@ -0,0 +1,24 @@ +{ + "configContent": "import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, + { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, + tseslint.configs.recommended, +]); +", + "configFilename": "eslint.config.mts", + "devDependencies": [ + "eslint", + "@eslint/js", + "globals", + "typescript-eslint", + ], + "installFlags": [ + "-D", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/cjs-configfile-ts-jiti b/tests/__snapshots__/cjs-configfile-ts-jiti new file mode 100644 index 0000000..d4a800f --- /dev/null +++ b/tests/__snapshots__/cjs-configfile-ts-jiti @@ -0,0 +1,25 @@ +{ + "configContent": "import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, + { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, + tseslint.configs.recommended, +]); +", + "configFilename": "eslint.config.mts", + "devDependencies": [ + "eslint", + "@eslint/js", + "globals", + "typescript-eslint", + "jiti", + ], + "installFlags": [ + "-D", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/esm-configfile-js b/tests/__snapshots__/esm-configfile-js new file mode 100644 index 0000000..4e3dda6 --- /dev/null +++ b/tests/__snapshots__/esm-configfile-js @@ -0,0 +1,23 @@ +{ + "configContent": "import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, + tseslint.configs.recommended, +]); +", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "@eslint/js", + "globals", + "typescript-eslint", + ], + "installFlags": [ + "-D", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/esm-configfile-ts b/tests/__snapshots__/esm-configfile-ts new file mode 100644 index 0000000..c03a049 --- /dev/null +++ b/tests/__snapshots__/esm-configfile-ts @@ -0,0 +1,23 @@ +{ + "configContent": "import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, + tseslint.configs.recommended, +]); +", + "configFilename": "eslint.config.ts", + "devDependencies": [ + "eslint", + "@eslint/js", + "globals", + "typescript-eslint", + ], + "installFlags": [ + "-D", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/esm-configfile-ts-jiti b/tests/__snapshots__/esm-configfile-ts-jiti new file mode 100644 index 0000000..68d793d --- /dev/null +++ b/tests/__snapshots__/esm-configfile-ts-jiti @@ -0,0 +1,24 @@ +{ + "configContent": "import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, + tseslint.configs.recommended, +]); +", + "configFilename": "eslint.config.ts", + "devDependencies": [ + "eslint", + "@eslint/js", + "globals", + "typescript-eslint", + "jiti", + ], + "installFlags": [ + "-D", + ], +} \ No newline at end of file diff --git a/tests/config-snapshots.spec.js b/tests/config-snapshots.spec.js index ea9c9d0..cee170c 100644 --- a/tests/config-snapshots.spec.js +++ b/tests/config-snapshots.spec.js @@ -36,7 +36,10 @@ describe("generate config for esm projects", () => { { name: "esm-markdown-gfm-problems", answers: { languages: ["md"], mdType: "gfm", purpose: "problems" } }, { name: "esm-javascript-json-problems", answers: { languages: ["javascript", "json"], purpose: "problems", moduleType: "esm", framework: "none", useTs: false, env: ["node"] } }, { name: "esm-css-syntax", answers: { languages: ["css"], purpose: "syntax" } }, - { name: "esm-css-problems", answers: { languages: ["css"], purpose: "problems" } } + { name: "esm-css-problems", answers: { languages: ["css"], purpose: "problems" } }, + { name: "esm-configfile-js", answers: { languages: ["javascript"], purpose: "problems", moduleType: "esm", framework: "none", useTs: true, configFileLanguage: "js", env: ["node"] } }, + { name: "esm-configfile-ts", answers: { languages: ["javascript"], purpose: "problems", moduleType: "esm", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: false, env: ["node"] } }, + { name: "esm-configfile-ts-jiti", answers: { languages: ["javascript"], purpose: "problems", moduleType: "esm", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: true, env: ["node"] } } ]; // generate all possible combinations @@ -77,7 +80,9 @@ describe("generate config for esm projects", () => { await generator.calc(); - expect(generator.result.configFilename).toBe("eslint.config.js"); + const expectedExtension = item.answers.configFileLanguage === "ts" ? "ts" : "js"; + + expect(generator.result.configFilename).toBe(`eslint.config.${expectedExtension}`); expect(generator.packageJsonPath).toBe(join(esmProjectDir, "./package.json")); expect(generator.result.configContent.endsWith("\n")).toBe(true); expect(generator.result).toMatchFileSnapshot(`./__snapshots__/${item.name}`); @@ -129,7 +134,10 @@ describe("generate config for cjs projects", () => { answers: { config: "eslint-config-standard" } - }]; + }, + { name: "cjs-configfile-js", answers: { languages: ["javascript"], purpose: "problems", moduleType: "commonjs", framework: "none", useTs: true, configFileLanguage: "js", env: ["node"] } }, + { name: "cjs-configfile-ts", answers: { languages: ["javascript"], purpose: "problems", moduleType: "commonjs", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: false, env: ["node"] } }, + { name: "cjs-configfile-ts-jiti", answers: { languages: ["javascript"], purpose: "problems", moduleType: "commonjs", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: true, env: ["node"] } }]; inputs.forEach(item => { test(`${item.name}`, async () => { @@ -137,7 +145,9 @@ describe("generate config for cjs projects", () => { await generator.calc(); - expect(generator.result.configFilename).toBe("eslint.config.mjs"); + const expectedExtension = item.answers.configFileLanguage === "ts" ? "mts" : "mjs"; + + expect(generator.result.configFilename).toBe(`eslint.config.${expectedExtension}`); expect(generator.packageJsonPath).toBe(join(cjsProjectDir, "./package.json")); expect(generator.result.configContent.endsWith("\n")).toBe(true); expect(generator.result).toMatchFileSnapshot(`./__snapshots__/${item.name}`);