Skip to content

Commit

Permalink
feat: add jsr show <pkg> command
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed Mar 6, 2024
1 parent a1f065c commit b5b5641
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 50 deletions.
72 changes: 72 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { JsrPackage } from "./utils";

export const JSR_URL = process.env.JSR_URL ?? "https://jsr.io";

export interface PackageMeta {
scope: string;
name: string;
latest?: string;
description?: string;
versions: Record<string, {}>;
}

export async function getPackageMeta(pkg: JsrPackage): Promise<PackageMeta> {
const url = `${JSR_URL}/@${pkg.scope}/${pkg.name}/meta.json`;
console.log("FETCH", url);
const res = await fetch(url);
if (!res.ok) {
// cancel unconsumed body to avoid memory leak
await res.body?.cancel();
throw new Error(`Received ${res.status} from ${url}`);
}

return (await res.json()) as PackageMeta;
}

export async function getLatestPackageVersion(
pkg: JsrPackage,
): Promise<string> {
const info = await getPackageMeta(pkg);
const { latest } = info;
if (latest === undefined) {
throw new Error(`Unable to find latest version of ${pkg}`);
}
return latest;
}

export interface NpmPackageInfo {
name: string;
description: string;
"dist-tags": { latest: string };
versions: Record<string, {
name: string;
version: string;
description: string;
dist: {
tarball: string;
shasum: string;
integrity: string;
};
dependencies: Record<string, string>;
}>;
time: {
created: string;
modified: string;
[key: string]: string;
};
}

export async function getNpmPackageInfo(
pkg: JsrPackage,
): Promise<NpmPackageInfo> {
const tmpUrl = new URL(`${JSR_URL}/@jsr/${pkg.scope}__${pkg.name}`);
const url = `${tmpUrl.protocol}//npm.${tmpUrl.host}${tmpUrl.pathname}`;
const res = await fetch(url);
if (!res.ok) {
// Cancel unconsumed body to avoid memory leak
await res.body?.cancel();
throw new Error(`Received ${res.status} from ${tmpUrl}`);
}
const json = await res.json();
return json as NpmPackageInfo;
}
48 changes: 30 additions & 18 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import * as kl from "kolorist";
import * as fs from "node:fs";
import * as path from "node:path";
import { parseArgs } from "node:util";
import { install, publish, remove, runScript } from "./commands";
import {
install,
publish,
remove,
runScript,
showPackageInfo,
} from "./commands";
import { JsrPackage, JsrPackageNameError, prettyTime, setDebug } from "./utils";
import { PkgManagerName } from "./pkg_manager";

