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 publish command #14

Merged
merged 23 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
14411e3
feat: add publish command
marvinhagemeister Feb 23, 2024
367f371
chore: add publish test
marvinhagemeister Feb 27, 2024
e7e93f5
chore: fix env passing
marvinhagemeister Feb 27, 2024
7f42b49
feat: add publish arguments
marvinhagemeister Feb 27, 2024
ac0207d
chore: add deno to GH actions
marvinhagemeister Feb 27, 2024
f8e4251
chore: update help text
marvinhagemeister Feb 27, 2024
7b82ed0
drop! add debug logs
marvinhagemeister Feb 27, 2024
9bab85b
drop! more logs
marvinhagemeister Feb 27, 2024
8865d01
chore: pass dummy token for CI
marvinhagemeister Feb 27, 2024
257a4d3
chore: remove only
marvinhagemeister Feb 27, 2024
bd4e217
chore: export publish function
marvinhagemeister Feb 27, 2024
cde3a67
feat: download local deno binary in postinstall script
marvinhagemeister Feb 27, 2024
a8a6a35
fix: postinstall script
marvinhagemeister Feb 27, 2024
7b0cd20
chore: remove deno from ci
marvinhagemeister Feb 27, 2024
05d580a
fix: don't compile postinstall
marvinhagemeister Feb 27, 2024
7831c21
fix: use cjs in script
marvinhagemeister Feb 27, 2024
e58e197
fix: include scripts in tarball
marvinhagemeister Feb 27, 2024
7d20cd4
fix: windows binary extension
marvinhagemeister Feb 27, 2024
252c5d5
chore: update canary version url
marvinhagemeister Feb 27, 2024
ac402d3
feat: lazily download deno binary for publish
marvinhagemeister Feb 27, 2024
7cb844a
fix : ensure downloaded binaries don't overwrite each other
marvinhagemeister Feb 27, 2024
fefc403
chore: add more comments
marvinhagemeister Feb 27, 2024
3480491
feat: clean old downloads
marvinhagemeister Feb 27, 2024
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
node_modules/
dist/
dist-esm/
.download/
*.log
*.tgz
*.tgz
package/
16 changes: 15 additions & 1 deletion package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"kolorist": "^1.8.0"
"kolorist": "^1.8.0",
"node-stream-zip": "^1.15.0"
}
}
34 changes: 33 additions & 1 deletion src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as kl from "kolorist";
import * as fs from "node:fs";
import * as path from "node:path";
import { parseArgs } from "node:util";
import { install, remove } from "./commands";
import { install, publish, remove } from "./commands";
import { JsrPackage, JsrPackageNameError, prettyTime, setDebug } from "./utils";
import { PkgManagerName } from "./pkg_manager";

Expand Down Expand Up @@ -34,6 +34,7 @@ Commands:
${prettyPrintRow([
["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."],
])}

Options:
Expand All @@ -52,6 +53,24 @@ ${prettyPrintRow([
["-h, --help", "Show this help text."],
["--version", "Print the version number."],
])}

Publish Options:
${prettyPrintRow([
[
"--token <Token>",
"The API token to use when publishing. If unset, interactive authentication is be used.",
],
[
"--dry-run",
"Prepare the package for publishing performing all checks and validations without uploading.",
],
["--allow-slow-types", "Allow publishing with slow types."],
])}

