Skip to content

Commit c001f7b

Browse files
feat: add jsr show <pkg> command (#45)
* fix: passed cwd argument not used * feat: add `jsr show <pkg>` command
1 parent a544b01 commit c001f7b

File tree

7 files changed

+224
-51
lines changed

7 files changed

+224
-51
lines changed

src/api.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { JsrPackage } from "./utils";
2+
3+
export const JSR_URL = process.env.JSR_URL ?? "https://jsr.io";
4+
5+
export interface PackageMeta {
6+
scope: string;
7+
name: string;
8+
latest?: string;
9+
description?: string;
10+
versions: Record<string, {}>;
11+
}
12+
13+
export async function getPackageMeta(pkg: JsrPackage): Promise<PackageMeta> {
14+
const url = `${JSR_URL}/@${pkg.scope}/${pkg.name}/meta.json`;
15+
console.log("FETCH", url);
16+
const res = await fetch(url);
17+
if (!res.ok) {
18+
// cancel unconsumed body to avoid memory leak
19+
await res.body?.cancel();
20+
throw new Error(`Received ${res.status} from ${url}`);
21+
}
22+
23+
return (await res.json()) as PackageMeta;
24+
}
25+
26+
export async function getLatestPackageVersion(
27+
pkg: JsrPackage,
28+
): Promise<string> {
29+
const info = await getPackageMeta(pkg);
30+
const { latest } = info;
31+
if (latest === undefined) {
32+
throw new Error(`Unable to find latest version of ${pkg}`);
33+
}
34+
return latest;
35+
}
36+
37+
export interface NpmPackageInfo {
38+
name: string;
39+
description: string;
40+
"dist-tags": { latest: string };
41+
versions: Record<string, {
42+
name: string;
43+
version: string;
44+
description: string;
45+
dist: {
46+
tarball: string;
47+
shasum: string;
48+
integrity: string;
49+
};
50+
dependencies: Record<string, string>;
51+
}>;
52+
time: {
53+
created: string;
54+
modified: string;
55+
[key: string]: string;
56+
};
57+
}
58+
59+
export async function getNpmPackageInfo(
60+
pkg: JsrPackage,
61+
): Promise<NpmPackageInfo> {
62+
const tmpUrl = new URL(`${JSR_URL}/@jsr/${pkg.scope}__${pkg.name}`);
63+
const url = `${tmpUrl.protocol}//npm.${tmpUrl.host}${tmpUrl.pathname}`;
64+
const res = await fetch(url);
65+
if (!res.ok) {
66+
// Cancel unconsumed body to avoid memory leak
67+
await res.body?.cancel();
68+
throw new Error(`Received ${res.status} from ${tmpUrl}`);
69+
}
70+
const json = await res.json();
71+
return json as NpmPackageInfo;
72+
}

src/bin.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import * as kl from "kolorist";
44
import * as fs from "node:fs";
55
import * as path from "node:path";
66
import { parseArgs } from "node:util";
7-
import { install, publish, remove, runScript } from "./commands";
7+
import {
8+
install,
9+
publish,
10+
remove,
11+
runScript,
12+
showPackageInfo,
13+
} from "./commands";
814
import { JsrPackage, JsrPackageNameError, prettyTime, setDebug } from "./utils";
915
import { PkgManagerName } from "./pkg_manager";
1016

@@ -44,6 +50,7 @@ ${
4450
["i, install, add", "Install one or more JSR packages."],
4551
["r, uninstall, remove", "Remove one or more JSR packages."],
4652
["publish", "Publish a package to the JSR registry."],
53+
["info, show, view", "Show package information."],
4754
])
4855
}
4956
@@ -115,25 +122,39 @@ function getPackages(positionals: string[]): JsrPackage[] {
115122
if (args.length === 0) {
116123
printHelp();
117124
process.exit(0);
125+
} else if (args.some((arg) => arg === "-h" || arg === "--help")) {
126+
printHelp();
127+
process.exit(0);
128+
} else if (args.some((arg) => arg === "-v" || arg === "--version")) {
129+
const version = JSON.parse(
130+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
131+
).version as string;
132+
console.log(version);
133+
process.exit(0);
118134
} else {
119135
const cmd = args[0];
120136
// Bypass cli argument validation for publish command. The underlying
121137
// `deno publish` cli is under active development and args may change
122138
// frequently.
123-
if (
124-
cmd === "publish" &&
125-
!args.some(
126-
(arg) =>
127-
arg === "-h" || arg === "--help" || arg === "--version" || arg === "-v",
128-
)
129-
) {
139+
if (cmd === "publish") {
130140
const binFolder = path.join(__dirname, "..", ".download");
131141
run(() =>
132142
publish(process.cwd(), {
133143
binFolder,
134144
publishArgs: args.slice(1),
135145
})
136146
);
147+
} else if (cmd === "view" || cmd === "show" || cmd === "info") {
148+
const pkgName = args[1];
149+
if (pkgName === undefined) {
150+
console.log(kl.red(`Missing package name.`));
151+
printHelp();
152+
process.exit(1);
153+
}
154+
155+
run(async () => {
156+
await showPackageInfo(pkgName);
157+
});
137158
} else {
138159
const options = parseArgs({
139160
args,
@@ -164,16 +185,7 @@ if (args.length === 0) {
164185
setDebug(true);
165186
}
166187

167-
if (options.values.help) {
168-
printHelp();
169-
process.exit(0);
170-
} else if (options.values.version) {
171-
const version = JSON.parse(
172-
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
173-
).version as string;
174-
console.log(version);
175-
process.exit(0);
176-
} else if (options.positionals.length === 0) {
188+
if (options.positionals.length === 0) {
177189
printHelp();
178190
process.exit(0);
179191
}

src/commands.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22
import * as path from "node:path";
33
import * as fs from "node:fs";
44
import * as kl from "kolorist";
5-
import { exec, fileExists, getNewLineChars, JsrPackage } from "./utils";
5+
import {
6+
exec,
7+
fileExists,
8+
getNewLineChars,
9+
JsrPackage,
10+
timeAgo,
11+
} from "./utils";
612
import { Bun, getPkgManager, PkgManagerName, YarnBerry } from "./pkg_manager";
713
import { downloadDeno, getDenoDownloadUrl } from "./download";
14+
import { getNpmPackageInfo, getPackageMeta } from "./api";
815

916
const NPMRC_FILE = ".npmrc";
1017
const BUNFIG_FILE = "bunfig.toml";
@@ -162,6 +169,43 @@ export async function runScript(
162169
script: string,
163170
options: BaseOptions,
164171
) {
165-
const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName);
172+
const pkgManager = await getPkgManager(cwd, options.pkgManagerName);
166173
await pkgManager.runScript(script);
167174
}
175+
176+
export async function showPackageInfo(raw: string) {
177+
const pkg = JsrPackage.from(raw);
178+
179+
const meta = await getPackageMeta(pkg);
180+
if (pkg.version === null) {
181+
if (meta.latest === undefined) {
182+
throw new Error(`Missing latest version for ${pkg}`);
183+
}
184+
pkg.version = meta.latest!;
185+
}
186+
187+
const versionCount = Object.keys(meta.versions).length;
188+
189+
const npmInfo = await getNpmPackageInfo(pkg);
190+
191+
const versionInfo = npmInfo.versions[pkg.version]!;
192+
const time = npmInfo.time[pkg.version];
193+
194+
const publishTime = new Date(time).getTime();
195+
196+
console.log();
197+
console.log(
198+
kl.cyan(`@${pkg.scope}/${pkg.name}@${pkg.version}`) +
199+
` | latest: ${kl.magenta(meta.latest ?? "-")} | versions: ${
200+
kl.magenta(versionCount)
201+
}`,
202+
);
203+
console.log(npmInfo.description);
204+
console.log();
205+
console.log(`npm tarball: ${kl.cyan(versionInfo.dist.tarball)}`);
206+
console.log(`npm integrity: ${kl.cyan(versionInfo.dist.integrity)}`);
207+
console.log();
208+
console.log(
209+
`published: ${kl.magenta(timeAgo(Date.now() - publishTime))}`,
210+
);
211+
}

src/pkg_manager.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
// Copyright 2024 the JSR authors. MIT license.
2+
import { getLatestPackageVersion } from "./api";
23
import { InstallOptions } from "./commands";
34
import { exec, findProjectDir, JsrPackage, logDebug } from "./utils";
45
import * as kl from "kolorist";
56

6-
const JSR_URL = "https://jsr.io";
7-
87
async function execWithLog(cmd: string, args: string[], cwd: string) {
98
console.log(kl.dim(`$ ${cmd} ${args.join(" ")}`));
109
return exec(cmd, args, cwd);
@@ -43,22 +42,6 @@ async function isYarnBerry(cwd: string) {
4342
return true;
4443
}
4544

46-
async function getLatestPackageVersion(pkg: JsrPackage) {
47-
const url = `${JSR_URL}/${pkg}/meta.json`;
48-
const res = await fetch(url);
49-
if (!res.ok) {
50-
// cancel the response body here in order to avoid a potential memory leak in node:
51-
// https://github.com/nodejs/undici/tree/c47e9e06d19cf61b2fa1fcbfb6be39a6e3133cab/docs#specification-compliance
52-
await res.body?.cancel();
53-
throw new Error(`Received ${res.status} from ${url}`);
54-
}
55-
const { latest } = await res.json();
56-
if (!latest) {
57-
throw new Error(`Unable to find latest version of ${pkg}`);
58-
}
59-
return latest;
60-
}
61-
6245
export interface PackageManager {
6346
cwd: string;
6447
install(packages: JsrPackage[], options: InstallOptions): Promise<void>;

src/utils.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ export async function findProjectDir(
132132
}
133133

134134
const PERIODS = {
135+
year: 365 * 24 * 60 * 60 * 1000,
136+
month: 30 * 24 * 60 * 60 * 1000,
137+
week: 7 * 24 * 60 * 60 * 1000,
135138
day: 24 * 60 * 60 * 1000,
136139
hour: 60 * 60 * 1000,
137140
minute: 60 * 1000,
@@ -152,37 +155,66 @@ export function prettyTime(diff: number) {
152155
return diff + "ms";
153156
}
154157

158+
export function timeAgo(diff: number) {
159+
if (diff > PERIODS.year) {
160+
const v = Math.floor(diff / PERIODS.year);
161+
return `${v} year${v > 1 ? "s" : ""} ago`;
162+
} else if (diff > PERIODS.month) {
163+
const v = Math.floor(diff / PERIODS.month);
164+
return `${v} month${v > 1 ? "s" : ""} ago`;
165+
} else if (diff > PERIODS.week) {
166+
const v = Math.floor(diff / PERIODS.week);
167+
return `${v} week${v > 1 ? "s" : ""} ago`;
168+
} else if (diff > PERIODS.day) {
169+
const v = Math.floor(diff / PERIODS.day);
170+
return `${v} day${v > 1 ? "s" : ""} ago`;
171+
} else if (diff > PERIODS.hour) {
172+
const v = Math.floor(diff / PERIODS.hour);
173+
return `${v} hour${v > 1 ? "s" : ""} ago`;
174+
} else if (diff > PERIODS.minute) {
175+
const v = Math.floor(diff / PERIODS.minute);
176+
return `${v} minute${v > 1 ? "s" : ""} ago`;
177+
} else if (diff > PERIODS.seconds) {
178+
const v = Math.floor(diff / PERIODS.seconds);
179+
return `${v} second${v > 1 ? "s" : ""} ago`;
180+
}
181+
182+
return "just now";
183+
}
184+
155185
export async function exec(
156186
cmd: string,
157187
args: string[],
158188
cwd: string,
159189
env?: Record<string, string | undefined>,
160-
captureStdout?: boolean,
190+
captureOutput?: boolean,
161191
) {
162192
const cp = spawn(
163193
cmd,
164194
args.map((arg) => process.platform === "win32" ? `"${arg}"` : `'${arg}'`),
165195
{
166-
stdio: captureStdout ? "pipe" : "inherit",
196+
stdio: captureOutput ? "pipe" : "inherit",
167197
cwd,
168198
shell: true,
169199
env,
170200
},
171201
);
172202

173-
return new Promise<string | undefined>((resolve) => {
174-
let stdoutChunks: string[] | undefined;
203+
let output = "";
175204

176-
if (captureStdout) {
177-
stdoutChunks = [];
178-
cp.stdout?.on("data", (data) => {
179-
stdoutChunks!.push(data);
180-
});
181-
}
205+
if (captureOutput) {
206+
cp.stdout?.on("data", (data) => {
207+
output += data;
208+
});
209+
cp.stderr?.on("data", (data) => {
210+
output += data;
211+
});
212+
}
182213

214+
return new Promise<string>((resolve) => {
183215
cp.on("exit", (code) => {
184-
const stdout = stdoutChunks?.join("");
185-
if (code === 0) resolve(stdout);
216+
console.log(output);
217+
if (code === 0) resolve(output);
186218
else process.exit(code ?? 1);
187219
});
188220
});

test/commands.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as path from "path";
22
import * as fs from "fs";
3+
import * as kl from "kolorist";
34
import {
45
DenoJson,
56
enableYarnBerry,
@@ -546,3 +547,31 @@ describe("run", () => {
546547
});
547548
});
548549
});
550+
551+
describe("show", () => {
552+
it("should show package information", async () => {
553+
const output = await runJsr(
554+
["show", "@std/encoding"],
555+
process.cwd(),
556+
undefined,
557+
true,
558+
);
559+
const txt = kl.stripColors(output);
560+
assert.ok(txt.includes("latest:"));
561+
assert.ok(txt.includes("npm tarball:"));
562+
});
563+
564+
it("can use 'view' alias", async () => {
565+
await runJsr(
566+
["view", "@std/encoding"],
567+
process.cwd(),
568+
);
569+
});
570+
571+
it("can use 'info' alias", async () => {
572+
await runJsr(
573+
["view", "@std/encoding"],
574+
process.cwd(),
575+
);
576+
});
577+
});

0 commit comments

Comments
 (0)