Expand Down Expand Up @@ -44,6 +50,7 @@ ${
["i, install, add", "Install one or more JSR packages."],
["r, uninstall, remove", "Remove one or more JSR packages."],
["publish", "Publish a package to the JSR registry."],
["info, show, view", "Show package information."],
])
}
Expand Down Expand Up @@ -115,25 +122,39 @@ function getPackages(positionals: string[]): JsrPackage[] {
if (args.length === 0) {
printHelp();
process.exit(0);
} else if (args.some((arg) => arg === "-h" || arg === "--help")) {
printHelp();
process.exit(0);
} else if (args.some((arg) => arg === "-v" || arg === "--version")) {
const version = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
).version as string;
console.log(version);
process.exit(0);
} else {
const cmd = args[0];
// Bypass cli argument validation for publish command. The underlying
// `deno publish` cli is under active development and args may change
// frequently.
if (
cmd === "publish" &&
!args.some(
(arg) =>
arg === "-h" || arg === "--help" || arg === "--version" || arg === "-v",
)
) {
if (cmd === "publish") {
const binFolder = path.join(__dirname, "..", ".download");
run(() =>
publish(process.cwd(), {
binFolder,
publishArgs: args.slice(1),
})
);
} else if (cmd === "view" || cmd === "show" || cmd === "info") {
const pkgName = args[1];
if (pkgName === undefined) {
console.log(kl.red(`Missing package name.`));
printHelp();
process.exit(1);
}

run(async () => {
await showPackageInfo(pkgName);
});
} else {
const options = parseArgs({
args,
Expand Down Expand Up @@ -164,16 +185,7 @@ if (args.length === 0) {
setDebug(true);
}

if (options.values.help) {
printHelp();
process.exit(0);
} else if (options.values.version) {
const version = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
).version as string;
console.log(version);
process.exit(0);
} else if (options.positionals.length === 0) {
if (options.positionals.length === 0) {
printHelp();
process.exit(0);
}
Expand Down
46 changes: 45 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
import * as path from "node:path";
import * as fs from "node:fs";
import * as kl from "kolorist";
import { exec, fileExists, getNewLineChars, JsrPackage } from "./utils";
import {
exec,
fileExists,
getNewLineChars,
JsrPackage,
timeAgo,
} from "./utils";
import { Bun, getPkgManager, PkgManagerName, YarnBerry } from "./pkg_manager";
import { downloadDeno, getDenoDownloadUrl } from "./download";
import { getNpmPackageInfo, getPackageMeta } from "./api";

const NPMRC_FILE = ".npmrc";
const BUNFIG_FILE = "bunfig.toml";
Expand Down Expand Up @@ -165,3 +172,40 @@ export async function runScript(
const pkgManager = await getPkgManager(cwd, options.pkgManagerName);
await pkgManager.runScript(script);
}

export async function showPackageInfo(raw: string) {
const pkg = JsrPackage.from(raw);

const meta = await getPackageMeta(pkg);
if (pkg.version === null) {
if (meta.latest === undefined) {
throw new Error(`Missing latest version for ${pkg}`);
}
pkg.version = meta.latest!;
}

const versionCount = Object.keys(meta.versions).length;

const npmInfo = await getNpmPackageInfo(pkg);

const versionInfo = npmInfo.versions[pkg.version]!;
const time = npmInfo.time[pkg.version];

const publishTime = new Date(time).getTime();

console.log();
console.log(
kl.cyan(`@${pkg.scope}/${pkg.name}@${pkg.version}`) +
` | latest: ${kl.magenta(meta.latest ?? "-")} | versions: ${
kl.magenta(versionCount)
}`,
);
console.log(npmInfo.description);
console.log();
console.log(`npm tarball: ${kl.cyan(versionInfo.dist.tarball)}`);
console.log(`npm integrity: ${kl.cyan(versionInfo.dist.integrity)}`);
console.log();
console.log(
`published: ${kl.magenta(timeAgo(Date.now() - publishTime))}`,
);
}
19 changes: 1 addition & 18 deletions src/pkg_manager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Copyright 2024 the JSR authors. MIT license.
import { getLatestPackageVersion } from "./api";
import { InstallOptions } from "./commands";
import { exec, findProjectDir, JsrPackage, logDebug } from "./utils";
import * as kl from "kolorist";

const JSR_URL = "https://jsr.io";

async function execWithLog(cmd: string, args: string[], cwd: string) {
console.log(kl.dim(`$ ${cmd} ${args.join(" ")}`));
return exec(cmd, args, cwd);
Expand Down Expand Up @@ -43,22 +42,6 @@ async function isYarnBerry(cwd: string) {
return true;
}

async function getLatestPackageVersion(pkg: JsrPackage) {
const url = `${JSR_URL}/${pkg}/meta.json`;
const res = await fetch(url);
if (!res.ok) {
// cancel the response body here in order to avoid a potential memory leak in node:
// https://github.com/nodejs/undici/tree/c47e9e06d19cf61b2fa1fcbfb6be39a6e3133cab/docs#specification-compliance
await res.body?.cancel();
throw new Error(`Received ${res.status} from ${url}`);
}
const { latest } = await res.json();
if (!latest) {
throw new Error(`Unable to find latest version of ${pkg}`);
}
return latest;
}

export interface PackageManager {
cwd: string;
install(packages: JsrPackage[], options: InstallOptions): Promise<void>;
Expand Down
56 changes: 44 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ export async function findProjectDir(
}

const PERIODS = {
year: 365 * 24 * 60 * 60 * 1000,
month: 30 * 24 * 60 * 60 * 1000,
week: 7 * 24 * 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
Expand All @@ -152,37 +155,66 @@ export function prettyTime(diff: number) {
return diff + "ms";
}

export function timeAgo(diff: number) {
if (diff > PERIODS.year) {
const v = Math.floor(diff / PERIODS.year);
return `${v} year${v > 1 ? "s" : ""} ago`;
} else if (diff > PERIODS.month) {
const v = Math.floor(diff / PERIODS.month);
return `${v} month${v > 1 ? "s" : ""} ago`;
} else if (diff > PERIODS.week) {
const v = Math.floor(diff / PERIODS.week);
return `${v} week${v > 1 ? "s" : ""} ago`;
} else if (diff > PERIODS.day) {
const v = Math.floor(diff / PERIODS.day);
return `${v} day${v > 1 ? "s" : ""} ago`;
} else if (diff > PERIODS.hour) {
const v = Math.floor(diff / PERIODS.hour);
return `${v} hour${v > 1 ? "s" : ""} ago`;
} else if (diff > PERIODS.minute) {
const v = Math.floor(diff / PERIODS.minute);
return `${v} minute${v > 1 ? "s" : ""} ago`;
} else if (diff > PERIODS.seconds) {
const v = Math.floor(diff / PERIODS.seconds);
return `${v} second${v > 1 ? "s" : ""} ago`;
}

return "just now";
}

export async function exec(
cmd: string,
args: string[],
cwd: string,
env?: Record<string, string | undefined>,
captureStdout?: boolean,
captureOutput?: boolean,
) {
const cp = spawn(
cmd,
args.map((arg) => process.platform === "win32" ? `"${arg}"` : `'${arg}'`),
{
stdio: captureStdout ? "pipe" : "inherit",
stdio: captureOutput ? "pipe" : "inherit",
cwd,
shell: true,
env,
},
);

return new Promise<string | undefined>((resolve) => {
let stdoutChunks: string[] | undefined;
let output = "";

if (captureStdout) {
stdoutChunks = [];
cp.stdout?.on("data", (data) => {
stdoutChunks!.push(data);
});
}
if (captureOutput) {
cp.stdout?.on("data", (data) => {
output += data;
});
cp.stderr?.on("data", (data) => {
output += data;
});
}

return new Promise<string>((resolve) => {
cp.on("exit", (code) => {
const stdout = stdoutChunks?.join("");
if (code === 0) resolve(stdout);
console.log(output);
if (code === 0) resolve(output);
else process.exit(code ?? 1);
});
});
Expand Down
29 changes: 29 additions & 0 deletions test/commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as path from "path";
import * as fs from "fs";
import * as kl from "kolorist";
import {
DenoJson,
enableYarnBerry,
Expand Down Expand Up @@ -546,3 +547,31 @@ describe("run", () => {
});
});
});

describe("show", () => {
it("should show package information", async () => {
const output = await runJsr(
["show", "@std/encoding"],
process.cwd(),
undefined,
true,
);
const txt = kl.stripColors(output);
assert.ok(txt.includes("latest:"));
assert.ok(txt.includes("npm tarball:"));
});

it("can use 'view' alias", async () => {
await runJsr(
["view", "@std/encoding"],
process.cwd(),
);
});

it("can use 'info' alias", async () => {
await runJsr(
["view", "@std/encoding"],
process.cwd(),
);
});
});
3 changes: 2 additions & 1 deletion test/test_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ export async function runJsr(
args: string[],
cwd: string,
env: Record<string, string> = {},
captureOutput = false,
) {
const bin = path.join(__dirname, "..", "src", "bin.ts");
const tsNode = path.join(__dirname, "..", "node_modules", ".bin", "ts-node");
return await exec(tsNode, [bin, ...args], cwd, {
...process.env,
npm_config_user_agent: undefined,
...env,
});
}, captureOutput);
}

export async function runInTempDir(fn: (dir: string) => Promise<void>) {
Expand Down

0 comments on commit b5b5641

Please sign in to comment.