Environment variables:
${prettyPrintRow([
["JSR_URL", "Use a different registry url for the publish command"],
])}
`);
}

Expand Down Expand Up @@ -80,6 +99,9 @@ if (args.length === 0) {
"save-prod": { type: "boolean", default: true, short: "P" },
"save-dev": { type: "boolean", default: false, short: "D" },
"save-optional": { type: "boolean", default: false, short: "O" },
"dry-run": { type: "boolean", default: false },
"allow-slow-types": { type: "boolean", default: false },
token: { type: "string" },
npm: { type: "boolean", default: false },
yarn: { type: "boolean", default: false },
pnpm: { type: "boolean", default: false },
Expand Down Expand Up @@ -134,6 +156,16 @@ if (args.length === 0) {
const packages = getPackages(options.positionals);
await remove(packages, { pkgManagerName });
});
} else if (cmd === "publish") {
const binFolder = path.join(__dirname, "..", ".download");
run(() =>
publish(process.cwd(), {
binFolder,
dryRun: options.values["dry-run"] ?? false,
allowSlowTypes: options.values["allow-slow-types"] ?? false,
token: options.values.token,
})
);
} else {
console.error(kl.red(`Unknown command: ${cmd}`));
console.log();
Expand Down
50 changes: 49 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as path from "node:path";
import * as fs from "node:fs";
import * as kl from "kolorist";
import { JsrPackage } from "./utils";
import { JsrPackage, exec, fileExists } from "./utils";
import { Bun, PkgManagerName, getPkgManager } from "./pkg_manager";
import { downloadDeno, getDenoDownloadUrl } from "./download";

const NPMRC_FILE = ".npmrc";
const BUNFIG_FILE = "bunfig.toml";
Expand Down Expand Up @@ -92,3 +93,50 @@ export async function remove(packages: JsrPackage[], options: BaseOptions) {
console.log(`Removing ${kl.cyan(packages.join(", "))}...`);
await pkgManager.remove(packages);
}

export interface PublishOptions {
binFolder: string;
dryRun: boolean;
allowSlowTypes: boolean;
token: string | undefined;
}

export async function publish(cwd: string, options: PublishOptions) {
const info = await getDenoDownloadUrl();

const binPath = path.join(
options.binFolder,
info.version,
// Ensure each binary has their own folder to avoid overwriting it
// in case jsr gets added to a project as a dependency where
// developers use multiple OSes
process.platform,
process.platform === "win32" ? "deno.exe" : "deno"
);

// Check if deno executable is available, download it if not.
if (!(await fileExists(binPath))) {
// Clear folder first to get rid of old download artifacts
// to avoid taking up lots of disk space.
try {
await fs.promises.rm(options.binFolder, { recursive: true });
} catch (err) {
if (!(err instanceof Error) || (err as any).code !== "ENOENT") {
throw err;
}
}

await downloadDeno(binPath, info);
}

// Ready to publish now!
const args = [
"publish",
"--unstable-bare-node-builtins",
"--unstable-sloppy-imports",
];
if (options.dryRun) args.push("--dry-run");
if (options.allowSlowTypes) args.push("--allow-slow-types");
if (options.token) args.push("--token", options.token);
await exec(binPath, args, cwd);
}
201 changes: 201 additions & 0 deletions src/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import * as os from "node:os";
import * as fs from "node:fs";
import * as path from "node:path";
import * as util from "node:util";
import * as stream from "node:stream";
import * as kl from "kolorist";
import * as StreamZip from "node-stream-zip";

const streamFinished = util.promisify(stream.finished);

const DENO_CANARY_INFO_URL = "https://dl.deno.land/canary-latest.txt";

// Example: https://github.com/denoland/deno/releases/download/v1.41.0/deno-aarch64-apple-darwin.zip
// Example: https://dl.deno.land/canary/d722de886b85093eeef08d1e9fd6f3193405762d/deno-aarch64-apple-darwin.zip
const FILENAMES: Record<string, string> = {
"darwin arm64": "deno-aarch64-apple-darwin",
"darwin x64": "deno-x86_64-apple-darwin",
"linux arm64": "deno-aarch64-unknown-linux-gnu",
"linux x64": "deno-x86_64-unknown-linux-gnu",
"win32 x64": "deno-x86_64-pc-windows-msvc",
};

export interface DownloadInfo {
url: string;
filename: string;
version: string;
}

export async function getDenoDownloadUrl(): Promise<DownloadInfo> {
const key = `${process.platform} ${os.arch()}`;
if (!(key in FILENAMES)) {
throw new Error(`Unsupported platform: ${key}`);
}

const name = FILENAMES[key];

const res = await fetch(DENO_CANARY_INFO_URL);
if (!res.ok) {
await res.body?.cancel();
throw new Error(
`${res.status}: Unable to retrieve canary version information from ${DENO_CANARY_INFO_URL}.`
);
}
const sha = (await res.text()).trim();

const filename = name + ".zip";
return {
url: `https://dl.deno.land/canary/${decodeURI(sha)}/${filename}`,
filename,
version: sha,
};
}

