diff --git a/README.md b/README.md index 9131d97..bcb267c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # narn -Never have to switch between npm and yarn commands ever again. +Never have to switch between yarn, npm, and pnpm commands ever again. -`narn` is a CLI that detects whether your current npm package is using npm or yarn. It then spawns the correct one with the correct arguments. The arguments to narn itself are exactly the same as if you're using yarn. The command will be converted to npm's syntax if the current package is managed by npm. +`narn` is a CLI that detects whether your current npm package is using npm, yarn, or pnpm. It then spawns the correct one with the correct arguments. The arguments to narn itself are exactly the same as if you're using yarn. The command will be converted to npm or pnpm's syntax if the current package is managed by npm / pnpm. ## Installation diff --git a/bin/narn.js b/bin/narn.js index e5d742c..51a9a7d 100755 --- a/bin/narn.js +++ b/bin/narn.js @@ -1,6 +1,11 @@ #!/usr/bin/env node const { spawn } = require("child_process"); -const { detectYarn, getYarnArgs, getNpmArgs } = require("../lib/narn-lib.js"); +const { + detectNpm, + detectPnpm, + getYarnArgs, + getNpmArgs +} = require("../lib/narn-lib.js"); const fs = require("fs"); const path = require("path"); @@ -10,8 +15,21 @@ const narnPackageJson = JSON.parse( async function runPackageManager() { const narnArgs = process.argv.slice(2); - const isYarn = await detectYarn(narnArgs); - let command = isYarn ? "yarn" : "npm"; + const [isNpm, isPnpm] = await Promise.all([ + detectNpm(narnArgs), + detectPnpm(narnArgs) + ]); + + let command; + + if (isPnpm) { + command = "pnpm"; + } else if (isNpm) { + command = "npm"; + } else { + command = "yarn"; + } + let commandArgs; const firstArg = narnArgs.length > 0 ? narnArgs[0] : null; @@ -19,7 +37,8 @@ async function runPackageManager() { console.info(`narn version ${narnPackageJson.version}`); commandArgs = narnArgs; } else { - commandArgs = isYarn ? getYarnArgs(narnArgs) : getNpmArgs(narnArgs); + commandArgs = + command === "yarn" ? getYarnArgs(narnArgs) : getNpmArgs(narnArgs, isPnpm); } command = commandArgs.command || command; diff --git a/lib/narn-lib.js b/lib/narn-lib.js index b66b8c5..d7bafc8 100644 --- a/lib/narn-lib.js +++ b/lib/narn-lib.js @@ -3,17 +3,34 @@ const path = require("path"); const minimist = require("minimist"); const validateNpmPackageName = require("validate-npm-package-name"); -exports.detectYarn = function detectYarn(args) { +exports.detectPnpm = function detectNpm(args) { if (args && args.length > 0 && args[0] === "global") { // all global commands go through yarn - return Promise.resolve(true); + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + fs.access( + path.resolve(process.cwd(), "pnpm-lock.yaml"), + fs.constants.F_OK, + noPnpmLock => { + resolve(!Boolean(noPnpmLock)); + } + ); + }); + } +}; + +exports.detectNpm = function detectNpm(args) { + if (args && args.length > 0 && args[0] === "global") { + // all global commands go through yarn + return Promise.resolve(false); } else { return new Promise((resolve, reject) => { fs.access( path.resolve(process.cwd(), "package-lock.json"), fs.constants.F_OK, noPackageLock => { - resolve(Boolean(noPackageLock)); + resolve(!Boolean(noPackageLock)); } ); }); @@ -22,8 +39,8 @@ exports.detectYarn = function detectYarn(args) { exports.getYarnArgs = narnArgs => narnArgs; -exports.getNpmArgs = narnArgs => { - const yarnArgs = minimist(narnArgs, { boolean: ["dev", "D"] }); +exports.getNpmArgs = (narnArgs, isPnpm = false) => { + const yarnArgs = minimist(narnArgs, { boolean: ["dev", "D", "--latest"] }); const yarnCommands = yarnArgs._; const yarnTarget = yarnCommands.length > 0 ? yarnCommands[0] : "install"; const yarnSubCommands = yarnCommands.slice(1); @@ -50,9 +67,18 @@ exports.getNpmArgs = narnArgs => { npmArgs = []; break; case "upgrade-interactive": - result = ["-iu", "&&", "npm", "install"]; - result.command = "ncu"; - return result; + if (isPnpm) { + npmTarget = "update"; + npmArgs = ["--interactive"]; + if (yarnArgs.latest) { + npmArgs.push("--latest"); + } + break; + } else { + result = ["-iu", "&&", "npm", "install"]; + result.command = "ncu"; + return result; + } case "publish": result = []; result.command = "np"; diff --git a/test/detect-package-manager.test.js b/test/detect-package-manager.test.js new file mode 100644 index 0000000..6dc3892 --- /dev/null +++ b/test/detect-package-manager.test.js @@ -0,0 +1,39 @@ +const { detectNpm, detectPnpm } = require("../lib/narn-lib"); +const fs = require("fs"); + +jest.mock("fs", () => ({ + access: jest.fn(), + constants: { + F_OK: 1 + } +})); + +describe("package manager detection", () => { + beforeEach(() => { + fs.access.mockReset(); + }); + + it("properly detects pnpm packages", async () => { + fs.access.mockImplementationOnce((path, mode, errBack) => { + if (path.includes("pnpm-lock.yaml")) { + errBack(false); + } else { + errBack(true); + } + }); + const isPnpm = await detectPnpm(["add", "lodash@1.0.0"]); + expect(isPnpm).toBe(true); + }); + + it("properly detects pnpm packages", async () => { + fs.access.mockImplementationOnce((path, mode, errBack) => { + if (path.includes("package-lock.json")) { + errBack(false); + } else { + errBack(true); + } + }); + const isNpm = await detectNpm(["add", "lodash@1.0.0"]); + expect(isNpm).toBe(true); + }); +}); diff --git a/test/global.test.js b/test/global.test.js index 7fae246..edeb3c0 100644 --- a/test/global.test.js +++ b/test/global.test.js @@ -1,4 +1,4 @@ -const { detectYarn } = require("../lib/narn-lib"); +const { detectNpm } = require("../lib/narn-lib"); const fs = require("fs"); jest.mock("fs", () => ({ @@ -14,20 +14,20 @@ describe("narn global commands", () => { }); it("supports installing global packages", async () => { - const isYarn = await detectYarn(["global", "add", "lodash@1.0.0"]); - expect(isYarn).toBe(true); + const isNpm = await detectNpm(["global", "add", "lodash@1.0.0"]); + expect(isNpm).toBe(false); }); it("supports uninstalling global packages", async () => { - const isYarn = await detectYarn(["global", "remove", "lodash"]); - expect(isYarn).toBe(true); + const isNpm = await detectNpm(["global", "remove", "lodash"]); + expect(isNpm).toBe(false); }); it("doesn't think everything is global", async () => { fs.access.mockImplementationOnce((path, mode, errBack) => { errBack(false); }); - const isYarn = await detectYarn(["add", "lodash@1.0.0"]); - expect(isYarn).toBe(false); + const isNpm = await detectNpm(["add", "lodash@1.0.0"]); + expect(isNpm).toBe(true); }); }); diff --git a/test/upgrade-interactive.test.js b/test/upgrade-interactive.test.js index 0972333..9480c2b 100644 --- a/test/upgrade-interactive.test.js +++ b/test/upgrade-interactive.test.js @@ -12,4 +12,18 @@ describe("narn upgrade-interactive", () => { expected.command = "ncu"; expect(getNpmArgs(["upgrade-interactive", "--latest"])).toEqual(expected); }); + + it("Runs pnpm update for pnpm projects", () => { + const isPnpm = true; + expect(getNpmArgs(["upgrade-interactive"], isPnpm)).toEqual([ + "update", + "--interactive" + ]); + + expect(getNpmArgs(["upgrade-interactive", "--latest"], isPnpm)).toEqual([ + "update", + "--interactive", + "--latest" + ]); + }); });