Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support -g, --global install arg #53

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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."],
Expand Down Expand Up @@ -105,9 +112,19 @@ ${
`);
}

function getPackages(positionals: string[]): JsrPackage[] {
function getPackages(positionals: string[]): Array<JsrPackage | NpmPackage> {
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.`));
Expand Down Expand Up @@ -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" },
Expand Down Expand Up @@ -207,6 +225,7 @@ if (args.length === 0) {
: options.values["save-optional"]
? "optional"
: "prod",
global: options.values.global ?? false,
pkgManagerName,
});
});
Expand Down
12 changes: 10 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
fileExists,
getNewLineChars,
JsrPackage,
NpmPackage,
timeAgo,
} from "./utils";
import { Bun, getPkgManager, PkgManagerName, YarnBerry } from "./pkg_manager";
Expand Down Expand Up @@ -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<JsrPackage | NpmPackage>,
options: InstallOptions,
) {
const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName);

if (pkgManager instanceof Bun) {
Expand All @@ -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<JsrPackage | NpmPackage>,
options: BaseOptions,
) {
const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName);
console.log(`Removing ${kl.cyan(packages.join(", "))}...`);
await pkgManager.remove(packages);
Expand Down
69 changes: 54 additions & 15 deletions src/pkg_manager.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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<JsrPackage | NpmPackage>): 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();
}
},
);
}

Expand Down Expand Up @@ -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<JsrPackage | NpmPackage>,
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<JsrPackage | NpmPackage>) {
await execWithLog(
"npm",
["remove", ...packages.map((pkg) => pkg.toString())],
Expand All @@ -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<JsrPackage | NpmPackage>,
options: InstallOptions,
) {
const args = [];
if (options.global) args.push("global");
args.push("add");
const mode = modeToFlagYarn(options.mode);
if (mode !== "") {
args.push(mode);
Expand All @@ -90,7 +111,7 @@ class Yarn implements PackageManager {
await execWithLog("yarn", args, this.cwd);
}

async remove(packages: JsrPackage[]) {
async remove(packages: Array<JsrPackage | NpmPackage>) {
await execWithLog(
"yarn",
["remove", ...packages.map((pkg) => pkg.toString())],
Expand All @@ -104,7 +125,15 @@ class Yarn implements PackageManager {
}

export class YarnBerry extends Yarn {
async install(packages: JsrPackage[], options: InstallOptions) {
async install(
packages: Array<JsrPackage | NpmPackage>,
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 !== "") {
Expand All @@ -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<JsrPackage | NpmPackage>) {
// 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);
}
Expand All @@ -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<JsrPackage | NpmPackage>,
options: InstallOptions,
) {
const args = ["add"];
if (options.global) args.push("--global");
const mode = modeToFlag(options.mode);
if (mode !== "") {
args.push(mode);
Expand All @@ -143,7 +178,7 @@ class Pnpm implements PackageManager {
await execWithLog("pnpm", args, this.cwd);
}

async remove(packages: JsrPackage[]) {
async remove(packages: Array<JsrPackage | NpmPackage>) {
await execWithLog(
"yarn",
["remove", ...packages.map((pkg) => pkg.toString())],
Expand All @@ -159,17 +194,21 @@ class Pnpm implements PackageManager {
export class Bun implements PackageManager {
constructor(public cwd: string) {}

async install(packages: JsrPackage[], options: InstallOptions) {
async install(
packages: Array<JsrPackage | NpmPackage>,
options: InstallOptions,
) {
const args = ["add"];
const mode = modeToFlagYarn(options.mode);
if (options.global) args.push("--global");
if (mode !== "") {
args.push(mode);
}
args.push(...toPackageArgs(packages));
await execWithLog("bun", args, this.cwd);
}

async remove(packages: JsrPackage[]) {
async remove(packages: Array<JsrPackage | NpmPackage>) {
await execWithLog(
"bun",
["remove", ...packages.map((pkg) => pkg.toString())],
Expand Down
32 changes: 31 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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<boolean> {
try {
const stat = await fs.promises.stat(file);
Expand Down
21 changes: 20 additions & 1 deletion test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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("@[email protected]").toString(),
"@[email protected]",
);
assert.equal(
NpmPackage.from("@foo-bar/[email protected]").toString(),
"@foo-bar/[email protected]",
);
});
});
Loading