export async function downloadDeno(
binPath: string,
info: DownloadInfo
): Promise<void> {
const binFolder = path.dirname(binPath);

await fs.promises.mkdir(binFolder, { recursive: true });

const res = await fetch(info.url);
const contentLen = Number(res.headers.get("content-length") ?? Infinity);
if (res.body == null) {
throw new Error(`Unexpected empty body`);
}

console.log(`Downloading JSR binary...`);

await withProgressBar(
async (tick) => {
const tmpFile = path.join(binFolder, info.filename + ".part");
const writable = fs.createWriteStream(tmpFile, "utf-8");

for await (const chunk of streamToAsyncIterable(res.body!)) {
tick(chunk.length);
writable.write(chunk);
}

writable.end();
await streamFinished(writable);
const file = path.join(binFolder, info.filename);
await fs.promises.rename(tmpFile, file);

const zip = new StreamZip.async({ file });
await zip.extract(null, binFolder);
await zip.close();

// Mark as executable
await fs.promises.chmod(binPath, 493);

// Delete downloaded file
await fs.promises.rm(file);
},
{ max: contentLen }
);
}

async function withProgressBar<T>(
fn: (tick: (n: number) => void) => Promise<T>,
options: { max: number }
): Promise<T> {
let current = 0;
let start = Date.now();
let passed = 0;
let logged = false;

const printStatus = throttle(() => {
passed = Date.now() - start;

const minutes = String(Math.floor(passed / 1000 / 60)).padStart(2, "0");
const seconds = String(Math.floor(passed / 1000) % 60).padStart(2, "0");
const time = `[${minutes}:${seconds}]`;
const stats = `${humanFileSize(current)}/${humanFileSize(options.max)}`;

const width = process.stdout.columns;

let s = time;
if (width - time.length - stats.length + 4 > 10) {
const barLength = Math.min(width, 50);
const percent = Math.floor((100 / options.max) * current);

const bar = "#".repeat((barLength / 100) * percent) + ">";
const remaining = kl.blue(
"-".repeat(Math.max(barLength - bar.length, 0))
);
s += ` [${kl.cyan(bar)}${remaining}] `;
}
s += kl.dim(stats);

if (process.stdout.isTTY) {
if (logged) {
process.stdout.write("\r\x1b[K");
}
logged = true;
process.stdout.write(s);
}
}, 16);

const tick = (n: number) => {
current += n;
printStatus();
};
const res = await fn(tick);
if (process.stdout.isTTY) {
process.stdout.write("\n");
} else {
console.log("Download completed");
}
return res;
}

async function* streamToAsyncIterable<T>(
stream: ReadableStream<T>
): AsyncIterable<T> {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}

function humanFileSize(bytes: number, digits = 1): string {
const thresh = 1024;

if (Math.abs(bytes) < thresh) {
return bytes + " B";
}

const units = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
let u = -1;
const r = 10 ** digits;

do {
bytes /= thresh;
++u;
} while (
Math.round(Math.abs(bytes) * r) / r >= thresh &&
u < units.length - 1
);

return `${bytes.toFixed(digits)} ${units[u]}`;
}

function throttle(fn: () => void, delay: number): () => void {
let timer: NodeJS.Timeout | null = null;

return () => {
if (timer === null) {
fn();
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export { install, remove, type InstallOptions } from "./commands";
export {
install,
remove,
type InstallOptions,
publish,
type PublishOptions,
} from "./commands";
export { JsrPackage, JsrPackageNameError } from "./utils";
Loading
Loading