diff --git a/src/bin.ts b/src/bin.ts index 5cbf11f..704b42b 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -11,7 +11,13 @@ import { runScript, showPackageInfo, } from "./commands"; -import { JsrPackage, JsrPackageNameError, prettyTime, setDebug } from "./utils"; +import { + JsrPackage, + JsrPackageNameError, + NpmPackage, + prettyTime, + setDebug, +} from "./utils"; import { PkgManagerName } from "./pkg_manager"; const args = process.argv.slice(2); @@ -63,6 +69,7 @@ ${ ], ["-D, --save-dev", "Package will be added to devDependencies."], ["-O, --save-optional", "Package will be added to optionalDependencies."], + ["-g, --global", "Install packages globally."], ["--npm", "Use npm to remove and install packages."], ["--yarn", "Use yarn to remove and install packages."], ["--pnpm", "Use pnpm to remove and install packages."], @@ -105,9 +112,19 @@ ${ `); } -function getPackages(positionals: string[]): JsrPackage[] { +function getPackages(positionals: string[]): Array { const pkgArgs = positionals.slice(1); - const packages = pkgArgs.map((p) => JsrPackage.from(p)); + const packages = pkgArgs.map((p) => { + try { + return JsrPackage.from(p); + } catch (_) { + try { + return NpmPackage.from(p); + } catch (_) { + throw new Error(`Invalid jsr or npm package name: ${p}`); + } + } + }); if (pkgArgs.length === 0) { console.error(kl.red(`Missing packages argument.`)); @@ -167,6 +184,7 @@ if (args.length === 0) { "allow-slow-types": { type: "boolean", default: false }, token: { type: "string" }, config: { type: "string", short: "c" }, + global: { type: "boolean", short: "g" }, "no-config": { type: "boolean" }, check: { type: "string" }, "no-check": { type: "string" }, @@ -207,6 +225,7 @@ if (args.length === 0) { : options.values["save-optional"] ? "optional" : "prod", + global: options.values.global ?? false, pkgManagerName, }); }); diff --git a/src/commands.ts b/src/commands.ts index 82599ee..99d4719 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -7,6 +7,7 @@ import { fileExists, getNewLineChars, JsrPackage, + NpmPackage, timeAgo, } from "./utils"; import { Bun, getPkgManager, PkgManagerName, YarnBerry } from "./pkg_manager"; @@ -84,9 +85,13 @@ export interface BaseOptions { export interface InstallOptions extends BaseOptions { mode: "dev" | "prod" | "optional"; + global: boolean; } -export async function install(packages: JsrPackage[], options: InstallOptions) { +export async function install( + packages: Array, + options: InstallOptions, +) { const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName); if (pkgManager instanceof Bun) { @@ -107,7 +112,10 @@ export async function install(packages: JsrPackage[], options: InstallOptions) { await pkgManager.install(packages, options); } -export async function remove(packages: JsrPackage[], options: BaseOptions) { +export async function remove( + packages: Array, + options: BaseOptions, +) { const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName); console.log(`Removing ${kl.cyan(packages.join(", "))}...`); await pkgManager.remove(packages); diff --git a/src/pkg_manager.ts b/src/pkg_manager.ts index 7e23102..9bce4f7 100644 --- a/src/pkg_manager.ts +++ b/src/pkg_manager.ts @@ -1,7 +1,13 @@ // Copyright 2024 the JSR authors. MIT license. import { getLatestPackageVersion } from "./api"; import { InstallOptions } from "./commands"; -import { exec, findProjectDir, JsrPackage, logDebug } from "./utils"; +import { + exec, + findProjectDir, + JsrPackage, + logDebug, + NpmPackage, +} from "./utils"; import * as kl from "kolorist"; async function execWithLog(cmd: string, args: string[], cwd: string) { @@ -21,9 +27,15 @@ function modeToFlagYarn(mode: InstallOptions["mode"]): string { return mode === "dev" ? "--dev" : mode === "optional" ? "--optional" : ""; } -function toPackageArgs(pkgs: JsrPackage[]): string[] { +function toPackageArgs(pkgs: Array): string[] { return pkgs.map( - (pkg) => `@${pkg.scope}/${pkg.name}@npm:${pkg.toNpmPackage()}`, + (pkg) => { + if (pkg instanceof JsrPackage) { + return `@${pkg.scope}/${pkg.name}@npm:${pkg.toNpmPackage()}`; + } else { + return pkg.toString(); + } + }, ); } @@ -53,18 +65,22 @@ export interface PackageManager { class Npm implements PackageManager { constructor(public cwd: string) {} - async install(packages: JsrPackage[], options: InstallOptions) { + async install( + packages: Array, + options: InstallOptions, + ) { const args = ["install"]; const mode = modeToFlag(options.mode); if (mode !== "") { args.push(mode); } + if (options.global) args.push("--global"); args.push(...toPackageArgs(packages)); await execWithLog("npm", args, this.cwd); } - async remove(packages: JsrPackage[]) { + async remove(packages: Array) { await execWithLog( "npm", ["remove", ...packages.map((pkg) => pkg.toString())], @@ -80,8 +96,13 @@ class Npm implements PackageManager { class Yarn implements PackageManager { constructor(public cwd: string) {} - async install(packages: JsrPackage[], options: InstallOptions) { - const args = ["add"]; + async install( + packages: Array, + options: InstallOptions, + ) { + const args = []; + if (options.global) args.push("global"); + args.push("add"); const mode = modeToFlagYarn(options.mode); if (mode !== "") { args.push(mode); @@ -90,7 +111,7 @@ class Yarn implements PackageManager { await execWithLog("yarn", args, this.cwd); } - async remove(packages: JsrPackage[]) { + async remove(packages: Array) { await execWithLog( "yarn", ["remove", ...packages.map((pkg) => pkg.toString())], @@ -104,7 +125,15 @@ class Yarn implements PackageManager { } export class YarnBerry extends Yarn { - async install(packages: JsrPackage[], options: InstallOptions) { + async install( + packages: Array, + options: InstallOptions, + ) { + if (options.global) { + throw new Error( + `Installing packages globally is not supported in yarn 2.x (berry).`, + ); + } const args = ["add"]; const mode = modeToFlagYarn(options.mode); if (mode !== "") { @@ -121,10 +150,12 @@ export class YarnBerry extends Yarn { await execWithLog("yarn", ["config", "set", key, value], this.cwd); } - private async toPackageArgs(pkgs: JsrPackage[]) { + private async toPackageArgs(pkgs: Array) { // nasty workaround for https://github.com/yarnpkg/berry/issues/1816 await Promise.all(pkgs.map(async (pkg) => { - pkg.version ??= `^${await getLatestPackageVersion(pkg)}`; + if (pkg instanceof JsrPackage) { + pkg.version ??= `^${await getLatestPackageVersion(pkg)}`; + } })); return toPackageArgs(pkgs); } @@ -133,8 +164,12 @@ export class YarnBerry extends Yarn { class Pnpm implements PackageManager { constructor(public cwd: string) {} - async install(packages: JsrPackage[], options: InstallOptions) { + async install( + packages: Array, + options: InstallOptions, + ) { const args = ["add"]; + if (options.global) args.push("--global"); const mode = modeToFlag(options.mode); if (mode !== "") { args.push(mode); @@ -143,7 +178,7 @@ class Pnpm implements PackageManager { await execWithLog("pnpm", args, this.cwd); } - async remove(packages: JsrPackage[]) { + async remove(packages: Array) { await execWithLog( "yarn", ["remove", ...packages.map((pkg) => pkg.toString())], @@ -159,9 +194,13 @@ class Pnpm implements PackageManager { export class Bun implements PackageManager { constructor(public cwd: string) {} - async install(packages: JsrPackage[], options: InstallOptions) { + async install( + packages: Array, + options: InstallOptions, + ) { const args = ["add"]; const mode = modeToFlagYarn(options.mode); + if (options.global) args.push("--global"); if (mode !== "") { args.push(mode); } @@ -169,7 +208,7 @@ export class Bun implements PackageManager { await execWithLog("bun", args, this.cwd); } - async remove(packages: JsrPackage[]) { + async remove(packages: Array) { await execWithLog( "bun", ["remove", ...packages.map((pkg) => pkg.toString())], diff --git a/src/utils.ts b/src/utils.ts index b33478c..91b821d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,13 +14,15 @@ export function logDebug(msg: string) { } } +const EXTRACT_REG_NPM = /^(@([a-z][a-z0-9-]+)\/)?([a-z0-9-]+)(@(.+))?$/; const EXTRACT_REG = /^@([a-z][a-z0-9-]+)\/([a-z0-9-]+)(@(.+))?$/; const EXTRACT_REG_PROXY = /^@jsr\/([a-z][a-z0-9-]+)__([a-z0-9-]+)(@(.+))?$/; export class JsrPackageNameError extends Error {} +export class NpmPackageNameError extends Error {} export class JsrPackage { - static from(input: string) { + static from(input: string): JsrPackage { const exactMatch = input.match(EXTRACT_REG); if (exactMatch !== null) { const scope = exactMatch[1]; @@ -59,6 +61,34 @@ export class JsrPackage { } } +export class NpmPackage { + static from(input: string): NpmPackage { + const match = input.match(EXTRACT_REG_NPM); + if (match === null) { + throw new NpmPackageNameError(`Invalid npm package name: ${input}`); + } + + const scope = match[2] ?? null; + const name = match[3]; + const version = match[5] ?? null; + + return new NpmPackage(scope, name, version); + } + + private constructor( + public scope: string | null, + public name: string, + public version: string | null, + ) {} + + toString() { + let s = this.scope ? `@${this.scope}/` : ""; + s += this.name; + if (this.version !== null) s += `@${this.version}`; + return s; + } +} + export async function fileExists(file: string): Promise { try { const stat = await fs.promises.stat(file); diff --git a/test/unit.test.ts b/test/unit.test.ts index c221d14..c7463ac 100644 --- a/test/unit.test.ts +++ b/test/unit.test.ts @@ -2,7 +2,7 @@ import * as path from "path"; import { runInTempDir } from "./test_utils"; import { setupNpmRc } from "../src/commands"; import * as assert from "assert/strict"; -import { readTextFile, writeTextFile } from "../src/utils"; +import { NpmPackage, readTextFile, writeTextFile } from "../src/utils"; describe("npmrc", () => { it("doesn't overwrite exising jsr mapping", async () => { @@ -35,3 +35,22 @@ describe("npmrc", () => { }); }); }); + +describe("NpmPackage", () => { + it("parse", () => { + assert.equal(NpmPackage.from("foo").toString(), "foo"); + assert.equal(NpmPackage.from("foo-bar").toString(), "foo-bar"); + assert.equal( + NpmPackage.from("@foo-bar/foo-bar").toString(), + "@foo-bar/foo-bar", + ); + assert.equal( + NpmPackage.from("@foo-bar@1.0.0").toString(), + "@foo-bar@1.0.0", + ); + assert.equal( + NpmPackage.from("@foo-bar/baz@1.0.0").toString(), + "@foo-bar/baz@1.0.0", + ); + }); +});