Skip to content

Commit 04935f1

Browse files
authored
feat: add TypeScript config file support (#181)
* feat: add TypeScript config file support * use semver
1 parent 9352550 commit 04935f1

File tree

9 files changed

+206
-6
lines changed

9 files changed

+206
-6
lines changed

lib/config-generator.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { spawnSync } from "node:child_process";
1313
import { writeFile } from "node:fs/promises";
1414
import enquirer from "enquirer";
1515
import semverGreaterThanRange from "semver/ranges/gtr.js";
16+
import semverLessThan from "semver/functions/lt.js";
1617
import { isPackageTypeModule, installSyncSaveDev, fetchPeerDependencies, findPackageJson } from "./utils/npm-utils.js";
1718
import { getShorthandName } from "./utils/naming.js";
1819
import * as log from "./utils/logging.js";
19-
import { langQuestions, jsQuestions, mdQuestions, installationQuestions } from "./questions.js";
20+
import { langQuestions, jsQuestions, mdQuestions, installationQuestions, addJitiQuestion } from "./questions.js";
2021

2122
//-----------------------------------------------------------------------------
2223
// Helpers
@@ -127,6 +128,16 @@ export class ConfigGenerator {
127128
if (this.answers.languages.includes("md")) {
128129
Object.assign(this.answers, await enquirer.prompt(mdQuestions));
129130
}
131+
132+
if (this.answers.configFileLanguage === "ts") {
133+
const nodeVersion = process.versions.node;
134+
135+
// Node.js v24.3.0 removed the experimental warning from type stripping.
136+
if (semverLessThan(nodeVersion, "24.3.0")) {
137+
log.info("Jiti is required for Node.js <24.3.0 to read TypeScript configuration files.");
138+
Object.assign(this.answers, await enquirer.prompt(addJitiQuestion));
139+
}
140+
}
130141
}
131142

132143
/**
@@ -136,7 +147,14 @@ export class ConfigGenerator {
136147
async calc() {
137148
const isESMModule = isPackageTypeModule(this.packageJsonPath);
138149

139-
this.result.configFilename = isESMModule ? "eslint.config.js" : "eslint.config.mjs";
150+
let configExt = isESMModule ? "js" : "mjs";
151+
152+
if (this.answers.configFileLanguage === "ts") {
153+
configExt = isESMModule ? "ts" : "mts";
154+
}
155+
156+
this.result.configFilename = `eslint.config.${configExt}`;
157+
140158
this.answers.config = typeof this.answers.config === "string"
141159
? { packageName: this.answers.config, type: "flat" }
142160
: this.answers.config;
@@ -320,6 +338,11 @@ export class ConfigGenerator {
320338
if (needCompatHelper) {
321339
this.result.devDependencies.push("@eslint/eslintrc", "@eslint/js");
322340
}
341+
342+
if (this.answers.addJiti) {
343+
this.result.devDependencies.push("jiti");
344+
}
345+
323346
this.result.configContent = `${importContent}
324347
${needCompatHelper ? helperContent : ""}
325348
export default defineConfig([\n${exportContent || " {}\n"}]);\n`; // defaults to `[{}]` to avoid empty config warning

lib/questions.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export const jsQuestions = [
5454
type: "toggle",
5555
name: "useTs",
5656
message: "Does your project use TypeScript?",
57+
disabled: "No",
58+
enabled: "Yes",
5759
initial: 0
5860
},
5961
{
@@ -66,6 +68,19 @@ export const jsQuestions = [
6668
{ message: "Browser", name: "browser" },
6769
{ message: "Node", name: "node" }
6870
]
71+
},
72+
{
73+
type: "select",
74+
name: "configFileLanguage",
75+
message: "Which language do you want your configuration file be written in?",
76+
initial: 0,
77+
choices: [
78+
{ message: "JavaScript", name: "js" },
79+
{ message: "TypeScript", name: "ts" }
80+
],
81+
skip() {
82+
return !this.state.answers.useTs;
83+
}
6984
}
7085
];
7186

@@ -99,3 +114,12 @@ export const installationQuestions = [
99114
}
100115
}
101116
];
117+
118+
export const addJitiQuestion = {
119+
type: "toggle",
120+
name: "addJiti",
121+
message: "Would you like to add Jiti as a devDependency?",
122+
disabled: "No",
123+
enabled: "Yes",
124+
initial: 1
125+
};

tests/__snapshots__/cjs-configfile-js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configContent": "import js from "@eslint/js";
3+
import globals from "globals";
4+
import tseslint from "typescript-eslint";
5+
import { defineConfig } from "eslint/config";
6+
7+
8+
export default defineConfig([
9+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
10+
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
11+
tseslint.configs.recommended,
12+
]);
13+
",
14+
"configFilename": "eslint.config.mjs",
15+
"devDependencies": [
16+
"eslint",
17+
"@eslint/js",
18+
"globals",
19+
"typescript-eslint",
20+
],
21+
"installFlags": [
22+
"-D",
23+
],
24+
}

tests/__snapshots__/cjs-configfile-ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configContent": "import js from "@eslint/js";
3+
import globals from "globals";
4+
import tseslint from "typescript-eslint";
5+
import { defineConfig } from "eslint/config";
6+
7+
8+
export default defineConfig([
9+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
10+
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
11+
tseslint.configs.recommended,
12+
]);
13+
",
14+
"configFilename": "eslint.config.mts",
15+
"devDependencies": [
16+
"eslint",
17+
"@eslint/js",
18+
"globals",
19+
"typescript-eslint",
20+
],
21+
"installFlags": [
22+
"-D",
23+
],
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"configContent": "import js from "@eslint/js";
3+
import globals from "globals";
4+
import tseslint from "typescript-eslint";
5+
import { defineConfig } from "eslint/config";
6+
7+
8+
export default defineConfig([
9+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
10+
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
11+
tseslint.configs.recommended,
12+
]);
13+
",
14+
"configFilename": "eslint.config.mts",
15+
"devDependencies": [
16+
"eslint",
17+
"@eslint/js",
18+
"globals",
19+
"typescript-eslint",
20+
"jiti",
21+
],
22+
"installFlags": [
23+
"-D",
24+
],
25+
}

tests/__snapshots__/esm-configfile-js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"configContent": "import js from "@eslint/js";
3+
import globals from "globals";
4+
import tseslint from "typescript-eslint";
5+
import { defineConfig } from "eslint/config";
6+
7+
8+
export default defineConfig([
9+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
10+
tseslint.configs.recommended,
11+
]);
12+
",
13+
"configFilename": "eslint.config.js",
14+
"devDependencies": [
15+
"eslint",
16+
"@eslint/js",
17+
"globals",
18+
"typescript-eslint",
19+
],
20+
"installFlags": [
21+
"-D",
22+
],
23+
}

tests/__snapshots__/esm-configfile-ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"configContent": "import js from "@eslint/js";
3+
import globals from "globals";
4+
import tseslint from "typescript-eslint";
5+
import { defineConfig } from "eslint/config";
6+
7+
8+
export default defineConfig([
9+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
10+
tseslint.configs.recommended,
11+
]);
12+
",
13+
"configFilename": "eslint.config.ts",
14+
"devDependencies": [
15+
"eslint",
16+
"@eslint/js",
17+
"globals",
18+
"typescript-eslint",
19+
],
20+
"installFlags": [
21+
"-D",
22+
],
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configContent": "import js from "@eslint/js";
3+
import globals from "globals";
4+
import tseslint from "typescript-eslint";
5+
import { defineConfig } from "eslint/config";
6+
7+
8+
export default defineConfig([
9+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
10+
tseslint.configs.recommended,
11+
]);
12+
",
13+
"configFilename": "eslint.config.ts",
14+
"devDependencies": [
15+
"eslint",
16+
"@eslint/js",
17+
"globals",
18+
"typescript-eslint",
19+
"jiti",
20+
],
21+
"installFlags": [
22+
"-D",
23+
],
24+
}

tests/config-snapshots.spec.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ describe("generate config for esm projects", () => {
3636
{ name: "esm-markdown-gfm-problems", answers: { languages: ["md"], mdType: "gfm", purpose: "problems" } },
3737
{ name: "esm-javascript-json-problems", answers: { languages: ["javascript", "json"], purpose: "problems", moduleType: "esm", framework: "none", useTs: false, env: ["node"] } },
3838
{ name: "esm-css-syntax", answers: { languages: ["css"], purpose: "syntax" } },
39-
{ name: "esm-css-problems", answers: { languages: ["css"], purpose: "problems" } }
39+
{ name: "esm-css-problems", answers: { languages: ["css"], purpose: "problems" } },
40+
{ name: "esm-configfile-js", answers: { languages: ["javascript"], purpose: "problems", moduleType: "esm", framework: "none", useTs: true, configFileLanguage: "js", env: ["node"] } },
41+
{ name: "esm-configfile-ts", answers: { languages: ["javascript"], purpose: "problems", moduleType: "esm", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: false, env: ["node"] } },
42+
{ name: "esm-configfile-ts-jiti", answers: { languages: ["javascript"], purpose: "problems", moduleType: "esm", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: true, env: ["node"] } }
4043
];
4144

4245
// generate all possible combinations
@@ -77,7 +80,9 @@ describe("generate config for esm projects", () => {
7780

7881
await generator.calc();
7982

80-
expect(generator.result.configFilename).toBe("eslint.config.js");
83+
const expectedExtension = item.answers.configFileLanguage === "ts" ? "ts" : "js";
84+
85+
expect(generator.result.configFilename).toBe(`eslint.config.${expectedExtension}`);
8186
expect(generator.packageJsonPath).toBe(join(esmProjectDir, "./package.json"));
8287
expect(generator.result.configContent.endsWith("\n")).toBe(true);
8388
expect(generator.result).toMatchFileSnapshot(`./__snapshots__/${item.name}`);
@@ -129,15 +134,20 @@ describe("generate config for cjs projects", () => {
129134
answers: {
130135
config: "eslint-config-standard"
131136
}
132-
}];
137+
},
138+
{ name: "cjs-configfile-js", answers: { languages: ["javascript"], purpose: "problems", moduleType: "commonjs", framework: "none", useTs: true, configFileLanguage: "js", env: ["node"] } },
139+
{ name: "cjs-configfile-ts", answers: { languages: ["javascript"], purpose: "problems", moduleType: "commonjs", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: false, env: ["node"] } },
140+
{ name: "cjs-configfile-ts-jiti", answers: { languages: ["javascript"], purpose: "problems", moduleType: "commonjs", framework: "none", useTs: true, configFileLanguage: "ts", addJiti: true, env: ["node"] } }];
133141

134142
inputs.forEach(item => {
135143
test(`${item.name}`, async () => {
136144
const generator = new ConfigGenerator({ cwd: cjsProjectDir, answers: item.answers });
137145

138146
await generator.calc();
139147

140-
expect(generator.result.configFilename).toBe("eslint.config.mjs");
148+
const expectedExtension = item.answers.configFileLanguage === "ts" ? "mts" : "mjs";
149+
150+
expect(generator.result.configFilename).toBe(`eslint.config.${expectedExtension}`);
141151
expect(generator.packageJsonPath).toBe(join(cjsProjectDir, "./package.json"));
142152
expect(generator.result.configContent.endsWith("\n")).toBe(true);
143153
expect(generator.result).toMatchFileSnapshot(`./__snapshots__/${item.name}`);

0 commit comments

Comments
 (0)