From ecdb5fa44fa15228894946b77d8c5eecc372a340 Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Wed, 8 Oct 2025 17:39:43 +0700 Subject: [PATCH 01/12] fix: improve shareable config --- lib/utils/npm-utils.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index 1f97db99..c2864734 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -12,6 +12,7 @@ import fs from "node:fs"; import spawn from "cross-spawn"; import path from "node:path"; import * as log from "./logging.js"; +import semverMaxSatisfying from "semver/ranges/max-satisfying.js"; //------------------------------------------------------------------------------ // Helpers @@ -104,8 +105,11 @@ async function fetchPeerDependencies(packageName) { const response = await fetch(`https://registry.npmjs.org/${name}`); const data = await response.json(); - const resolvedVersion = - version === "latest" ? data["dist-tags"]?.latest : version; + if (data.error) { + throw new Error(`Package "${name}@${version}" is ${data.error.toLowerCase()}`); + } + + const resolvedVersion = semverMaxSatisfying(Object.keys(data.versions), data["dist-tags"]?.[version] ?? version); const packageVersion = data.versions[resolvedVersion]; if (!packageVersion) { @@ -113,7 +117,7 @@ async function fetchPeerDependencies(packageName) { `Version "${version}" not found for package "${name}".` ); } - return Object.entries(packageVersion.peerDependencies).map( + return Object.entries(Object(packageVersion.peerDependencies)).map( ([pkgName, pkgVersion]) => `${pkgName}@${pkgVersion}` ); } catch { @@ -125,6 +129,11 @@ async function fetchPeerDependencies(packageName) { const fetchedText = npmProcess.stdout.trim(); const peers = JSON.parse(fetchedText || "{}"); + + if (peers.error) { + throw new Error(`Cannot fetch "${packageName}" with error: ${peers.error.summary}`); + } + const dependencies = []; Object.keys(peers).forEach(pkgName => { From fecf3f343f39eba76bb2f03903f037247ab628de Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Thu, 9 Oct 2025 14:30:00 +0700 Subject: [PATCH 02/12] throw error (again) --- lib/utils/npm-utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index c2864734..87d63ddc 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -105,6 +105,7 @@ async function fetchPeerDependencies(packageName) { const response = await fetch(`https://registry.npmjs.org/${name}`); const data = await response.json(); + if (data.error) { throw new Error(`Package "${name}@${version}" is ${data.error.toLowerCase()}`); } @@ -120,10 +121,9 @@ async function fetchPeerDependencies(packageName) { return Object.entries(Object(packageVersion.peerDependencies)).map( ([pkgName, pkgVersion]) => `${pkgName}@${pkgVersion}` ); - } catch { - - // TODO: should throw an error instead of returning null - return null; + } catch (err) { + // eslint-disable-next-line preserve-caught-error -- Throw error again :) + throw new Error(`Cannot fetch "${name}@${version}" with error: ${err.message || err}`); } } const fetchedText = npmProcess.stdout.trim(); From c2f62598c0c4428ed91f7a7d3c10742b97e841b0 Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Mon, 13 Oct 2025 11:13:21 +0700 Subject: [PATCH 03/12] update test --- tests/utils/npm-utils.spec.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/utils/npm-utils.spec.js b/tests/utils/npm-utils.spec.js index e3269a0b..76262784 100644 --- a/tests/utils/npm-utils.spec.js +++ b/tests/utils/npm-utils.spec.js @@ -18,7 +18,7 @@ import { parsePackageName } from "../../lib/utils/npm-utils.js"; import { defineInMemoryFs } from "../_utils/in-memory-fs.js"; -import { assert, describe, afterEach, it } from "vitest"; +import { assert, describe, afterEach, it, expect } from "vitest"; import fs from "node:fs"; import process from "node:process"; @@ -281,12 +281,10 @@ describe("npmUtils", () => { fetchStub.restore(); }); - it("should return null if an error is thrown", async () => { + it("should throw if an error is thrown", async () => { const stub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } }); - const peerDependencies = await fetchPeerDependencies("desired-package"); - - assert.isNull(peerDependencies); + expect(async () => await fetchPeerDependencies("desired-package")).rejects.toThrowError(); stub.restore(); }); From 22d042ca3d151b70c278399347c1d32f0bfe85f0 Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Tue, 14 Oct 2025 02:58:42 +0700 Subject: [PATCH 04/12] move from spawn npm to fetch only --- lib/utils/npm-utils.js | 66 ++++++++++++------------------------------ 1 file changed, 19 insertions(+), 47 deletions(-) diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index 87d63ddc..b96bdc68 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -87,60 +87,32 @@ function parsePackageName(packageName) { * @returns {Object} Gotten peerDependencies. Returns null if npm was not found. */ async function fetchPeerDependencies(packageName) { - const npmProcess = spawn.sync( - "npm", - ["show", "--json", packageName, "peerDependencies"], - { encoding: "utf8" } - ); + const { name, version } = parsePackageName(packageName); - const error = npmProcess.error; + try { + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- Fallback using built-in fetch + const response = await fetch(`https://registry.npmjs.org/${name}`); + const data = await response.json(); - if (error && error.code === "ENOENT") { - - // Fallback to using the npm registry API directly when npm is not available. - const { name, version } = parsePackageName(packageName); - - try { - // eslint-disable-next-line n/no-unsupported-features/node-builtins -- Fallback using built-in fetch - const response = await fetch(`https://registry.npmjs.org/${name}`); - const data = await response.json(); - - - if (data.error) { - throw new Error(`Package "${name}@${version}" is ${data.error.toLowerCase()}`); - } + if (data.error) { + throw new Error(data.error); + } - const resolvedVersion = semverMaxSatisfying(Object.keys(data.versions), data["dist-tags"]?.[version] ?? version); - const packageVersion = data.versions[resolvedVersion]; + const resolvedVersion = semverMaxSatisfying(Object.keys(data.versions), data["dist-tags"]?.[version] ?? version); + const packageVersion = data.versions[resolvedVersion]; - if (!packageVersion) { - throw new Error( - `Version "${version}" not found for package "${name}".` - ); - } - return Object.entries(Object(packageVersion.peerDependencies)).map( - ([pkgName, pkgVersion]) => `${pkgName}@${pkgVersion}` + if (!packageVersion) { + throw new Error( + `Version "${version}" not found for package "${name}".` ); - } catch (err) { - // eslint-disable-next-line preserve-caught-error -- Throw error again :) - throw new Error(`Cannot fetch "${name}@${version}" with error: ${err.message || err}`); } + return Object.entries(Object(packageVersion.peerDependencies)).map( + ([pkgName, pkgVersion]) => `${pkgName}@${pkgVersion}` + ); + } catch (err) { + // eslint-disable-next-line preserve-caught-error -- Throw error again :) + throw new Error(`Cannot fetch "${name}@${version}" with error: ${err.message || err}`); } - const fetchedText = npmProcess.stdout.trim(); - - const peers = JSON.parse(fetchedText || "{}"); - - if (peers.error) { - throw new Error(`Cannot fetch "${packageName}" with error: ${peers.error.summary}`); - } - - const dependencies = []; - - Object.keys(peers).forEach(pkgName => { - dependencies.push(`${pkgName}@${peers[pkgName]}`); - }); - - return dependencies; } /** From bd293689d01106197b1e43149559241f3bd1c156 Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Tue, 14 Oct 2025 03:12:27 +0700 Subject: [PATCH 05/12] update test --- tests/utils/npm-utils.spec.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/utils/npm-utils.spec.js b/tests/utils/npm-utils.spec.js index 76262784..27f80a2e 100644 --- a/tests/utils/npm-utils.spec.js +++ b/tests/utils/npm-utils.spec.js @@ -241,20 +241,10 @@ describe("npmUtils", () => { }); describe("fetchPeerDependencies()", () => { - it("should execute 'npm show --json peerDependencies' command", async () => { - const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); - - await fetchPeerDependencies("desired-package"); - assert(stub.calledOnce); - assert.strictEqual(stub.firstCall.args[0], "npm"); - assert.deepStrictEqual(stub.firstCall.args[1], ["show", "--json", "desired-package", "peerDependencies"]); - stub.restore(); - }); // Skip on Node.js v21 due to a bug where fetch cannot be stubbed // See: https://github.com/sinonjs/sinon/issues/2590 - it.skipIf(process.version.startsWith("v21"))("should fetch peer dependencies from npm registry when npm is not available", async () => { - const npmStub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } }); + it.skipIf(process.version.startsWith("v21"))("should fetch peer dependencies from npm registry", async () => { const fetchStub = sinon.stub(globalThis, "fetch"); const mockResponse = { @@ -277,12 +267,19 @@ describe("npmUtils", () => { assert(fetchStub.calledOnceWith("https://registry.npmjs.org/desired-package")); assert.deepStrictEqual(result, ["eslint@9.0.0"]); - npmStub.restore(); fetchStub.restore(); }); it("should throw if an error is thrown", async () => { - const stub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } }); + const stub = sinon.stub(globalThis, "fetch"); + + const mockResponse = { + json: sinon.stub().resolves({ error: "Not found" }), + ok: false, + status: 404 + }; + + stub.resolves(mockResponse); expect(async () => await fetchPeerDependencies("desired-package")).rejects.toThrowError(); From 896baf750766314955c77d750510845b88d0f331 Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Tue, 14 Oct 2025 03:22:07 +0700 Subject: [PATCH 06/12] update config-generator calc function --- lib/config-generator.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/config-generator.js b/lib/config-generator.js index b1145104..6901a4f8 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -14,7 +14,7 @@ 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 { 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"; @@ -287,40 +287,39 @@ export class ConfigGenerator { // passed `--config` if (this.answers.config) { const config = this.answers.config; + const { name } = parsePackageName(config.packageName); this.result.devDependencies.push(config.packageName); // install peer dependencies - it's needed for most eslintrc-style shared configs. const peers = await fetchPeerDependencies(config.packageName); - if (peers !== null) { - const eslintIndex = peers.findIndex(dep => (dep.startsWith("eslint@"))); + const eslintIndex = peers.findIndex(dep => (dep.startsWith("eslint@"))); - if (eslintIndex === -1) { - // eslint is not in the peer dependencies + if (eslintIndex === -1) { + // eslint is not in the peer dependencies - this.result.devDependencies.push(...peers); - } else { - const versionMatch = peers[eslintIndex].match(/eslint@(.+)/u); - const versionRequirement = versionMatch[1]; // Complete version requirement string + this.result.devDependencies.push(...peers); + } else { + const versionMatch = peers[eslintIndex].match(/eslint@(.+)/u); + const versionRequirement = versionMatch[1]; // Complete version requirement string - // Check if the version requirement allows for ESLint 9.22.0+ - isDefineConfigExported = !semverGreaterThanRange("9.22.0", versionRequirement); + // Check if the version requirement allows for ESLint 9.22.0+ + isDefineConfigExported = !semverGreaterThanRange("9.22.0", versionRequirement); - // eslint is in the peer dependencies => overwrite eslint version - this.result.devDependencies[0] = peers[eslintIndex]; - peers.splice(eslintIndex, 1); - this.result.devDependencies.push(...peers); - } + // eslint is in the peer dependencies => overwrite eslint version + this.result.devDependencies[0] = peers[eslintIndex]; + peers.splice(eslintIndex, 1); + this.result.devDependencies.push(...peers); } if (config.type === "flat" || config.type === void 0) { - importContent += `import config from "${config.packageName}";\n`; + importContent += `import config from "${name}";\n`; exportContent += " config,\n"; } else if (config.type === "eslintrc") { needCompatHelper = true; - const shorthandName = getShorthandName(config.packageName, "eslint-config"); + const shorthandName = getShorthandName(name, "eslint-config"); exportContent += ` compat.extends("${shorthandName}"),\n`; } From bfbff1c0e1129683e0614a03b8b2e780d7156a89 Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Tue, 14 Oct 2025 10:40:16 +0700 Subject: [PATCH 07/12] refactor --- lib/config-generator.js | 6 +++--- lib/utils/npm-utils.js | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/config-generator.js b/lib/config-generator.js index 6901a4f8..9a5cd2f4 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -287,7 +287,7 @@ export class ConfigGenerator { // passed `--config` if (this.answers.config) { const config = this.answers.config; - const { name } = parsePackageName(config.packageName); + const { name: packageName } = parsePackageName(config.packageName); this.result.devDependencies.push(config.packageName); @@ -314,12 +314,12 @@ export class ConfigGenerator { } if (config.type === "flat" || config.type === void 0) { - importContent += `import config from "${name}";\n`; + importContent += `import config from "${packageName}";\n`; exportContent += " config,\n"; } else if (config.type === "eslintrc") { needCompatHelper = true; - const shorthandName = getShorthandName(name, "eslint-config"); + const shorthandName = getShorthandName(packageName, "eslint-config"); exportContent += ` compat.extends("${shorthandName}"),\n`; } diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index b96bdc68..5b902c61 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -95,6 +95,9 @@ async function fetchPeerDependencies(packageName) { const data = await response.json(); if (data.error) { + if (data.error instanceof Error) { + throw data.error; + } throw new Error(data.error); } From 6970f86b2a8a063802d1328d5a1081bcca25407a Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Tue, 14 Oct 2025 10:42:42 +0700 Subject: [PATCH 08/12] refactor (again) --- lib/utils/npm-utils.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index 5b902c61..b96bdc68 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -95,9 +95,6 @@ async function fetchPeerDependencies(packageName) { const data = await response.json(); if (data.error) { - if (data.error instanceof Error) { - throw data.error; - } throw new Error(data.error); } From 122699f48db6a70754da2ea0883608dfb2950eed Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Tue, 14 Oct 2025 14:50:26 +0700 Subject: [PATCH 09/12] refactor error message --- lib/utils/npm-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index b96bdc68..f8056923 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -95,7 +95,7 @@ async function fetchPeerDependencies(packageName) { const data = await response.json(); if (data.error) { - throw new Error(data.error); + throw new Error(`Unexpected Error when fetching package \`${name}\`: ${data.error}`); } const resolvedVersion = semverMaxSatisfying(Object.keys(data.versions), data["dist-tags"]?.[version] ?? version); From cde48246ebd6f70f639862828a215fd757d40006 Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Sat, 18 Oct 2025 12:54:22 +0700 Subject: [PATCH 10/12] update test --- tests/utils/npm-utils.spec.js | 63 ++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/tests/utils/npm-utils.spec.js b/tests/utils/npm-utils.spec.js index 27f80a2e..c72f5ac5 100644 --- a/tests/utils/npm-utils.spec.js +++ b/tests/utils/npm-utils.spec.js @@ -270,6 +270,67 @@ describe("npmUtils", () => { fetchStub.restore(); }); + it("should handle package with version tag", async () => { + const stub = sinon.stub(globalThis, "fetch"); + + const mockResponse = { + json: sinon.stub().resolves({ + "dist-tags": { latest: "9.0.0" }, + versions: { + "9.0.0": { + peerDependencies: { eslint: "9.0.0" } + }, + "8.0.0": { + peerDependencies: { eslint: "8.0.0" } + }, + "7.0.0": { + peerDependencies: { eslint: "7.0.0" } + } + } + }), + ok: true, + status: 200 + }; + + stub.resolves(mockResponse); + + await expect(fetchPeerDependencies("desired-package@8")).resolves.toEqual(["eslint@8.0.0"]); + + stub.restore(); + }); + + it("should handle package with dist tag", async () => { + const stub = sinon.stub(globalThis, "fetch"); + + const mockResponse = { + json: sinon.stub().resolves({ + "dist-tags": { + latest: "9.0.0", + legacy: "7.0.0" + }, + versions: { + "9.0.0": { + peerDependencies: { eslint: "9.0.0" } + }, + "8.0.0": { + peerDependencies: { eslint: "8.0.0" } + }, + "7.0.0": { + peerDependencies: { eslint: "7.0.0" } + } + } + }), + ok: true, + status: 200 + }; + + stub.resolves(mockResponse); + + await expect(fetchPeerDependencies("desired-package@legacy")).resolves.toEqual(["eslint@7.0.0"]); + + stub.restore(); + }); + it("should throw if an error is thrown", async () => { const stub = sinon.stub(globalThis, "fetch"); @@ -281,7 +342,7 @@ describe("npmUtils", () => { stub.resolves(mockResponse); - expect(async () => await fetchPeerDependencies("desired-package")).rejects.toThrowError(); + await expect(() => fetchPeerDependencies("desired-package")).rejects.toThrowError(); stub.restore(); }); From 73bea018d464fd4ada101ae154bf08f21e1dbcea Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Sun, 19 Oct 2025 22:10:48 +0700 Subject: [PATCH 11/12] revert error message --- lib/utils/npm-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index f8056923..b96bdc68 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -95,7 +95,7 @@ async function fetchPeerDependencies(packageName) { const data = await response.json(); if (data.error) { - throw new Error(`Unexpected Error when fetching package \`${name}\`: ${data.error}`); + throw new Error(data.error); } const resolvedVersion = semverMaxSatisfying(Object.keys(data.versions), data["dist-tags"]?.[version] ?? version); From f7fedc3829897d35e48cc7ce96892eee9414bcff Mon Sep 17 00:00:00 2001 From: hyperz111 Date: Mon, 20 Oct 2025 07:57:03 +0700 Subject: [PATCH 12/12] clean comments --- lib/utils/npm-utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/npm-utils.js b/lib/utils/npm-utils.js index b96bdc68..1b7d600a 100644 --- a/lib/utils/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -90,7 +90,7 @@ async function fetchPeerDependencies(packageName) { const { name, version } = parsePackageName(packageName); try { - // eslint-disable-next-line n/no-unsupported-features/node-builtins -- Fallback using built-in fetch + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- Using built-in fetch const response = await fetch(`https://registry.npmjs.org/${name}`); const data = await response.json(); @@ -110,7 +110,7 @@ async function fetchPeerDependencies(packageName) { ([pkgName, pkgVersion]) => `${pkgName}@${pkgVersion}` ); } catch (err) { - // eslint-disable-next-line preserve-caught-error -- Throw error again :) + // eslint-disable-next-line preserve-caught-error -- Throw error throw new Error(`Cannot fetch "${name}@${version}" with error: ${err.message || err}`); } }