Skip to content

Commit 3fccf98

Browse files
authored
feat: improve shareable config (#208)
1 parent 19c3185 commit 3fccf98

File tree

3 files changed

+111
-75
lines changed

3 files changed

+111
-75
lines changed

lib/config-generator.js

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { writeFile } from "node:fs/promises";
1414
import enquirer from "enquirer";
1515
import semverGreaterThanRange from "semver/ranges/gtr.js";
1616
import semverLessThan from "semver/functions/lt.js";
17-
import { isPackageTypeModule, installSyncSaveDev, fetchPeerDependencies, findPackageJson } from "./utils/npm-utils.js";
17+
import { isPackageTypeModule, installSyncSaveDev, fetchPeerDependencies, findPackageJson, parsePackageName } from "./utils/npm-utils.js";
1818
import { getShorthandName } from "./utils/naming.js";
1919
import * as log from "./utils/logging.js";
2020
import { langQuestions, jsQuestions, mdQuestions, installationQuestions, addJitiQuestion } from "./questions.js";
@@ -287,40 +287,39 @@ export class ConfigGenerator {
287287
// passed `--config`
288288
if (this.answers.config) {
289289
const config = this.answers.config;
290+
const { name: packageName } = parsePackageName(config.packageName);
290291

291292
this.result.devDependencies.push(config.packageName);
292293

293294
// install peer dependencies - it's needed for most eslintrc-style shared configs.
294295
const peers = await fetchPeerDependencies(config.packageName);
295296

296-
if (peers !== null) {
297-
const eslintIndex = peers.findIndex(dep => (dep.startsWith("eslint@")));
297+
const eslintIndex = peers.findIndex(dep => (dep.startsWith("eslint@")));
298298

299-
if (eslintIndex === -1) {
300-
// eslint is not in the peer dependencies
299+
if (eslintIndex === -1) {
300+
// eslint is not in the peer dependencies
301301

302-
this.result.devDependencies.push(...peers);
303-
} else {
304-
const versionMatch = peers[eslintIndex].match(/eslint@(.+)/u);
305-
const versionRequirement = versionMatch[1]; // Complete version requirement string
302+
this.result.devDependencies.push(...peers);
303+
} else {
304+
const versionMatch = peers[eslintIndex].match(/eslint@(.+)/u);
305+
const versionRequirement = versionMatch[1]; // Complete version requirement string
306306

307-
// Check if the version requirement allows for ESLint 9.22.0+
308-
isDefineConfigExported = !semverGreaterThanRange("9.22.0", versionRequirement);
307+
// Check if the version requirement allows for ESLint 9.22.0+
308+
isDefineConfigExported = !semverGreaterThanRange("9.22.0", versionRequirement);
309309

310-
// eslint is in the peer dependencies => overwrite eslint version
311-
this.result.devDependencies[0] = peers[eslintIndex];
312-
peers.splice(eslintIndex, 1);
313-
this.result.devDependencies.push(...peers);
314-
}
310+
// eslint is in the peer dependencies => overwrite eslint version
311+
this.result.devDependencies[0] = peers[eslintIndex];
312+
peers.splice(eslintIndex, 1);
313+
this.result.devDependencies.push(...peers);
315314
}
316315

317316
if (config.type === "flat" || config.type === void 0) {
318-
importContent += `import config from "${config.packageName}";\n`;
317+
importContent += `import config from "${packageName}";\n`;
319318
exportContent += " config,\n";
320319
} else if (config.type === "eslintrc") {
321320
needCompatHelper = true;
322321

323-
const shorthandName = getShorthandName(config.packageName, "eslint-config");
322+
const shorthandName = getShorthandName(packageName, "eslint-config");
324323

325324
exportContent += ` compat.extends("${shorthandName}"),\n`;
326325
}

lib/utils/npm-utils.js

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import fs from "node:fs";
1212
import spawn from "cross-spawn";
1313
import path from "node:path";
1414
import * as log from "./logging.js";
15+
import semverMaxSatisfying from "semver/ranges/max-satisfying.js";
1516

1617
//------------------------------------------------------------------------------
1718
// Helpers
@@ -86,52 +87,32 @@ function parsePackageName(packageName) {
8687
* @returns {Object} Gotten peerDependencies. Returns null if npm was not found.
8788
*/
8889
async function fetchPeerDependencies(packageName) {
89-
const npmProcess = spawn.sync(
90-
"npm",
91-
["show", "--json", packageName, "peerDependencies"],
92-
{ encoding: "utf8" }
93-
);
90+
const { name, version } = parsePackageName(packageName);
9491

95-
const error = npmProcess.error;
92+
try {
93+
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- Using built-in fetch
94+
const response = await fetch(`https://registry.npmjs.org/${name}`);
95+
const data = await response.json();
9696

97-
if (error && error.code === "ENOENT") {
97+
if (data.error) {
98+
throw new Error(data.error);
99+
}
98100

99-
// Fallback to using the npm registry API directly when npm is not available.
100-
const { name, version } = parsePackageName(packageName);
101-
102-
try {
103-
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- Fallback using built-in fetch
104-
const response = await fetch(`https://registry.npmjs.org/${name}`);
105-
const data = await response.json();
106-
107-
const resolvedVersion =
108-
version === "latest" ? data["dist-tags"]?.latest : version;
109-
const packageVersion = data.versions[resolvedVersion];
110-
111-
if (!packageVersion) {
112-
throw new Error(
113-
`Version "${version}" not found for package "${name}".`
114-
);
115-
}
116-
return Object.entries(packageVersion.peerDependencies).map(
117-
([pkgName, pkgVersion]) => `${pkgName}@${pkgVersion}`
118-
);
119-
} catch {
101+
const resolvedVersion = semverMaxSatisfying(Object.keys(data.versions), data["dist-tags"]?.[version] ?? version);
102+
const packageVersion = data.versions[resolvedVersion];
120103

121-
// TODO: should throw an error instead of returning null
122-
return null;
104+
if (!packageVersion) {
105+
throw new Error(
106+
`Version "${version}" not found for package "${name}".`
107+
);
123108
}
109+
return Object.entries(Object(packageVersion.peerDependencies)).map(
110+
([pkgName, pkgVersion]) => `${pkgName}@${pkgVersion}`
111+
);
112+
} catch (err) {
113+
// eslint-disable-next-line preserve-caught-error -- Throw error
114+
throw new Error(`Cannot fetch "${name}@${version}" with error: ${err.message || err}`);
124115
}
125-
const fetchedText = npmProcess.stdout.trim();
126-
127-
const peers = JSON.parse(fetchedText || "{}");
128-
const dependencies = [];
129-
130-
Object.keys(peers).forEach(pkgName => {
131-
dependencies.push(`${pkgName}@${peers[pkgName]}`);
132-
});
133-
134-
return dependencies;
135116
}
136117

137118
/**

tests/utils/npm-utils.spec.js

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
parsePackageName
1919
} from "../../lib/utils/npm-utils.js";
2020
import { defineInMemoryFs } from "../_utils/in-memory-fs.js";
21-
import { assert, describe, afterEach, it } from "vitest";
21+
import { assert, describe, afterEach, it, expect } from "vitest";
2222
import fs from "node:fs";
2323
import process from "node:process";
2424

@@ -241,20 +241,10 @@ describe("npmUtils", () => {
241241
});
242242

243243
describe("fetchPeerDependencies()", () => {
244-
it("should execute 'npm show --json <packageName> peerDependencies' command", async () => {
245-
const stub = sinon.stub(spawn, "sync").returns({ stdout: "" });
246-
247-
await fetchPeerDependencies("desired-package");
248-
assert(stub.calledOnce);
249-
assert.strictEqual(stub.firstCall.args[0], "npm");
250-
assert.deepStrictEqual(stub.firstCall.args[1], ["show", "--json", "desired-package", "peerDependencies"]);
251-
stub.restore();
252-
});
253244

254245
// Skip on Node.js v21 due to a bug where fetch cannot be stubbed
255246
// See: https://github.com/sinonjs/sinon/issues/2590
256-
it.skipIf(process.version.startsWith("v21"))("should fetch peer dependencies from npm registry when npm is not available", async () => {
257-
const npmStub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } });
247+
it.skipIf(process.version.startsWith("v21"))("should fetch peer dependencies from npm registry", async () => {
258248
const fetchStub = sinon.stub(globalThis, "fetch");
259249

260250
const mockResponse = {
@@ -277,16 +267,82 @@ describe("npmUtils", () => {
277267
assert(fetchStub.calledOnceWith("https://registry.npmjs.org/desired-package"));
278268
assert.deepStrictEqual(result, ["[email protected]"]);
279269

280-
npmStub.restore();
281270
fetchStub.restore();
282271
});
283272

284-
it("should return null if an error is thrown", async () => {
285-
const stub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } });
273+
it("should handle package with version tag", async () => {
274+
const stub = sinon.stub(globalThis, "fetch");
275+
276+
const mockResponse = {
277+
json: sinon.stub().resolves({
278+
"dist-tags": { latest: "9.0.0" },
279+
versions: {
280+
"9.0.0": {
281+
peerDependencies: { eslint: "9.0.0" }
282+
},
283+
"8.0.0": {
284+
peerDependencies: { eslint: "8.0.0" }
285+
},
286+
"7.0.0": {
287+
peerDependencies: { eslint: "7.0.0" }
288+
}
289+
}
290+
}),
291+
ok: true,
292+
status: 200
293+
};
294+
295+
stub.resolves(mockResponse);
296+
297+
await expect(fetchPeerDependencies("desired-package@8")).resolves.toEqual(["[email protected]"]);
298+
299+
stub.restore();
300+
});
301+
302+
it("should handle package with dist tag", async () => {
303+
const stub = sinon.stub(globalThis, "fetch");
304+
305+
const mockResponse = {
306+
json: sinon.stub().resolves({
307+
"dist-tags": {
308+
latest: "9.0.0",
309+
legacy: "7.0.0"
310+
},
311+
versions: {
312+
"9.0.0": {
313+
peerDependencies: { eslint: "9.0.0" }
314+
},
315+
"8.0.0": {
316+
peerDependencies: { eslint: "8.0.0" }
317+
},
318+
"7.0.0": {
319+
peerDependencies: { eslint: "7.0.0" }
320+
}
321+
}
322+
}),
323+
ok: true,
324+
status: 200
325+
};
326+
327+
stub.resolves(mockResponse);
328+
329+
await expect(fetchPeerDependencies("desired-package@legacy")).resolves.toEqual(["[email protected]"]);
330+
331+
stub.restore();
332+
});
333+
334+
it("should throw if an error is thrown", async () => {
335+
const stub = sinon.stub(globalThis, "fetch");
336+
337+
const mockResponse = {
338+
json: sinon.stub().resolves({ error: "Not found" }),
339+
ok: false,
340+
status: 404
341+
};
286342

287-
const peerDependencies = await fetchPeerDependencies("desired-package");
343+
stub.resolves(mockResponse);
288344

289-
assert.isNull(peerDependencies);
345+
await expect(() => fetchPeerDependencies("desired-package")).rejects.toThrowError();
290346

291347
stub.restore();
292348
});

0 commit comments

Comments
 (0)