diff --git a/bin/create-config.js b/bin/create-config.js index ccf9d7b1..5f494e99 100755 --- a/bin/create-config.js +++ b/bin/create-config.js @@ -7,15 +7,13 @@ import { ConfigGenerator } from "../lib/config-generator.js"; import { findPackageJson } from "../lib/utils/npm-utils.js"; -import * as log from "../lib/utils/logging.js"; +import { intro, outro } from "@clack/prompts"; import process from "node:process"; import fs from "node:fs/promises"; import { parseArgs } from "node:util"; const pkg = JSON.parse(await fs.readFile(new URL("../package.json", import.meta.url), "utf8")); -log.log(`${pkg.name}: v${pkg.version}`); - let options; try { @@ -33,20 +31,13 @@ try { options = values; } catch (error) { - log.error(error.message); + // eslint-disable-next-line no-console -- show an error + console.error("Error:", error.message); // eslint-disable-next-line n/no-process-exit -- exit gracefully on invalid arguments process.exit(1); } -process.on("uncaughtException", error => { - if (error instanceof Error && error.code === "ERR_USE_AFTER_CLOSE") { - log.error("Operation canceled"); - // eslint-disable-next-line n/no-process-exit -- exit gracefully on Ctrl+C - process.exit(1); - } else { - throw error; - } -}); +intro(`${pkg.name}: v${pkg.version}`); const cwd = process.cwd(); const packageJsonPath = findPackageJson(cwd); @@ -72,3 +63,5 @@ if (!options.config) { await generator.calc(); await generator.output(); } + +outro("Thank you"); diff --git a/lib/config-generator.js b/lib/config-generator.js index d0e1b179..ab63958e 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -11,12 +11,11 @@ import process from "node:process"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { writeFile } from "node:fs/promises"; -import enquirer from "enquirer"; +import * as p from "@clack/prompts"; import semverGreaterThanRange from "semver/ranges/gtr.js"; import semverLessThan from "semver/functions/lt.js"; import { isPackageTypeModule, installSyncSaveDev, fetchPeerDependencies, findPackageJson, parsePackageName } from "./utils/npm-utils.js"; import { getShorthandName } from "./utils/naming.js"; -import * as log from "./utils/logging.js"; import { langQuestions, jsQuestions, mdQuestions, installationQuestions, addJitiQuestion } from "./questions.js"; //----------------------------------------------------------------------------- @@ -86,6 +85,21 @@ function addEnvironmentConfig({ env, devDependencies }) { }; } +/** + * `p.group` wrapper. + * @param {Record Promise>} prompts Prompts Object. + * @returns {Promise>} Prompts result. + */ +function groupPrompts(prompts) { + return p.group(prompts, { + onCancel() { + p.cancel("Operation cancelled."); + // eslint-disable-next-line n/no-process-exit -- Exit gracefully + process.exit(0); + } + }); +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -119,14 +133,14 @@ export class ConfigGenerator { * @returns {Promise} */ async prompt() { - Object.assign(this.answers, await enquirer.prompt(langQuestions)); + Object.assign(this.answers, await groupPrompts(langQuestions)); if (this.answers.languages.includes("javascript")) { - Object.assign(this.answers, await enquirer.prompt(jsQuestions)); + Object.assign(this.answers, await groupPrompts(jsQuestions)); } if (this.answers.languages.includes("md")) { - Object.assign(this.answers, await enquirer.prompt(mdQuestions)); + Object.assign(this.answers, await groupPrompts(mdQuestions)); } if (this.answers.configFileLanguage === "ts") { @@ -134,8 +148,8 @@ export class ConfigGenerator { // 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)); + p.log.info("Jiti is required for Node.js <24.3.0 to read TypeScript configuration files."); + Object.assign(this.answers, await groupPrompts(addJitiQuestion)); } } } @@ -353,15 +367,14 @@ export default defineConfig([\n${exportContent || " {}\n"}]);\n`; // defaults t */ async output() { - log.info("The config that you've selected requires the following dependencies:\n"); - log.log(this.result.devDependencies.join(", ")); - + p.log.info("The config that you've selected requires the following dependencies:"); + p.note(this.result.devDependencies.join(", "), "Required Dependencies"); - const { executeInstallation, packageManager } = (await enquirer.prompt(installationQuestions)); + const { executeInstallation, packageManager } = (await groupPrompts(installationQuestions)); const configPath = path.join(this.cwd, this.result.configFilename); if (executeInstallation === true) { - log.log("☕️Installing..."); + p.log.step("☕️Installing..."); installSyncSaveDev(this.result.devDependencies, packageManager, this.result.installFlags); await writeFile(configPath, this.result.configContent); @@ -371,15 +384,15 @@ export default defineConfig([\n${exportContent || " {}\n"}]);\n`; // defaults t 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."); + p.log.error("A config file was generated, but the config file itself may not follow your linting rules."); } else { - log.success(`Successfully created ${configPath} file.`); + p.log.success(`Successfully created ${configPath} file.`); } } else { await writeFile(configPath, this.result.configContent); - log.success(`Successfully created ${configPath} file.`); - log.warn("You will need to install the dependencies yourself."); + p.log.success(`Successfully created ${configPath} file.`); + p.log.warn("You will need to install the dependencies yourself."); } } } diff --git a/lib/questions.js b/lib/questions.js index 1e27f32f..67cc39a4 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -11,152 +11,119 @@ // Imports // ------------------------------------------------------------------------------ -import colors from "ansi-colors"; - -// ------------------------------------------------------------------------------ -// Helpers -// ------------------------------------------------------------------------------ - -/** - * Set questions prompt style options in here. - * @param {PlainObject[]} questionsPromptArray Array of questions prompt. - * @returns {PlainObject[]} Questions prompt with style options. - */ -function setQuestionsPromptStyle(questionsPromptArray) { - return questionsPromptArray.map(opts => ({ - ...opts, - symbols: { - - // For option symbol in select and multiselect - indicator: { - on: colors.cyan(colors.symbols.radioOn), - off: colors.gray(colors.symbols.radioOff) - } - } - })); -} +import * as p from "@clack/prompts"; // ------------------------------------------------------------------------------ // Exports // ------------------------------------------------------------------------------ -export const langQuestions = setQuestionsPromptStyle([{ - type: "multiselect", - name: "languages", - message: "What do you want to lint?", - choices: [ - { message: "JavaScript", name: "javascript" }, - { message: "JSON", name: "json" }, - { message: "JSON with comments", name: "jsonc" }, - { message: "JSON5", name: "json5" }, - { message: "Markdown", name: "md" }, - { message: "CSS", name: "css" } - ], - initial: 0 -}, { - 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" } - ] -}]); +export const langQuestions = { + languages: () => p.multiselect({ + message: "What do you want to lint?", + options: [ + { label: "JavaScript", value: "javascript" }, + { label: "JSON", value: "json" }, + { label: "JSON with comments", value: "jsonc" }, + { label: "JSON5", value: "json5" }, + { label: "Markdown", value: "md" }, + { label: "CSS", value: "css" } + ], + initialValues: ["javascript"] + }), + purpose: () => p.select({ + message: "How would you like to use ESLint?", + initialValue: "problems", + options: [ + { label: "To check syntax only", value: "syntax" }, + { label: "To check syntax and find problems", value: "problems" } + ] + }) +}; -export const jsQuestions = setQuestionsPromptStyle([ - { - type: "select", - name: "moduleType", +export const jsQuestions = { + moduleType: () => p.select({ 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" } + initialValue: "esm", + options: [ + { label: "JavaScript modules (import/export)", value: "esm" }, + { label: "CommonJS (require/exports)", value: "commonjs" }, + { label: "None of these", value: "script" } ] - }, - { - type: "select", - name: "framework", + }), + framework: () => p.select({ 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" } + initialValue: "react", + options: [ + { label: "React", value: "react" }, + { label: "Vue.js", value: "vue" }, + { label: "None of these", value: "none" } ] - }, - { - type: "toggle", - name: "useTs", + }), + useTs: () => p.confirm({ message: "Does your project use TypeScript?", - disabled: "No", - enabled: "Yes", - initial: 0 - }, - { - type: "multiselect", - name: "env", + initialValue: false + }), + env: () => p.multiselect({ message: "Where does your code run?", - hint: "(Press to select, to toggle all, to invert selection)", - initial: 0, - choices: [ - { message: "Browser", name: "browser" }, - { message: "Node", name: "node" } + initialValues: ["browser"], + options: [ + { label: "Browser", value: "browser" }, + { label: "Node", value: "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; + }), + async configFileLanguage({ results }) { + if (results.useTs) { + return await p.select({ + message: "Which language do you want your configuration file be written in?", + initialValue: "js", + options: [ + { label: "JavaScript", value: "js" }, + { label: "TypeScript", value: "ts" } + ] + }); } + + return "js"; } -]); +}; -export const mdQuestions = setQuestionsPromptStyle([{ - type: "select", - name: "mdType", - message: "What flavor of Markdown do you want to lint?", - initial: 0, - choices: [ - { message: "CommonMark", name: "commonmark" }, - { message: "GitHub Flavored Markdown", name: "gfm" } - ] -}]); +export const mdQuestions = { + mdType: () => p.select({ + message: "What flavor of Markdown do you want to lint?", + initialValue: "commonmark", + options: [ + { label: "CommonMark", value: "commonmark" }, + { label: "GitHub Flavored Markdown", value: "gfm" } + ] + }) +}; -export const installationQuestions = setQuestionsPromptStyle([ - { - type: "toggle", - name: "executeInstallation", +export const installationQuestions = { + executeInstallation: () => p.confirm({ message: "Would you like to install them now?", - enabled: "Yes", - disabled: "No", - initial: 1 - }, { - type: "select", - name: "packageManager", - message: "Which package manager do you want to use?", - initial: 0, - choices: ["npm", "yarn", "pnpm", "bun"], - skip() { - return this.state.answers.executeInstallation === false; + initialValue: true + }), + async packageManager({ results }) { + if (results.executeInstallation) { + return await p.select({ + message: "Which package manager do you want to use?", + initialValue: "npm", + options: [ + { value: "npm" }, + { value: "yarn" }, + { value: "pnpm" }, + { value: "bun" } + ] + }); } + + return "npm"; } -]); +}; -export const addJitiQuestion = setQuestionsPromptStyle([{ - type: "toggle", - name: "addJiti", - message: "Would you like to add Jiti as a devDependency?", - disabled: "No", - enabled: "Yes", - initial: 1 -}]); +export const addJitiQuestion = { + addJiti: () => p.confirm({ + message: "Would you like to add Jiti as a devDependency?", + initialValue: true + }) +}; diff --git a/lib/utils/logging.js b/lib/utils/logging.js deleted file mode 100644 index 7cd72c92..00000000 --- a/lib/utils/logging.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @fileoverview Handle logging for ESLint - * @author Gyandeep Singh - */ - -// ------------------------------------------------------------------------------ -// Imports -// ------------------------------------------------------------------------------ - -import colors from "ansi-colors"; - -// ------------------------------------------------------------------------------ -// Helpers -// ------------------------------------------------------------------------------ - -/** - * Used for joining and add bold style to an array of arguments. - * @param {any[]} args Array of arguments. - * @returns {string} Joined and bolded string. - */ -function boldArgs(args) { - return colors.bold(args.join(" ")); -} - -// ------------------------------------------------------------------------------ -// Exports -// ------------------------------------------------------------------------------ - -/* eslint no-console: "off" -- Logging util */ - -/** - * Cover for console.log - * @param {...any} args The elements to log. - * @returns {void} - */ -export function log(...args) { - console.log(boldArgs(args)); -} - -/** - * Cover for console.log with check symbol - * @param {...any} args The elements to log. - * @returns {void} - */ -export function success(...args) { - console.log(colors.green(colors.symbols.check), boldArgs(args)); -} - -/** - * Cover for console.info with info symbol - * @param {...any} args The elements to log. - * @returns {void} - */ -export function info(...args) { - console.info(colors.blue(colors.symbols.info), boldArgs(args)); -} - -/** - * Cover for console.warn with warn symbol - * @param {...any} args The elements to log. - * @returns {void} - */ -export function warn(...args) { - console.warn(colors.yellow(colors.symbols.warning), boldArgs(args)); -} - -/** - * Cover for console.error with cross symbol - * @param {...any} args The elements to log. - * @returns {void} - */ -export function error(...args) { - console.error(colors.magenta(colors.symbols.cross), boldArgs(args)); -} diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index 61e5e09b..0d19d8d0 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -11,7 +11,7 @@ import fs from "node:fs"; import spawn from "cross-spawn"; import path from "node:path"; -import * as log from "./logging.js"; +import { log } from "@clack/prompts"; import semverMaxSatisfying from "semver/ranges/max-satisfying.js"; //------------------------------------------------------------------------------ diff --git a/package.json b/package.json index c7367d31..5317128d 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,8 @@ "test:snapshots:update": "vitest -u run snapshots" }, "dependencies": { - "ansi-colors": "^4.1.3", + "@clack/prompts": "^1.1.0", "cross-spawn": "^7.0.2", - "enquirer": "^2.3.5", "semver": "^7.7.1" }, "devDependencies": { diff --git a/tests/utils/npm-utils.spec.js b/tests/utils/npm-utils.spec.js index c72f5ac5..705aae85 100644 --- a/tests/utils/npm-utils.spec.js +++ b/tests/utils/npm-utils.spec.js @@ -93,10 +93,10 @@ describe("npmUtils", () => { }); describe("checkDeps()", () => { - let installStatus = checkDeps(["enquirer", "mocha", "notarealpackage", "jshint"]); + let installStatus = checkDeps(["semver", "mocha", "notarealpackage", "jshint"]); it("should find a direct dependency of the project", () => { - assert.isTrue(installStatus.enquirer); + assert.isTrue(installStatus.semver); }); it("should not find a dev dependency of the project", () => { @@ -202,9 +202,9 @@ describe("npmUtils", () => { it("should log an error message if npm throws ENOENT error", async () => { const logErrorStub = sinon.spy(); const npmUtilsStub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } }); - const log = await import("../../lib/utils/logging.js"); + const { log } = await import("@clack/prompts"); - sinon.replaceGetter(log, "error", () => logErrorStub); + sinon.stub(log, "error").get(() => logErrorStub); installSyncSaveDev("some-package");