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: add --from-jsr-config option #68

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"jsonc-parser": "^3.2.1",
"kolorist": "^1.8.0",
"node-stream-zip": "^1.15.0"
}
Expand Down
43 changes: 42 additions & 1 deletion src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import {
showPackageInfo,
} from "./commands";
import {
DenoJson,
ExecError,
findProjectDir,
JsrPackage,
JsrPackageNameError,
NpmPackage,
Package,
prettyTime,
readJson,
setDebug,
} from "./utils";
import { PkgManagerName } from "./pkg_manager";
Expand Down Expand Up @@ -74,6 +78,10 @@ ${
["--yarn", "Use yarn to remove and install packages."],
["--pnpm", "Use pnpm to remove and install packages."],
["--bun", "Use bun to remove and install packages."],
[
"--from-jsr-config",
"Install 'jsr:*' and 'npm:*' packages from jsr config file as npm packages.",
],
["--verbose", "Show additional debugging information."],
["-h, --help", "Show this help text."],
["-v, --version", "Print the version number."],
Expand Down Expand Up @@ -174,6 +182,7 @@ if (args.length === 0) {
"save-optional": { type: "boolean", default: false, short: "O" },
"dry-run": { type: "boolean", default: false },
"allow-slow-types": { type: "boolean", default: false },
"from-jsr-config": { type: "boolean", default: false },
token: { type: "string" },
config: { type: "string", short: "c" },
"no-config": { type: "boolean" },
Expand Down Expand Up @@ -211,7 +220,39 @@ if (args.length === 0) {

if (cmd === "i" || cmd === "install" || cmd === "add") {
run(async () => {
const packages = getPackages(options.positionals, true);
const packages: Package[] = getPackages(options.positionals, true);
if (options.values["from-jsr-config"]) {
if (packages.length > 0) {
console.error(
kl.red(
"The flag '--from-jsr-config' cannot be used when package names are passed to the install command.",
),
);
process.exit(1);
}

const projectInfo = await findProjectDir(process.cwd());
const jsrFile = projectInfo.jsrJsonPath || projectInfo.denoJsonPath;
if (jsrFile === null) {
console.error(
`Could not find either jsr.json, jsr.jsonc, deno.json or deno.jsonc file in the project.`,
);
process.exit(1);
}

const json = await readJson<DenoJson>(jsrFile);
if (json.imports !== null && typeof json.imports === "object") {
for (const specifier of Object.values(json.imports)) {
if (specifier.startsWith("jsr:")) {
const raw = specifier.slice("jsr:".length);
packages.push(JsrPackage.from(raw));
} else if (specifier.startsWith("npm:")) {
const raw = specifier.slice("npm:".length);
packages.push(NpmPackage.from(raw));
}
}
}
}

await install(packages, {
mode: options.values["save-dev"]
Expand Down
5 changes: 3 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,
Package,
timeAgo,
} from "./utils";
import { Bun, getPkgManager, PkgManagerName, YarnBerry } from "./pkg_manager";
Expand Down Expand Up @@ -86,10 +87,10 @@ export interface InstallOptions extends BaseOptions {
mode: "dev" | "prod" | "optional";
}

export async function install(packages: JsrPackage[], options: InstallOptions) {
export async function install(packages: Package[], options: InstallOptions) {
const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName);

if (packages.length > 0) {
if (packages.some((pkg) => pkg instanceof JsrPackage)) {
if (pkgManager instanceof Bun) {
// Bun doesn't support reading from .npmrc yet
await setupBunfigToml(pkgManager.cwd);
Expand Down
40 changes: 24 additions & 16 deletions src/pkg_manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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, Package } from "./utils";
import * as kl from "kolorist";

async function execWithLog(cmd: string, args: string[], cwd: string) {
Expand All @@ -21,9 +21,15 @@ function modeToFlagYarn(mode: InstallOptions["mode"]): string {
return mode === "dev" ? "--dev" : mode === "optional" ? "--optional" : "";
}

function toPackageArgs(pkgs: JsrPackage[]): string[] {
function toPackageArgs(pkgs: Package[]): 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 All @@ -44,16 +50,16 @@ async function isYarnBerry(cwd: string) {

export interface PackageManager {
cwd: string;
install(packages: JsrPackage[], options: InstallOptions): Promise<void>;
remove(packages: JsrPackage[]): Promise<void>;
install(packages: Package[], options: InstallOptions): Promise<void>;
remove(packages: Package[]): Promise<void>;
runScript(script: string): Promise<void>;
setConfigValue?(key: string, value: string): Promise<void>;
}

class Npm implements PackageManager {
constructor(public cwd: string) {}

async install(packages: JsrPackage[], options: InstallOptions) {
async install(packages: Package[], options: InstallOptions) {
const args = ["install"];
const mode = modeToFlag(options.mode);
if (mode !== "") {
Expand All @@ -64,7 +70,7 @@ class Npm implements PackageManager {
await execWithLog("npm", args, this.cwd);
}

async remove(packages: JsrPackage[]) {
async remove(packages: Package[]) {
await execWithLog(
"npm",
["remove", ...packages.map((pkg) => pkg.toString())],
Expand All @@ -80,7 +86,7 @@ class Npm implements PackageManager {
class Yarn implements PackageManager {
constructor(public cwd: string) {}

async install(packages: JsrPackage[], options: InstallOptions) {
async install(packages: Package[], options: InstallOptions) {
const args = ["add"];
const mode = modeToFlagYarn(options.mode);
if (mode !== "") {
Expand All @@ -90,7 +96,7 @@ class Yarn implements PackageManager {
await execWithLog("yarn", args, this.cwd);
}

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

export class YarnBerry extends Yarn {
async install(packages: JsrPackage[], options: InstallOptions) {
async install(packages: Package[], options: InstallOptions) {
const args = ["add"];
const mode = modeToFlagYarn(options.mode);
if (mode !== "") {
Expand All @@ -121,10 +127,12 @@ export class YarnBerry extends Yarn {
await execWithLog("yarn", ["config", "set", key, value], this.cwd);
}

private async toPackageArgs(pkgs: JsrPackage[]) {
private async toPackageArgs(pkgs: Package[]) {
// 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,7 +141,7 @@ export class YarnBerry extends Yarn {
class Pnpm implements PackageManager {
constructor(public cwd: string) {}

async install(packages: JsrPackage[], options: InstallOptions) {
async install(packages: Package[], options: InstallOptions) {
const args = ["add"];
const mode = modeToFlag(options.mode);
if (mode !== "") {
Expand All @@ -143,7 +151,7 @@ class Pnpm implements PackageManager {
await execWithLog("pnpm", args, this.cwd);
}

async remove(packages: JsrPackage[]) {
async remove(packages: Package[]) {
await execWithLog(
"yarn",
["remove", ...packages.map((pkg) => pkg.toString())],
Expand All @@ -159,7 +167,7 @@ class Pnpm implements PackageManager {
export class Bun implements PackageManager {
constructor(public cwd: string) {}

async install(packages: JsrPackage[], options: InstallOptions) {
async install(packages: Package[], options: InstallOptions) {
const args = ["add"];
const mode = modeToFlagYarn(options.mode);
if (mode !== "") {
Expand All @@ -169,7 +177,7 @@ export class Bun implements PackageManager {
await execWithLog("bun", args, this.cwd);
}

async remove(packages: JsrPackage[]) {
async remove(packages: Package[]) {
await execWithLog(
"bun",
["remove", ...packages.map((pkg) => pkg.toString())],
Expand Down
69 changes: 68 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from "node:path";
import * as fs from "node:fs";
import { PkgManagerName } from "./pkg_manager";
import { spawn } from "node:child_process";
import * as JSONC from "jsonc-parser";

export let DEBUG = false;
export function setDebug(enabled: boolean) {
Expand All @@ -14,10 +15,12 @@ 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) {
Expand Down Expand Up @@ -59,6 +62,36 @@ 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 !== null ? `@${this.scope}/` : "";
s += this.name;
if (this.version !== null) s += `@${this.version}`;
return s;
}
}

export type Package = JsrPackage | NpmPackage;

export async function fileExists(file: string): Promise<boolean> {
try {
const stat = await fs.promises.stat(file);
Expand All @@ -72,6 +105,8 @@ export interface ProjectInfo {
projectDir: string;
pkgManagerName: PkgManagerName | null;
pkgJsonPath: string | null;
denoJsonPath: string | null;
jsrJsonPath: string | null;
}
export async function findProjectDir(
cwd: string,
Expand All @@ -80,6 +115,8 @@ export async function findProjectDir(
projectDir: cwd,
pkgManagerName: null,
pkgJsonPath: null,
denoJsonPath: null,
jsrJsonPath: null,
},
): Promise<ProjectInfo> {
// Ensure we check for `package.json` first as this defines
Expand All @@ -94,6 +131,29 @@ export async function findProjectDir(
}
}

if (result.denoJsonPath === null) {
const denoJsonPath = path.join(dir, "deno.json");
const denoJsoncPath = path.join(dir, "deno.jsonc");
if (await fileExists(denoJsonPath)) {
logDebug(`Found deno.json at ${denoJsonPath}`);
result.denoJsonPath = denoJsonPath;
} else if (await fileExists(denoJsoncPath)) {
logDebug(`Found deno.jsonc at ${denoJsoncPath}`);
result.denoJsonPath = denoJsoncPath;
}
}
if (result.jsrJsonPath === null) {
const jsrJsonPath = path.join(dir, "jsr.json");
const jsrJsoncPath = path.join(dir, "jsr.jsonc");
if (await fileExists(jsrJsonPath)) {
logDebug(`Found jsr.json at ${jsrJsonPath}`);
result.jsrJsonPath = jsrJsonPath;
} else if (await fileExists(jsrJsoncPath)) {
logDebug(`Found jsr.jsonc at ${jsrJsoncPath}`);
result.jsrJsonPath = jsrJsoncPath;
}
}

const npmLockfile = path.join(dir, "package-lock.json");
if (await fileExists(npmLockfile)) {
logDebug(`Detected npm from lockfile ${npmLockfile}`);
Expand Down Expand Up @@ -238,7 +298,14 @@ export function getNewLineChars(source: string) {

export async function readJson<T>(file: string): Promise<T> {
const content = await fs.promises.readFile(file, "utf-8");
return JSON.parse(content);
return file.endsWith(".jsonc") ? JSONC.parse(content) : JSON.parse(content);
}

export interface DenoJson {
name?: string;
version?: string;
exports?: string | Record<string, string>;
imports?: Record<string, string>;
}

export interface PkgJson {
Expand Down
Loading
Loading