aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-npm/scripts/npm-build.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bun-npm/scripts/npm-build.ts')
-rw-r--r--packages/bun-npm/scripts/npm-build.ts226
1 files changed, 226 insertions, 0 deletions
diff --git a/packages/bun-npm/scripts/npm-build.ts b/packages/bun-npm/scripts/npm-build.ts
new file mode 100644
index 000000000..c8fcb7d86
--- /dev/null
+++ b/packages/bun-npm/scripts/npm-build.ts
@@ -0,0 +1,226 @@
+import type { Endpoints } from "@octokit/types";
+import { fetch, spawn } from "../src/util";
+import type { JSZipObject } from "jszip";
+import { loadAsync } from "jszip";
+import { join } from "node:path";
+import { chmod, read, write } from "../src/util";
+import type { BuildOptions } from "esbuild";
+import { buildSync, formatMessagesSync } from "esbuild";
+import type { Platform } from "../src/platform";
+import { platforms } from "../src/platform";
+
+type Release =
+ Endpoints["GET /repos/{owner}/{repo}/releases/latest"]["response"]["data"];
+
+const npmPackage = "bun";
+const npmOwner = "@oven";
+let npmVersion: string;
+
+const [tag, action] = process.argv.slice(2);
+
+await build(tag);
+if (action === "publish") {
+ await publish();
+} else if (action === "dry-run") {
+ await publish(true);
+} else if (action) {
+ throw new Error(`Unknown action: ${action}`);
+}
+
+async function build(version: string): Promise<void> {
+ const release = await getRelease(version);
+ if (release.tag_name === "canary") {
+ const { tag_name } = await getRelease();
+ const sha = await getSha(tag_name);
+ npmVersion = `${tag_name.replace("bun-v", "")}+canary.${sha}`;
+ } else {
+ npmVersion = release.tag_name.replace("bun-v", "");
+ }
+ await buildBasePackage();
+ for (const platform of platforms) {
+ await buildPackage(release, platform);
+ }
+}
+
+async function publish(dryRun?: boolean): Promise<void> {
+ const npmPackages = platforms.map(({ bin }) => `${npmOwner}/${bin}`);
+ npmPackages.push(npmPackage);
+ for (const npmPackage of npmPackages) {
+ publishPackage(npmPackage, dryRun);
+ }
+}
+
+async function buildBasePackage() {
+ const done = log("Building:", `${npmPackage}@${npmVersion}`);
+ const cwd = join("npm", npmPackage);
+ const define = {
+ npmVersion: `"${npmVersion}"`,
+ npmPackage: `"${npmPackage}"`,
+ npmOwner: `"${npmOwner}"`,
+ };
+ buildJs(join("scripts", "npm-postinstall.ts"), join(cwd, "install.js"), {
+ define,
+ });
+ buildJs(join("scripts", "npm-exec.ts"), join(cwd, "bin", "bun"), {
+ define,
+ banner: {
+ js: "#!/usr/bin/env node",
+ },
+ });
+ const os = [...new Set(platforms.map(({ os }) => os))];
+ const cpu = [...new Set(platforms.map(({ arch }) => arch))];
+ patchJson(join(cwd, "package.json"), {
+ name: npmPackage,
+ version: npmVersion,
+ scripts: {
+ postinstall: "node install.js",
+ },
+ optionalDependencies: Object.fromEntries(
+ platforms.map(({ bin }) => [`${npmOwner}/${bin}`, npmVersion]),
+ ),
+ bin: {
+ bun: "bin/bun",
+ },
+ os,
+ cpu,
+ });
+ done();
+}
+
+async function buildPackage(
+ release: Release,
+ { bin, exe, os, arch }: Platform,
+): Promise<void> {
+ const npmPackage = `${npmOwner}/${bin}`;
+ const done = log("Building:", `${npmPackage}@${npmVersion}`);
+ const asset = release.assets.find(({ name }) => name === `${bin}.zip`);
+ if (!asset) {
+ throw new Error(`No asset found: ${bin}`);
+ }
+ const bun = await extractFromZip(asset.browser_download_url, `${bin}/bun`);
+ const cwd = join("npm", npmPackage);
+ write(join(cwd, exe), await bun.async("arraybuffer"));
+ chmod(join(cwd, exe), 0o755);
+ patchJson(join(cwd, "package.json"), {
+ name: npmPackage,
+ version: npmVersion,
+ preferUnplugged: true,
+ os: [os],
+ cpu: [arch],
+ });
+ done();
+}
+
+function publishPackage(name: string, dryRun?: boolean): void {
+ const done = log(dryRun ? "Dry-run Publishing:" : "Publishing:", name);
+ const { exitCode, stdout, stderr } = spawn(
+ "npm",
+ [
+ "publish",
+ "--access",
+ "public",
+ "--tag",
+ npmVersion.startsWith("canary") ? "canary" : "latest",
+ ...(dryRun ? ["--dry-run"] : []),
+ ],
+ {
+ cwd: join("npm", name),
+ },
+ );
+ if (exitCode === 0) {
+ done();
+ return;
+ }
+ throw new Error(stdout || stderr);
+}
+
+async function extractFromZip(
+ url: string,
+ filename: string,
+): Promise<JSZipObject> {
+ const response = await fetch(url);
+ const buffer = await response.arrayBuffer();
+ const zip = await loadAsync(buffer);
+ for (const [name, file] of Object.entries(zip.files)) {
+ if (!file.dir && name.startsWith(filename)) {
+ return file;
+ }
+ }
+ console.warn("Found files:", Object.keys(zip.files));
+ throw new Error(`File not found: ${filename}`);
+}
+
+async function getRelease(version?: string | null): Promise<Release> {
+ const response = await fetchGithub(
+ version ? `releases/tags/${formatTag(version)}` : `releases/latest`,
+ );
+ return response.json();
+}
+
+async function getSha(version: string): Promise<string> {
+ const response = await fetchGithub(`git/ref/tags/${formatTag(version)}`);
+ const {
+ object,
+ }: Endpoints["GET /repos/{owner}/{repo}/git/ref/{ref}"]["response"]["data"] =
+ await response.json();
+ return object.sha.substring(0, 7);
+}
+
+async function fetchGithub(path: string) {
+ const headers = new Headers();
+ const token = process.env.GITHUB_TOKEN;
+ if (token) {
+ headers.set("Authorization", `Bearer ${token}`);
+ }
+ const url = new URL(path, "https://api.github.com/repos/oven-sh/bun/");
+ return fetch(url.toString());
+}
+
+function formatTag(version: string): string {
+ if (version.startsWith("canary") || version.startsWith("bun-v")) {
+ return version;
+ }
+ return `bun-v${version}`;
+}
+
+function patchJson(path: string, patch: object): void {
+ let value;
+ try {
+ const existing = JSON.parse(read(path));
+ value = {
+ ...existing,
+ ...patch,
+ };
+ } catch {
+ value = patch;
+ }
+ write(path, `${JSON.stringify(value, undefined, 2)}\n`);
+}
+
+function buildJs(src: string, dst: string, options: BuildOptions = {}): void {
+ const { errors } = buildSync({
+ bundle: true,
+ treeShaking: true,
+ keepNames: true,
+ minifySyntax: true,
+ pure: ["console.debug"],
+ platform: "node",
+ target: "es6",
+ format: "cjs",
+ entryPoints: [src],
+ outfile: dst,
+ ...options,
+ });
+ if (errors?.length) {
+ const messages = formatMessagesSync(errors, { kind: "error" });
+ throw new Error(messages.join("\n"));
+ }
+}
+
+function log(...args: any[]): () => void {
+ console.write(Bun.inspect(...args));
+ const start = Date.now();
+ return () => {
+ console.write(` [${(Date.now() - start).toFixed()} ms]\n`);
+ };
+}