From 14411e3afa37f389129005e0ca2073cd634a250a Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Fri, 23 Feb 2024 19:23:54 +0100 Subject: [PATCH 01/23] feat: add publish command --- src/bin.ts | 14 +++++++++++++- src/commands.ts | 8 +++++++- test/commands.test.ts | 36 +++++++++++++++++++++++++++++++++++- test/test_utils.ts | 23 ++++++++++++++--------- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 178f1a2..0458715 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -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"; @@ -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 (experimental)"], ])} Options: @@ -48,10 +49,17 @@ ${prettyPrintRow([ ["--yarn", "Use yarn to remove and install packages."], ["--pnpm", "Use pnpm to remove and install packages."], ["--bun", "Use bun to remove and install packages."], + [ + "--dry-run", + "Prepare package, but don't publish when running 'jsr publish'.", + ], ["--verbose", "Show additional debugging information."], ["-h, --help", "Show this help text."], ["--version", "Print the version number."], ])} + +Environment variables: + JSR_URL Use a different registry url for the publish command `); } @@ -80,6 +88,7 @@ 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 }, npm: { type: "boolean", default: false }, yarn: { type: "boolean", default: false }, pnpm: { type: "boolean", default: false }, @@ -134,6 +143,9 @@ if (args.length === 0) { const packages = getPackages(options.positionals); await remove(packages, { pkgManagerName }); }); + } else if (cmd === "publish") { + const dryRun = options.values["dry-run"] ?? false; + run(() => publish(process.cwd(), dryRun)); } else { console.error(kl.red(`Unknown command: ${cmd}`)); console.log(); diff --git a/src/commands.ts b/src/commands.ts index 598e130..ca0c381 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,7 +1,7 @@ import * as path from "node:path"; import * as fs from "node:fs"; import * as kl from "kolorist"; -import { JsrPackage } from "./utils"; +import { JsrPackage, exec } from "./utils"; import { Bun, PkgManagerName, getPkgManager } from "./pkg_manager"; const NPMRC_FILE = ".npmrc"; @@ -92,3 +92,9 @@ export async function remove(packages: JsrPackage[], options: BaseOptions) { console.log(`Removing ${kl.cyan(packages.join(", "))}...`); await pkgManager.remove(packages); } + +export async function publish(cwd: string, dryRun: boolean) { + const args = ["publish"]; + if (dryRun) args.push("--dry-run"); + await exec("deno", args, cwd); +} diff --git a/test/commands.test.ts b/test/commands.test.ts index bbc9f80..851f972 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -1,6 +1,12 @@ import * as path from "path"; import * as fs from "fs"; -import { isDirectory, isFile, runJsr, withTempEnv } from "./test_utils"; +import { + isDirectory, + isFile, + runInTempDir, + runJsr, + withTempEnv, +} from "./test_utils"; import * as assert from "node:assert/strict"; describe("install", () => { @@ -252,3 +258,31 @@ describe("remove", () => { ); }); }); + +// This is experimental and under heavy development on the deno side +describe.skip("publish", () => { + it("should publish a package", async () => { + await runInTempDir(async (dir) => { + const pkgJsonPath = path.join(dir, "package.json"); + const pkgJson = JSON.parse( + await fs.promises.readFile(pkgJsonPath, "utf-8") + ); + pkgJson.exports = { + ".": "./mod.js", + }; + await fs.promises.writeFile( + pkgJsonPath, + JSON.stringify(pkgJson), + "utf-8" + ); + + await fs.promises.writeFile( + path.join(dir, "mod.js"), + "export const value = 42;", + "utf-8" + ); + + await runJsr(["publish", "--dry-run"], dir); + }); + }); +}); diff --git a/test/test_utils.ts b/test/test_utils.ts index e5c53dd..f3a4c74 100644 --- a/test/test_utils.ts +++ b/test/test_utils.ts @@ -26,11 +26,7 @@ export async function runJsr( return await exec(tsNode, [bin, ...args], cwd, env); } -export async function withTempEnv( - args: string[], - fn: (getPkgJson: () => Promise, dir: string) => Promise, - options: { env?: Record } = {} -): Promise { +export async function runInTempDir(fn: (dir: string) => Promise) { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "jsr-cli")); await fs.promises.writeFile( @@ -46,17 +42,26 @@ export async function withTempEnv( ), "utf-8" ); - try { + await fn(dir); + } finally { + await fs.promises.rm(dir, { recursive: true, force: true }); + } +} + +export async function withTempEnv( + args: string[], + fn: (getPkgJson: () => Promise, dir: string) => Promise, + options: { env?: Record } = {} +): Promise { + await runInTempDir(async (dir) => { await runJsr(args, dir, options.env); const pkgJson = async () => JSON.parse( await fs.promises.readFile(path.join(dir, "package.json"), "utf-8") ) as PkgJson; await fn(pkgJson, dir); - } finally { - fs.promises.rm(dir, { recursive: true, force: true }); - } + }); } export async function isDirectory(path: string): Promise { From 367f371e6313fc4736a774ca650fdb8206cff2f5 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 09:19:18 +0100 Subject: [PATCH 02/23] chore: add publish test --- test/commands.test.ts | 22 ++++++++++++++++------ test/test_utils.ts | 38 ++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/test/commands.test.ts b/test/commands.test.ts index 851f972..470c9d0 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -1,11 +1,15 @@ import * as path from "path"; import * as fs from "fs"; import { + DenoJson, + PkgJson, isDirectory, isFile, + readJson, runInTempDir, runJsr, withTempEnv, + writeJson, } from "./test_utils"; import * as assert from "node:assert/strict"; @@ -259,14 +263,11 @@ describe("remove", () => { }); }); -// This is experimental and under heavy development on the deno side -describe.skip("publish", () => { +describe("publish", () => { it("should publish a package", async () => { await runInTempDir(async (dir) => { const pkgJsonPath = path.join(dir, "package.json"); - const pkgJson = JSON.parse( - await fs.promises.readFile(pkgJsonPath, "utf-8") - ); + const pkgJson = await readJson(pkgJsonPath); pkgJson.exports = { ".": "./mod.js", }; @@ -277,11 +278,20 @@ describe.skip("publish", () => { ); await fs.promises.writeFile( - path.join(dir, "mod.js"), + path.join(dir, "mod.ts"), "export const value = 42;", "utf-8" ); + // TODO: Change this once deno supports jsr.json + await writeJson(path.join(dir, "deno.json"), { + name: "@deno/jsr-cli-test", + version: pkgJson.version, + exports: { + ".": "./mod.ts", + }, + }); + await runJsr(["publish", "--dry-run"], dir); }); }); diff --git a/test/test_utils.ts b/test/test_utils.ts index f3a4c74..9b5c96b 100644 --- a/test/test_utils.ts +++ b/test/test_utils.ts @@ -11,6 +11,13 @@ export interface PkgJson { dependencies?: Record; devDependencies?: Record; optionalDependencies?: Record; + exports?: string | Record>; +} + +export interface DenoJson { + name: string; + version: string; + exports: string | Record; } export async function runJsr( @@ -29,19 +36,11 @@ export async function runJsr( export async function runInTempDir(fn: (dir: string) => Promise) { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "jsr-cli")); - await fs.promises.writeFile( - path.join(dir, "package.json"), - JSON.stringify( - { - name: "jsr-test-package", - version: "0.0.1", - license: "MIT", - }, - null, - 2 - ), - "utf-8" - ); + await writeJson(path.join(dir, "package.json"), { + name: "jsr-test-package", + version: "0.0.1", + license: "MIT", + }); try { await fn(dir); } finally { @@ -57,9 +56,7 @@ export async function withTempEnv( await runInTempDir(async (dir) => { await runJsr(args, dir, options.env); const pkgJson = async () => - JSON.parse( - await fs.promises.readFile(path.join(dir, "package.json"), "utf-8") - ) as PkgJson; + readJson(path.join(dir, "package.json")); await fn(pkgJson, dir); }); } @@ -79,3 +76,12 @@ export async function isFile(path: string): Promise { return false; } } + +export async function readJson(file: string): Promise { + const content = await fs.promises.readFile(file, "utf-8"); + return JSON.parse(content); +} + +export async function writeJson(file: string, data: T): Promise { + await fs.promises.writeFile(file, JSON.stringify(data, null, 2), "utf-8"); +} From e7e93f5e0865432b53041961e5e54de078d9d3be Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 09:22:13 +0100 Subject: [PATCH 03/23] chore: fix env passing --- src/utils.ts | 2 +- test/test_utils.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index ed34fbb..6782778 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -153,7 +153,7 @@ export async function exec( cmd: string, args: string[], cwd: string, - env?: Record + env?: Record ) { const cp = spawn(cmd, args, { stdio: "inherit", cwd, shell: true, env }); diff --git a/test/test_utils.ts b/test/test_utils.ts index 9b5c96b..8adfdea 100644 --- a/test/test_utils.ts +++ b/test/test_utils.ts @@ -24,13 +24,12 @@ export async function runJsr( args: string[], cwd: string, env: Record = { - ...process.env, npm_config_user_agent: "npm/", } ) { 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, env); + return await exec(tsNode, [bin, ...args], cwd, { ...process.env, ...env }); } export async function runInTempDir(fn: (dir: string) => Promise) { From 7f42b4922d6a6a223a1a25bb93bd71d167df377c Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 09:34:24 +0100 Subject: [PATCH 04/23] feat: add publish arguments --- src/bin.ts | 32 +++++++++++++++++++++++++------- src/commands.ts | 18 +++++++++++++++--- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 0458715..9dcfc51 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -49,17 +49,28 @@ ${prettyPrintRow([ ["--yarn", "Use yarn to remove and install packages."], ["--pnpm", "Use pnpm to remove and install packages."], ["--bun", "Use bun to remove and install packages."], - [ - "--dry-run", - "Prepare package, but don't publish when running 'jsr publish'.", - ], ["--verbose", "Show additional debugging information."], ["-h, --help", "Show this help text."], ["--version", "Print the version number."], ])} +Publish Options: +${prettyPrintRow([ + [ + "--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: - JSR_URL Use a different registry url for the publish command +${prettyPrintRow([ + ["JSR_URL", "Use a different registry url for the publish command"], +])} `); } @@ -89,6 +100,8 @@ if (args.length === 0) { "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 }, @@ -144,8 +157,13 @@ if (args.length === 0) { await remove(packages, { pkgManagerName }); }); } else if (cmd === "publish") { - const dryRun = options.values["dry-run"] ?? false; - run(() => publish(process.cwd(), dryRun)); + run(() => + publish(process.cwd(), { + 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(); diff --git a/src/commands.ts b/src/commands.ts index ca0c381..db962f7 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -93,8 +93,20 @@ export async function remove(packages: JsrPackage[], options: BaseOptions) { await pkgManager.remove(packages); } -export async function publish(cwd: string, dryRun: boolean) { - const args = ["publish"]; - if (dryRun) args.push("--dry-run"); +export interface PublishOptions { + dryRun: boolean; + allowSlowTypes: boolean; + token: string | undefined; +} + +export async function publish(cwd: string, options: PublishOptions) { + 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("deno", args, cwd); } From ac0207d494656a187b6a4ec994c33ee0817a518d Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 09:35:41 +0100 Subject: [PATCH 05/23] chore: add deno to GH actions --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b5fcc3..29481d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: with: version: 8 - uses: oven-sh/setup-bun@v1 + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x - run: npm i - run: npm run build --if-present @@ -47,6 +50,9 @@ jobs: - uses: pnpm/action-setup@v3 with: version: 8 + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x - run: npm i - run: npm run build --if-present From f8e42519b6e6c387ea830d45928498d426e7a628 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 09:37:06 +0100 Subject: [PATCH 06/23] chore: update help text --- src/bin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin.ts b/src/bin.ts index 9dcfc51..081ee11 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -34,7 +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 (experimental)"], + ["publish", "Publish a package to the JSR registry."], ])} Options: From 7b82ed004a51a924e72366398177f8d8cc64c315 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 09:40:58 +0100 Subject: [PATCH 07/23] drop! add debug logs --- test/test_utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_utils.ts b/test/test_utils.ts index 8adfdea..8418994 100644 --- a/test/test_utils.ts +++ b/test/test_utils.ts @@ -29,6 +29,7 @@ export async function runJsr( ) { const bin = path.join(__dirname, "..", "src", "bin.ts"); const tsNode = path.join(__dirname, "..", "node_modules", ".bin", "ts-node"); + console.log(bin, args); return await exec(tsNode, [bin, ...args], cwd, { ...process.env, ...env }); } From 9bab85b3ff0cf8649dec594ee397f3f723bf9611 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 09:59:05 +0100 Subject: [PATCH 08/23] drop! more logs --- src/utils.ts | 1 + test/test_utils.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 6782778..ac2f7e5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -155,6 +155,7 @@ export async function exec( cwd: string, env?: Record ) { + console.log("spawn", cmd, args); const cp = spawn(cmd, args, { stdio: "inherit", cwd, shell: true, env }); return new Promise((resolve) => { diff --git a/test/test_utils.ts b/test/test_utils.ts index 8418994..8adfdea 100644 --- a/test/test_utils.ts +++ b/test/test_utils.ts @@ -29,7 +29,6 @@ export async function runJsr( ) { const bin = path.join(__dirname, "..", "src", "bin.ts"); const tsNode = path.join(__dirname, "..", "node_modules", ".bin", "ts-node"); - console.log(bin, args); return await exec(tsNode, [bin, ...args], cwd, { ...process.env, ...env }); } From 8865d014e14ca6c5dcf6f2fea4307cf836e5dac5 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 10:02:06 +0100 Subject: [PATCH 09/23] chore: pass dummy token for CI --- src/utils.ts | 1 - test/commands.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index ac2f7e5..6782778 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -155,7 +155,6 @@ export async function exec( cwd: string, env?: Record ) { - console.log("spawn", cmd, args); const cp = spawn(cmd, args, { stdio: "inherit", cwd, shell: true, env }); return new Promise((resolve) => { diff --git a/test/commands.test.ts b/test/commands.test.ts index 470c9d0..0cb36a8 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -263,7 +263,7 @@ describe("remove", () => { }); }); -describe("publish", () => { +describe.only("publish", () => { it("should publish a package", async () => { await runInTempDir(async (dir) => { const pkgJsonPath = path.join(dir, "package.json"); @@ -292,7 +292,7 @@ describe("publish", () => { }, }); - await runJsr(["publish", "--dry-run"], dir); + await runJsr(["publish", "--dry-run", "--token", "dummy-token"], dir); }); }); }); From 257a4d356d43199ff0bb96af97145ad058d7082a Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 10:02:17 +0100 Subject: [PATCH 10/23] chore: remove only --- test/commands.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands.test.ts b/test/commands.test.ts index 0cb36a8..21f345f 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -263,7 +263,7 @@ describe("remove", () => { }); }); -describe.only("publish", () => { +describe("publish", () => { it("should publish a package", async () => { await runInTempDir(async (dir) => { const pkgJsonPath = path.join(dir, "package.json"); From bd4e2178648d95f5ede2076ee874db4d003338e9 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 10:14:02 +0100 Subject: [PATCH 11/23] chore: export publish function --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 3371f1b..bb52eb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; From cde3a676cec5e8f917d13858d7bda42c8ea73b71 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:14:22 +0100 Subject: [PATCH 12/23] feat: download local deno binary in postinstall script --- .gitignore | 5 +- package-lock.json | 16 +++- package.json | 11 ++- scripts/postinstall.ts | 194 +++++++++++++++++++++++++++++++++++++++++ src/bin.ts | 2 + src/commands.ts | 3 +- tsconfig.esm.json | 3 +- tsconfig.scripts.json | 12 +++ 8 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 scripts/postinstall.ts create mode 100644 tsconfig.scripts.json diff --git a/.gitignore b/.gitignore index b9c8819..ce35f66 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,8 @@ node_modules/ dist/ dist-esm/ +dist-scripts/ +.download/ *.log -*.tgz \ No newline at end of file +*.tgz +package/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b172b7b..5be2ff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,11 @@ "": { "name": "jsr", "version": "0.7.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { - "kolorist": "^1.8.0" + "kolorist": "^1.8.0", + "node-stream-zip": "^1.15.0" }, "bin": { "jsr": "dist/bin.js" @@ -942,6 +944,18 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index 5c4af9c..f5985f2 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "scripts": { "test": "mocha -r ts-node/register --extensions ts,tsx --timeout 30000 --watch-files src,test 'test/**/*.test.ts'", "cli": "ts-node src/bin.ts", - "build": "rimraf dist dist-esm && tsc && tsc -p tsconfig.esm.json", - "prepublishOnly": "tsc" + "build": "rimraf dist dist-esm dist-scripts && tsc && tsc -p tsconfig.esm.json && tsc -p tsconfig.scripts.json", + "prepublishOnly": "tsc", + "install": "node dist/postinstall.js" }, "keywords": [ "install", @@ -35,7 +36,8 @@ "license": "MIT", "files": [ "dist/", - "dist-esm/" + "dist-esm/", + "dist-scripts/" ], "devDependencies": { "@types/mocha": "^10.0.6", @@ -46,6 +48,7 @@ "typescript": "^5.3.3" }, "dependencies": { - "kolorist": "^1.8.0" + "kolorist": "^1.8.0", + "node-stream-zip": "^1.15.0" } } diff --git a/scripts/postinstall.ts b/scripts/postinstall.ts new file mode 100644 index 0000000..213480e --- /dev/null +++ b/scripts/postinstall.ts @@ -0,0 +1,194 @@ +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://storage.googleapis.com/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 = { + "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", +}; + +async function getDenoDownloadUrl(): Promise<{ + url: string; + filename: string; +}> { + 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, + }; +} + +(async () => { + const info = await getDenoDownloadUrl(); + + const targetPath = path.join(__dirname, "..", ".download"); + await fs.promises.mkdir(targetPath, { 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(targetPath, 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(targetPath, info.filename); + await fs.promises.rename(tmpFile, file); + + const zip = new StreamZip.async({ file }); + await zip.extract(null, targetPath); + await zip.close(); + + const deno = path.join(targetPath, "deno"); + await fs.promises.chmod(deno, 493); + + // Delete downloaded file + await fs.promises.rm(file); + }, + { max: contentLen } + ); +})(); + +async function withProgressBar( + fn: (tick: (n: number) => void) => Promise, + options: { max: number } +): Promise { + 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(stream: ReadableStream) { + 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) { + 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) { + let timer: NodeJS.Timeout | null = null; + + return () => { + if (timer === null) { + fn(); + timer = setTimeout(() => { + timer = null; + }, delay); + } + }; +} diff --git a/src/bin.ts b/src/bin.ts index 081ee11..2460ce6 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -157,8 +157,10 @@ if (args.length === 0) { await remove(packages, { pkgManagerName }); }); } else if (cmd === "publish") { + const binPath = path.join(__dirname, "..", ".download", "deno"); run(() => publish(process.cwd(), { + binPath, dryRun: options.values["dry-run"] ?? false, allowSlowTypes: options.values["allow-slow-types"] ?? false, token: options.values.token, diff --git a/src/commands.ts b/src/commands.ts index db962f7..602dbde 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -94,6 +94,7 @@ export async function remove(packages: JsrPackage[], options: BaseOptions) { } export interface PublishOptions { + binPath: string; dryRun: boolean; allowSlowTypes: boolean; token: string | undefined; @@ -108,5 +109,5 @@ export async function publish(cwd: string, options: PublishOptions) { 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("deno", args, cwd); + await exec(options.binPath, args, cwd); } diff --git a/tsconfig.esm.json b/tsconfig.esm.json index d35963a..280899e 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -6,5 +6,6 @@ "strict": true, "outDir": "dist-esm/", "declaration": true - } + }, + "include": ["src"] } \ No newline at end of file diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 0000000..4b592ec --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ESNext", + "strict": true, + "outDir": "dist-scripts/", + "declaration": true, + "sourceMap": true, + "isolatedModules": true + }, + "include": ["scripts"] +} \ No newline at end of file From a8a6a35a9372433dbe213d7af164c32c6e4641df Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:15:44 +0100 Subject: [PATCH 13/23] fix: postinstall script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5985f2..2f9aa8f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "cli": "ts-node src/bin.ts", "build": "rimraf dist dist-esm dist-scripts && tsc && tsc -p tsconfig.esm.json && tsc -p tsconfig.scripts.json", "prepublishOnly": "tsc", - "install": "node dist/postinstall.js" + "install": "node dist-scripts/postinstall.js" }, "keywords": [ "install", From 7b0cd2060029f240b12ff5311f234679ac4e17ed Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:15:50 +0100 Subject: [PATCH 14/23] chore: remove deno from ci --- .github/workflows/ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29481d2..0b5fcc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,6 @@ jobs: with: version: 8 - uses: oven-sh/setup-bun@v1 - - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x - run: npm i - run: npm run build --if-present @@ -50,9 +47,6 @@ jobs: - uses: pnpm/action-setup@v3 with: version: 8 - - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x - run: npm i - run: npm run build --if-present From 05d580a4e52c6a350c98e4d958cc024b07fe8130 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:20:45 +0100 Subject: [PATCH 15/23] fix: don't compile postinstall --- package.json | 4 +-- scripts/{postinstall.ts => postinstall.js} | 31 +++++++++++----------- tsconfig.scripts.json | 12 --------- 3 files changed, 18 insertions(+), 29 deletions(-) rename scripts/{postinstall.ts => postinstall.js} (85%) delete mode 100644 tsconfig.scripts.json diff --git a/package.json b/package.json index 2f9aa8f..2777b6b 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "scripts": { "test": "mocha -r ts-node/register --extensions ts,tsx --timeout 30000 --watch-files src,test 'test/**/*.test.ts'", "cli": "ts-node src/bin.ts", - "build": "rimraf dist dist-esm dist-scripts && tsc && tsc -p tsconfig.esm.json && tsc -p tsconfig.scripts.json", + "build": "rimraf dist dist-esm && tsc && tsc -p tsconfig.esm.json", "prepublishOnly": "tsc", - "install": "node dist-scripts/postinstall.js" + "install": "node scripts/postinstall.js" }, "keywords": [ "install", diff --git a/scripts/postinstall.ts b/scripts/postinstall.js similarity index 85% rename from scripts/postinstall.ts rename to scripts/postinstall.js index 213480e..38d463c 100644 --- a/scripts/postinstall.ts +++ b/scripts/postinstall.js @@ -13,7 +13,8 @@ const DENO_CANARY_INFO_URL = // 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 = { +/** @type {Record} */ +const FILENAMES = { "darwin arm64": "deno-aarch64-apple-darwin", "darwin x64": "deno-x86_64-apple-darwin", "linux arm64": "deno-aarch64-unknown-linux-gnu", @@ -21,10 +22,8 @@ const FILENAMES: Record = { "win32 x64": "deno-x86_64-pc-windows-msvc", }; -async function getDenoDownloadUrl(): Promise<{ - url: string; - filename: string; -}> { +/** @returns {Promise<{url: string, filename: string}>} */ +async function getDenoDownloadUrl() { const key = `${process.platform} ${os.arch()}`; if (!(key in FILENAMES)) { throw new Error(`Unsupported platform: ${key}`); @@ -67,7 +66,7 @@ async function getDenoDownloadUrl(): Promise<{ const tmpFile = path.join(targetPath, info.filename + ".part"); const writable = fs.createWriteStream(tmpFile, "utf-8"); - for await (const chunk of streamToAsyncIterable(res.body!)) { + for await (const chunk of streamToAsyncIterable(res.body)) { tick(chunk.length); writable.write(chunk); } @@ -91,10 +90,8 @@ async function getDenoDownloadUrl(): Promise<{ ); })(); -async function withProgressBar( - fn: (tick: (n: number) => void) => Promise, - options: { max: number } -): Promise { +/** @type {(fn: (tick: (n: number) => void) => Promise, options: {max: number}) => Promise} */ +async function withProgressBar(fn, options) { let current = 0; let start = Date.now(); let passed = 0; @@ -132,7 +129,8 @@ async function withProgressBar( } }, 16); - const tick = (n: number) => { + /** @type {(n: number) => void} */ + const tick = (n) => { current += n; printStatus(); }; @@ -145,7 +143,8 @@ async function withProgressBar( return res; } -async function* streamToAsyncIterable(stream: ReadableStream) { +/** @type {(stream: ReadableStream) => AsyncIterable} */ +async function* streamToAsyncIterable(stream) { const reader = stream.getReader(); try { while (true) { @@ -158,7 +157,8 @@ async function* streamToAsyncIterable(stream: ReadableStream) { } } -function humanFileSize(bytes: number, digits = 1) { +/** @type {(bytes: number, digists?: number) => string} */ +function humanFileSize(bytes, digits = 1) { const thresh = 1024; if (Math.abs(bytes) < thresh) { @@ -180,8 +180,9 @@ function humanFileSize(bytes: number, digits = 1) { return `${bytes.toFixed(digits)} ${units[u]}`; } -function throttle(fn: () => void, delay: number) { - let timer: NodeJS.Timeout | null = null; +/** @type {(fn: () => void, delay: number) => () => void} */ +function throttle(fn, delay) { + let timer = null; return () => { if (timer === null) { diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json deleted file mode 100644 index 4b592ec..0000000 --- a/tsconfig.scripts.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "module": "CommonJS", - "target": "ESNext", - "strict": true, - "outDir": "dist-scripts/", - "declaration": true, - "sourceMap": true, - "isolatedModules": true - }, - "include": ["scripts"] -} \ No newline at end of file From 7831c212fe8c9d71db9a6d2cdfd77532bdcb9230 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:22:07 +0100 Subject: [PATCH 16/23] fix: use cjs in script --- scripts/postinstall.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 38d463c..8480578 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -1,10 +1,10 @@ -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 os = require("node:os"); +const fs = require("node:fs"); +const path = require("node:path"); +const util = require("node:util"); +const stream = require("node:stream"); +const kl = require("kolorist"); +const StreamZip = require("node-stream-zip"); const streamFinished = util.promisify(stream.finished); From e58e197300d59d3cc9f6a042670e332e916a0be9 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:23:24 +0100 Subject: [PATCH 17/23] fix: include scripts in tarball --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2777b6b..484a0bd 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "files": [ "dist/", "dist-esm/", - "dist-scripts/" + "scripts/" ], "devDependencies": { "@types/mocha": "^10.0.6", From 7d20cd4cdba602688ba3ca885edf29b79a0a1730 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:24:48 +0100 Subject: [PATCH 18/23] fix: windows binary extension --- scripts/postinstall.js | 5 ++++- src/bin.ts | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 8480578..d101328 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -80,7 +80,10 @@ async function getDenoDownloadUrl() { await zip.extract(null, targetPath); await zip.close(); - const deno = path.join(targetPath, "deno"); + const deno = path.join( + targetPath, + process.platform === "win32" ? "deno.exe" : "deno" + ); await fs.promises.chmod(deno, 493); // Delete downloaded file diff --git a/src/bin.ts b/src/bin.ts index 2460ce6..17ff7d5 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -157,7 +157,12 @@ if (args.length === 0) { await remove(packages, { pkgManagerName }); }); } else if (cmd === "publish") { - const binPath = path.join(__dirname, "..", ".download", "deno"); + const binPath = path.join( + __dirname, + "..", + ".download", + process.platform === "win32" ? "deno.exe" : "deno" + ); run(() => publish(process.cwd(), { binPath, From 252c5d5a995b27d744d35ce6467669a28dc4b8e5 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 12:47:09 +0100 Subject: [PATCH 19/23] chore: update canary version url Co-authored-by: Luca Casonato --- scripts/postinstall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index d101328..cf8271e 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -9,7 +9,7 @@ const StreamZip = require("node-stream-zip"); const streamFinished = util.promisify(stream.finished); const DENO_CANARY_INFO_URL = - "https://storage.googleapis.com/dl.deno.land/canary-latest.txt"; + "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 From ac402d372e0b7b514f395b5ee10f60a0acbebcb4 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 13:39:58 +0100 Subject: [PATCH 20/23] feat: lazily download deno binary for publish --- .gitignore | 1 - package.json | 6 +- src/bin.ts | 9 +-- src/commands.ts | 18 +++++- scripts/postinstall.js => src/download.ts | 71 +++++++++++------------ src/utils.ts | 2 +- test/commands.test.ts | 2 +- 7 files changed, 55 insertions(+), 54 deletions(-) rename scripts/postinstall.js => src/download.ts (71%) diff --git a/.gitignore b/.gitignore index ce35f66..e62f69b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ node_modules/ dist/ dist-esm/ -dist-scripts/ .download/ *.log *.tgz diff --git a/package.json b/package.json index 484a0bd..9c8e344 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "test": "mocha -r ts-node/register --extensions ts,tsx --timeout 30000 --watch-files src,test 'test/**/*.test.ts'", "cli": "ts-node src/bin.ts", "build": "rimraf dist dist-esm && tsc && tsc -p tsconfig.esm.json", - "prepublishOnly": "tsc", - "install": "node scripts/postinstall.js" + "prepublishOnly": "tsc" }, "keywords": [ "install", @@ -36,8 +35,7 @@ "license": "MIT", "files": [ "dist/", - "dist-esm/", - "scripts/" + "dist-esm/" ], "devDependencies": { "@types/mocha": "^10.0.6", diff --git a/src/bin.ts b/src/bin.ts index 17ff7d5..509c5e8 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -157,15 +157,10 @@ if (args.length === 0) { await remove(packages, { pkgManagerName }); }); } else if (cmd === "publish") { - const binPath = path.join( - __dirname, - "..", - ".download", - process.platform === "win32" ? "deno.exe" : "deno" - ); + const binFolder = path.join(__dirname, "..", ".download"); run(() => publish(process.cwd(), { - binPath, + binFolder, dryRun: options.values["dry-run"] ?? false, allowSlowTypes: options.values["allow-slow-types"] ?? false, token: options.values.token, diff --git a/src/commands.ts b/src/commands.ts index 602dbde..f722d6c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,8 +1,9 @@ import * as path from "node:path"; import * as fs from "node:fs"; import * as kl from "kolorist"; -import { JsrPackage, exec } from "./utils"; +import { JsrPackage, exec, fileExists } from "./utils"; import { Bun, PkgManagerName, getPkgManager } from "./pkg_manager"; +import { downloadDeno } from "./download"; const NPMRC_FILE = ".npmrc"; const BUNFIG_FILE = "bunfig.toml"; @@ -94,13 +95,24 @@ export async function remove(packages: JsrPackage[], options: BaseOptions) { } export interface PublishOptions { - binPath: string; + binFolder: string; dryRun: boolean; allowSlowTypes: boolean; token: string | undefined; } export async function publish(cwd: string, options: PublishOptions) { + // Check if deno executable is available, download it if not. + const binPath = path.join( + options.binFolder, + process.platform === "win32" ? "deno.exe" : "deno" + ); + + if (!(await fileExists(binPath))) { + await downloadDeno(binPath); + } + + // Ready to publish now! const args = [ "publish", "--unstable-bare-node-builtins", @@ -109,5 +121,5 @@ export async function publish(cwd: string, options: PublishOptions) { 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(options.binPath, args, cwd); + await exec(binPath, args, cwd); } diff --git a/scripts/postinstall.js b/src/download.ts similarity index 71% rename from scripts/postinstall.js rename to src/download.ts index cf8271e..c12952b 100644 --- a/scripts/postinstall.js +++ b/src/download.ts @@ -1,20 +1,18 @@ -const os = require("node:os"); -const fs = require("node:fs"); -const path = require("node:path"); -const util = require("node:util"); -const stream = require("node:stream"); -const kl = require("kolorist"); -const StreamZip = require("node-stream-zip"); +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"; +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 -/** @type {Record} */ -const FILENAMES = { +const FILENAMES: Record = { "darwin arm64": "deno-aarch64-apple-darwin", "darwin x64": "deno-x86_64-apple-darwin", "linux arm64": "deno-aarch64-unknown-linux-gnu", @@ -22,8 +20,10 @@ const FILENAMES = { "win32 x64": "deno-x86_64-pc-windows-msvc", }; -/** @returns {Promise<{url: string, filename: string}>} */ -async function getDenoDownloadUrl() { +async function getDenoDownloadUrl(): Promise<{ + url: string; + filename: string; +}> { const key = `${process.platform} ${os.arch()}`; if (!(key in FILENAMES)) { throw new Error(`Unsupported platform: ${key}`); @@ -47,11 +47,11 @@ async function getDenoDownloadUrl() { }; } -(async () => { +export async function downloadDeno(binPath: string): Promise { const info = await getDenoDownloadUrl(); + const binFolder = path.dirname(binPath); - const targetPath = path.join(__dirname, "..", ".download"); - await fs.promises.mkdir(targetPath, { recursive: true }); + await fs.promises.mkdir(binFolder, { recursive: true }); const res = await fetch(info.url); const contentLen = Number(res.headers.get("content-length") ?? Infinity); @@ -63,38 +63,37 @@ async function getDenoDownloadUrl() { await withProgressBar( async (tick) => { - const tmpFile = path.join(targetPath, info.filename + ".part"); + const tmpFile = path.join(binFolder, info.filename + ".part"); const writable = fs.createWriteStream(tmpFile, "utf-8"); - for await (const chunk of streamToAsyncIterable(res.body)) { + for await (const chunk of streamToAsyncIterable(res.body!)) { tick(chunk.length); writable.write(chunk); } writable.end(); await streamFinished(writable); - const file = path.join(targetPath, info.filename); + const file = path.join(binFolder, info.filename); await fs.promises.rename(tmpFile, file); const zip = new StreamZip.async({ file }); - await zip.extract(null, targetPath); + await zip.extract(null, binFolder); await zip.close(); - const deno = path.join( - targetPath, - process.platform === "win32" ? "deno.exe" : "deno" - ); - await fs.promises.chmod(deno, 493); + // Mark as executable + await fs.promises.chmod(binPath, 493); // Delete downloaded file await fs.promises.rm(file); }, { max: contentLen } ); -})(); +} -/** @type {(fn: (tick: (n: number) => void) => Promise, options: {max: number}) => Promise} */ -async function withProgressBar(fn, options) { +async function withProgressBar( + fn: (tick: (n: number) => void) => Promise, + options: { max: number } +): Promise { let current = 0; let start = Date.now(); let passed = 0; @@ -132,8 +131,7 @@ async function withProgressBar(fn, options) { } }, 16); - /** @type {(n: number) => void} */ - const tick = (n) => { + const tick = (n: number) => { current += n; printStatus(); }; @@ -146,8 +144,9 @@ async function withProgressBar(fn, options) { return res; } -/** @type {(stream: ReadableStream) => AsyncIterable} */ -async function* streamToAsyncIterable(stream) { +async function* streamToAsyncIterable( + stream: ReadableStream +): AsyncIterable { const reader = stream.getReader(); try { while (true) { @@ -160,8 +159,7 @@ async function* streamToAsyncIterable(stream) { } } -/** @type {(bytes: number, digists?: number) => string} */ -function humanFileSize(bytes, digits = 1) { +function humanFileSize(bytes: number, digits = 1): string { const thresh = 1024; if (Math.abs(bytes) < thresh) { @@ -183,9 +181,8 @@ function humanFileSize(bytes, digits = 1) { return `${bytes.toFixed(digits)} ${units[u]}`; } -/** @type {(fn: () => void, delay: number) => () => void} */ -function throttle(fn, delay) { - let timer = null; +function throttle(fn: () => void, delay: number): () => void { + let timer: NodeJS.Timeout | null = null; return () => { if (timer === null) { diff --git a/src/utils.ts b/src/utils.ts index 6782778..550995f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -58,7 +58,7 @@ export class JsrPackage { } } -async function fileExists(file: string): Promise { +export async function fileExists(file: string): Promise { try { const stat = await fs.promises.stat(file); return stat.isFile(); diff --git a/test/commands.test.ts b/test/commands.test.ts index 21f345f..9d44a3f 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -294,5 +294,5 @@ describe("publish", () => { await runJsr(["publish", "--dry-run", "--token", "dummy-token"], dir); }); - }); + }).timeout(600000); }); From 7cb844a79e357b1632954643063bf4ac3870e441 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 13:44:06 +0100 Subject: [PATCH 21/23] fix : ensure downloaded binaries don't overwrite each other --- src/commands.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands.ts b/src/commands.ts index f722d6c..bbce30e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -105,6 +105,7 @@ export async function publish(cwd: string, options: PublishOptions) { // Check if deno executable is available, download it if not. const binPath = path.join( options.binFolder, + process.platform, process.platform === "win32" ? "deno.exe" : "deno" ); From fefc4032115ba651477a2a842f21aa564a9f273d Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 13:45:45 +0100 Subject: [PATCH 22/23] chore: add more comments --- src/commands.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands.ts b/src/commands.ts index bbce30e..cc2e4ca 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -102,13 +102,16 @@ export interface PublishOptions { } export async function publish(cwd: string, options: PublishOptions) { - // Check if deno executable is available, download it if not. const binPath = path.join( options.binFolder, + // 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))) { await downloadDeno(binPath); } From 3480491188b8ff9940a01e635b096c32ed7a9c55 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Tue, 27 Feb 2024 14:01:38 +0100 Subject: [PATCH 23/23] feat: clean old downloads --- src/commands.ts | 17 +++++++++++++++-- src/download.ts | 14 ++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index cc2e4ca..cad6821 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -3,7 +3,7 @@ import * as fs from "node:fs"; import * as kl from "kolorist"; import { JsrPackage, exec, fileExists } from "./utils"; import { Bun, PkgManagerName, getPkgManager } from "./pkg_manager"; -import { downloadDeno } from "./download"; +import { downloadDeno, getDenoDownloadUrl } from "./download"; const NPMRC_FILE = ".npmrc"; const BUNFIG_FILE = "bunfig.toml"; @@ -102,8 +102,11 @@ export interface PublishOptions { } 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 @@ -113,7 +116,17 @@ export async function publish(cwd: string, options: PublishOptions) { // Check if deno executable is available, download it if not. if (!(await fileExists(binPath))) { - await downloadDeno(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! diff --git a/src/download.ts b/src/download.ts index c12952b..3f534c9 100644 --- a/src/download.ts +++ b/src/download.ts @@ -20,10 +20,13 @@ const FILENAMES: Record = { "win32 x64": "deno-x86_64-pc-windows-msvc", }; -async function getDenoDownloadUrl(): Promise<{ +export interface DownloadInfo { url: string; filename: string; -}> { + version: string; +} + +export async function getDenoDownloadUrl(): Promise { const key = `${process.platform} ${os.arch()}`; if (!(key in FILENAMES)) { throw new Error(`Unsupported platform: ${key}`); @@ -44,11 +47,14 @@ async function getDenoDownloadUrl(): Promise<{ return { url: `https://dl.deno.land/canary/${decodeURI(sha)}/${filename}`, filename, + version: sha, }; } -export async function downloadDeno(binPath: string): Promise { - const info = await getDenoDownloadUrl(); +export async function downloadDeno( + binPath: string, + info: DownloadInfo +): Promise { const binFolder = path.dirname(binPath); await fs.promises.mkdir(binFolder, { recursive: true });