Compare commits

...

7 Commits

Author SHA1 Message Date
Ashcon Partovi
0eb7142c9f Tweak semver for canary again 2023-01-23 23:37:55 -08:00
Ashcon Partovi
f52831ba42 Prevent bun from being run in slow mode 2023-01-23 23:05:46 -08:00
Ashcon Partovi
ee7fe5892c Use GITHUB_TOKEN 2023-01-23 22:53:01 -08:00
Ashcon Partovi
028b3f0aff Tweak semver for canary 2023-01-23 10:11:07 -08:00
Ashcon Partovi
edd583e481 Update version to latest release 2023-01-22 15:52:17 -08:00
Ashcon Partovi
de90a5d935 Add newlines to patchJson 2023-01-22 15:50:08 -08:00
Ashcon Partovi
43403f693f Add bun-npm package to publish and install Bun via npm 2023-01-22 15:47:25 -08:00
27 changed files with 990 additions and 0 deletions

47
.github/workflows/bun-npm-release.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Release bun to npm
on:
release:
types:
- published
- edited # canary only
workflow_dispatch:
inputs:
tag:
type: string
description: The tag to publish, defaults to 'canary' if empty
default: canary
jobs:
release:
name: Release
runs-on: ubuntu-latest
if: github.repository_owner == 'oven-sh'
defaults:
run:
working-directory: packages/bun-npm
steps:
- id: checkout
name: Checkout
uses: actions/checkout@v3
- id: setup-env
name: Setup Environment
run: |
TAG="${{ github.event.inputs.tag }}"
TAG="${TAG:-"${{ github.event.release.tag_name }}"}"
TAG="${TAG:-"canary"}"
echo "Setup tag: ${TAG}"
echo "TAG=${TAG}" >> ${GITHUB_ENV}
- id: setup-bun
name: Setup Bun
uses: oven-sh/setup-bun@v0.1.8
with:
bun-version: canary
github-token: ${{ secrets.GITHUB_TOKEN }}
- id: bun-install
name: Install Dependencies
run: bun install
- id: bun-run
name: Release
run: bun run npm -- publish "${{ env.TAG }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

5
packages/bun-npm/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.DS_Store
.env
node_modules
/npm/**/bin
/npm/**/*.js

1
packages/bun-npm/.npmrc Normal file
View File

@@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@@ -0,0 +1,15 @@
# bun-npm
Scripts that allow Bun to be installed with `npm install`.
### Running
```sh
bun run npm # build assets for the latest release
bun run npm -- <release> # build assets for the provided release
bun run npm -- <release> [dry-run|publish] # build and publish assets to npm
```
### Credits
- [esbuild](https://github.com/evanw/esbuild), for its npm scripts which this was largely based off of.

BIN
packages/bun-npm/bun.lockb Executable file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
# Bun
This is the macOS arm64 binary for Bun, a fast all-in-one JavaScript runtime. https://bun.sh

View File

@@ -0,0 +1,16 @@
{
"name": "@oven/bun-darwin-aarch64",
"version": "0.5.1",
"description": "This is the macOS arm64 binary for Bun, a fast all-in-one JavaScript runtime.",
"homepage": "https://bun.sh",
"bugs": "https://github.com/oven-sh/issues",
"license": "MIT",
"repository": "https://github.com/oven-sh/bun",
"preferUnplugged": true,
"os": [
"darwin"
],
"cpu": [
"arm64"
]
}

View File

@@ -0,0 +1,5 @@
# Bun
This is the macOS x64 binary for Bun, a fast all-in-one JavaScript runtime. https://bun.sh
_Note: "Baseline" builds are for machines that do not support [AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) instructions._

View File

@@ -0,0 +1,16 @@
{
"name": "@oven/bun-darwin-x64-baseline",
"version": "0.5.1",
"description": "This is the macOS x64 binary for Bun, a fast all-in-one JavaScript runtime.",
"homepage": "https://bun.sh",
"bugs": "https://github.com/oven-sh/issues",
"license": "MIT",
"repository": "https://github.com/oven-sh/bun",
"preferUnplugged": true,
"os": [
"darwin"
],
"cpu": [
"x64"
]
}

View File

@@ -0,0 +1,3 @@
# Bun
This is the macOS x64 binary for Bun, a fast all-in-one JavaScript runtime. https://bun.sh

View File

@@ -0,0 +1,16 @@
{
"name": "@oven/bun-darwin-x64",
"version": "0.5.1",
"description": "This is the macOS x64 binary for Bun, a fast all-in-one JavaScript runtime.",
"homepage": "https://bun.sh",
"bugs": "https://github.com/oven-sh/issues",
"license": "MIT",
"repository": "https://github.com/oven-sh/bun",
"preferUnplugged": true,
"os": [
"darwin"
],
"cpu": [
"x64"
]
}

View File

@@ -0,0 +1,3 @@
# Bun
This is the Linux arm64 binary for Bun, a fast all-in-one JavaScript runtime. https://bun.sh

View File

@@ -0,0 +1,16 @@
{
"name": "@oven/bun-linux-aarch64",
"version": "0.5.1",
"description": "This is the Linux arm64 binary for Bun, a fast all-in-one JavaScript runtime.",
"homepage": "https://bun.sh",
"bugs": "https://github.com/oven-sh/issues",
"license": "MIT",
"repository": "https://github.com/oven-sh/bun",
"preferUnplugged": true,
"os": [
"linux"
],
"cpu": [
"arm64"
]
}

View File

@@ -0,0 +1,5 @@
# Bun
This is the Linux x64 binary for Bun, a fast all-in-one JavaScript runtime. https://bun.sh
_Note: "Baseline" builds are for machines that do not support [AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) instructions._

View File

@@ -0,0 +1,16 @@
{
"name": "@oven/bun-linux-x64-baseline",
"version": "0.5.1",
"description": "This is the Linux x64 binary for Bun, a fast all-in-one JavaScript runtime.",
"homepage": "https://bun.sh",
"bugs": "https://github.com/oven-sh/issues",
"license": "MIT",
"repository": "https://github.com/oven-sh/bun",
"preferUnplugged": true,
"os": [
"linux"
],
"cpu": [
"x64"
]
}

View File

@@ -0,0 +1,3 @@
# Bun
This is the Linux x64 binary for Bun, a fast all-in-one JavaScript runtime. https://bun.sh

View File

@@ -0,0 +1,16 @@
{
"name": "@oven/bun-linux-x64",
"version": "0.5.1",
"description": "This is the Linux x64 binary for Bun, a fast all-in-one JavaScript runtime.",
"homepage": "https://bun.sh",
"bugs": "https://github.com/oven-sh/issues",
"license": "MIT",
"repository": "https://github.com/oven-sh/bun",
"preferUnplugged": true,
"os": [
"linux"
],
"cpu": [
"x64"
]
}

View File

@@ -0,0 +1,31 @@
# Bun
Bun is a fast all-in-one JavaScript runtime. https://bun.sh
### Install
```sh
npm install -g bun
```
### Upgrade
```sh
bun upgrade
```
### Supported Platforms
- [macOS, arm64 (Apple Silicon)](https://www.npmjs.com/package/@oven/bun-darwin-aarch64)
- [macOS, x64](<(https://www.npmjs.com/package/@oven/bun-darwin-x64)>)
- [macOS, x64 (without AVX2 instructions)](https://www.npmjs.com/package/@oven/bun-darwin-x64-baseline)
- [Linux, arm64](https://www.npmjs.com/package/@oven/bun-linux-aarch64)
- [Linux, x64](https://www.npmjs.com/package/@oven/bun-linux-x64)
- [Linux, x64 (without AVX2 instructions)](https://www.npmjs.com/package/@oven/bun-linux-x64-baseline)
- [Windows (using Windows Subsystem for Linux, aka. "WSL")](https://relatablecode.com/how-to-set-up-bun-on-a-windows-machine)
### Future Platforms
- [Windows](https://github.com/oven-sh/bun/issues/43)
- Unix-like variants such as FreeBSD, OpenBSD, etc.
- Android and iOS

View File

@@ -0,0 +1,41 @@
{
"name": "bun",
"version": "0.5.1",
"description": "Bun is a fast all-in-one JavaScript runtime.",
"keywords": [
"bun",
"bun.js",
"node",
"node.js",
"runtime",
"bundler",
"transpiler",
"typescript"
],
"homepage": "https://bun.sh",
"bugs": "https://github.com/oven-sh/issues",
"license": "MIT",
"bin": {
"bun": "bin/bun"
},
"repository": "https://github.com/oven-sh/bun",
"scripts": {
"postinstall": "node install.js"
},
"optionalDependencies": {
"@oven/bun-darwin-aarch64": "0.5.1",
"@oven/bun-darwin-x64": "0.5.1",
"@oven/bun-darwin-x64-baseline": "0.5.1",
"@oven/bun-linux-aarch64": "0.5.1",
"@oven/bun-linux-x64": "0.5.1",
"@oven/bun-linux-x64-baseline": "0.5.1"
},
"os": [
"darwin",
"linux"
],
"cpu": [
"arm64",
"x64"
]
}

View File

@@ -0,0 +1,15 @@
{
"private": true,
"dependencies": {},
"devDependencies": {
"@octokit/types": "^8.1.1",
"bun-types": "^0.4.0",
"prettier": "^2.8.2",
"esbuild": "^0.17.3",
"jszip": "^3.10.1"
},
"scripts": {
"format": "prettier --write src scripts",
"npm": "bun scripts/npm-build.ts"
}
}

View File

@@ -0,0 +1,227 @@
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);
// Note: this needs to be run using canary
npmVersion = `${Bun.version}-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`);
};
}

View File

@@ -0,0 +1,13 @@
import { importBun } from "../src/install";
import { execFileSync } from "child_process";
importBun()
.then((bun) => {
return execFileSync(bun, process.argv.slice(2), {
stdio: "inherit",
});
})
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,10 @@
import { importBun, optimizeBun } from "../src/install";
importBun()
.then((path) => {
optimizeBun(path);
})
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,159 @@
import { fetch, chmod, join, rename, rm, tmp, write, spawn } from "./util";
import { unzipSync } from "zlib";
import type { Platform } from "./platform";
import { os, arch, supportedPlatforms } from "./platform";
declare const npmVersion: string;
declare const npmPackage: string;
declare const npmOwner: string;
export async function importBun(): Promise<string> {
if (!supportedPlatforms.length) {
throw new Error(`Unsupported platform: ${os} ${arch}`);
}
for (const platform of supportedPlatforms) {
try {
return await requireBun(platform);
} catch (error) {
console.debug("requireBun failed", error);
}
}
throw new Error(`Failed to install package "${npmPackage}"`);
}
async function requireBun(platform: Platform): Promise<string> {
const npmPackage = `${npmOwner}/${platform.bin}`;
function resolveBun() {
const exe = require.resolve(join(npmPackage, platform.exe));
const { exitCode, stderr, stdout } = spawn(exe, ["--version"]);
if (exitCode === 0) {
return exe;
}
throw new Error(stderr || stdout);
}
try {
return resolveBun();
} catch (error) {
console.debug("resolveBun failed", error);
console.error(
`Failed to find package "${npmPackage}".`,
`You may have used the "--no-optional" flag when running "npm install".`,
);
}
const cwd = join("node_modules", npmPackage);
try {
installBun(platform, cwd);
} catch (error) {
console.debug("installBun failed", error);
console.error(
`Failed to install package "${npmPackage}" using "npm install".`,
error,
);
try {
await downloadBun(platform, cwd);
} catch (error) {
console.debug("downloadBun failed", error);
console.error(
`Failed to download package "${npmPackage}" from "registry.npmjs.org".`,
error,
);
}
}
return resolveBun();
}
function installBun(platform: Platform, dst: string): void {
const npmPackage = `${npmOwner}/${platform.bin}`;
const cwd = tmp();
try {
write(join(cwd, "package.json"), "{}");
const { exitCode } = spawn(
"npm",
[
"install",
"--loglevel=error",
"--prefer-offline",
"--no-audit",
"--progress=false",
`${npmPackage}@${npmVersion}`,
],
{
cwd,
stdio: "pipe",
env: {
...process.env,
npm_config_global: undefined,
},
},
);
if (exitCode === 0) {
rename(join(cwd, "node_modules", npmPackage), dst);
}
} finally {
try {
rm(cwd);
} catch (error) {
console.debug("rm failed", error);
// There is nothing to do if the directory cannot be cleaned up.
}
}
}
async function downloadBun(platform: Platform, dst: string): Promise<void> {
const response = await fetch(
`https://registry.npmjs.org/${npmOwner}/${platform.bin}/-/${platform.bin}-${npmVersion}.tgz`,
);
const tgz = await response.arrayBuffer();
let buffer: Buffer;
try {
buffer = unzipSync(tgz);
} catch (cause) {
throw new Error("Invalid gzip data", { cause });
}
function str(i: number, n: number): string {
return String.fromCharCode(...buffer.subarray(i, i + n)).replace(
/\0.*$/,
"",
);
}
let offset = 0;
while (offset < buffer.length) {
const name = str(offset, 100).replace("package/", "");
const size = parseInt(str(offset + 124, 12), 8);
offset += 512;
if (!isNaN(size)) {
write(join(dst, name), buffer.subarray(offset, offset + size));
if (name === platform.exe) {
try {
chmod(join(dst, name), 0o755);
} catch (error) {
console.debug("chmod failed", error);
}
}
offset += (size + 511) & ~511;
}
}
}
export function optimizeBun(path: string): void {
if (os === "win32") {
throw new Error(
"You must use Windows Subsystem for Linux, aka. WSL, to run bun. Learn more: https://learn.microsoft.com/en-us/windows/wsl/install",
);
}
const { npm_config_user_agent } = process.env;
if (npm_config_user_agent && /\byarn\//.test(npm_config_user_agent)) {
throw new Error(
"Yarn does not support bun, because it does not allow linking to binaries. To use bun, install using the following command: curl -fsSL https://bun.sh/install | bash",
);
}
try {
rename(path, join(__dirname, "bin", "bun"));
return;
} catch (error) {
console.debug("optimizeBun failed", error);
}
throw new Error(
"Your package manager doesn't seem to support bun. To use bun, install using the following command: curl -fsSL https://bun.sh/install | bash",
);
}

View File

@@ -0,0 +1,100 @@
import { read, spawn } from "./util";
export const os = process.platform;
export const arch =
os === "darwin" && process.arch === "x64" && isRosetta2()
? "arm64"
: process.arch;
export const avx2 =
(arch === "x64" && os === "linux" && isLinuxAVX2()) ||
(os === "darwin" && isDarwinAVX2());
export type Platform = {
os: string;
arch: string;
avx2?: boolean;
bin: string;
exe: string;
};
export const platforms: Platform[] = [
{
os: "darwin",
arch: "arm64",
bin: "bun-darwin-aarch64",
exe: "bin/bun",
},
{
os: "darwin",
arch: "x64",
avx2: true,
bin: "bun-darwin-x64",
exe: "bin/bun",
},
{
os: "darwin",
arch: "x64",
bin: "bun-darwin-x64-baseline",
exe: "bin/bun",
},
{
os: "linux",
arch: "arm64",
bin: "bun-linux-aarch64",
exe: "bin/bun",
},
{
os: "linux",
arch: "x64",
avx2: true,
bin: "bun-linux-x64",
exe: "bin/bun",
},
{
os: "linux",
arch: "x64",
bin: "bun-linux-x64-baseline",
exe: "bin/bun",
},
];
export const supportedPlatforms: Platform[] = platforms
.filter(
(platform) =>
platform.os === os && platform.arch === arch && (!platform.avx2 || avx2),
)
.sort((a, b) => (a.avx2 === b.avx2 ? 0 : a.avx2 ? -1 : 1));
function isLinuxAVX2(): boolean {
try {
return read("/proc/cpuinfo").includes("avx2");
} catch (error) {
console.debug("isLinuxAVX2 failed", error);
return false;
}
}
function isDarwinAVX2(): boolean {
try {
const { exitCode, stdout } = spawn("sysctl", ["-n", "machdep.cpu"]);
return exitCode === 0 && stdout.includes("AVX2");
} catch (error) {
console.debug("isDarwinAVX2 failed", error);
return false;
}
}
function isRosetta2(): boolean {
try {
const { exitCode, stdout } = spawn("sysctl", [
"-n",
"sysctl.proc_translated",
]);
return exitCode === 0 && stdout.includes("1");
} catch (error) {
console.debug("isRosetta2 failed", error);
return false;
}
}

View File

@@ -0,0 +1,191 @@
import fs from "fs";
import path, { dirname } from "path";
import { tmpdir } from "os";
import child_process from "child_process";
if (process.env["DEBUG"] !== "1") {
console.debug = () => {};
}
export function join(...paths: (string | string[])[]): string {
return path.join(...paths.flat(2));
}
export function tmp(): string {
const path = fs.mkdtempSync(join(tmpdir(), "bun-"));
console.debug("tmp", path);
return path;
}
export function rm(path: string): void {
console.debug("rm", path);
try {
fs.rmSync(path, { recursive: true });
return;
} catch (error) {
console.debug("rmSync failed", error);
// Did not exist before Node.js v14.
// Attempt again with older, slower implementation.
}
let stats: fs.Stats;
try {
stats = fs.lstatSync(path);
} catch (error) {
console.debug("lstatSync failed", error);
// The file was likely deleted, so return early.
return;
}
if (!stats.isDirectory()) {
fs.unlinkSync(path);
return;
}
try {
fs.rmdirSync(path, { recursive: true });
return;
} catch (error) {
console.debug("rmdirSync failed", error);
// Recursive flag did not exist before Node.js X.
// Attempt again with older, slower implementation.
}
for (const filename of fs.readdirSync(path)) {
rm(join(path, filename));
}
fs.rmdirSync(path);
}
export function rename(path: string, newPath: string): void {
console.debug("rename", path, newPath);
try {
fs.renameSync(path, newPath);
return;
} catch (error) {
console.debug("renameSync failed", error);
// If there is an error, delete the new path and try again.
}
try {
rm(newPath);
} catch (error) {
console.debug("rm failed", error);
// The path could have been deleted already.
}
fs.renameSync(path, newPath);
}
export function write(
path: string,
content: string | ArrayBuffer | ArrayBufferView,
): void {
console.debug("write", path);
try {
fs.writeFileSync(path, content);
return;
} catch (error) {
console.debug("writeFileSync failed", error);
// If there is an error, ensure the parent directory
// exists and try again.
try {
fs.mkdirSync(dirname(path), { recursive: true });
} catch (error) {
console.debug("mkdirSync failed", error);
// The directory could have been created already.
}
fs.writeFileSync(path, content);
}
}
export function read(path: string): string {
console.debug("read", path);
return fs.readFileSync(path, "utf-8");
}
export function chmod(path: string, mode: fs.Mode): void {
console.debug("chmod", path, mode);
fs.chmodSync(path, mode);
}
export function spawn(
cmd: string,
args: string[],
options: child_process.SpawnOptions = {},
): {
exitCode: number;
stdout: string;
stderr: string;
} {
console.debug("spawn", [cmd, ...args].join(" "));
const { status, stdout, stderr } = child_process.spawnSync(cmd, args, {
stdio: "pipe",
encoding: "utf-8",
...options,
});
return {
exitCode: status ?? 1,
stdout,
stderr,
};
}
export type Response = {
readonly status: number;
arrayBuffer(): Promise<ArrayBuffer>;
json<T>(): Promise<T>;
};
export const fetch = "fetch" in globalThis ? webFetch : nodeFetch;
async function webFetch(url: string, assert?: boolean): Promise<Response> {
const response = await globalThis.fetch(url);
console.debug("fetch", url, response.status);
if (assert !== false && !isOk(response.status)) {
throw new Error(`${response.status}: ${url}`);
}
return response;
}
async function nodeFetch(url: string, assert?: boolean): Promise<Response> {
const { get } = await import("node:http");
return new Promise((resolve, reject) => {
get(url, (response) => {
console.debug("get", url, response.statusCode);
const status = response.statusCode ?? 501;
if (response.headers.location && isRedirect(status)) {
return nodeFetch(url).then(resolve, reject);
}
if (assert !== false && !isOk(status)) {
return reject(new Error(`${status}: ${url}`));
}
const body: Buffer[] = [];
response.on("data", (chunk) => {
body.push(chunk);
});
response.on("end", () => {
resolve({
status,
async arrayBuffer() {
return Buffer.concat(body).buffer as ArrayBuffer;
},
async json() {
const text = Buffer.concat(body).toString("utf-8");
return JSON.parse(text);
},
});
});
}).on("error", reject);
});
}
function isOk(status: number): boolean {
return status === 200;
}
function isRedirect(status: number): boolean {
switch (status) {
case 301: // Moved Permanently
case 308: // Permanent Redirect
case 302: // Found
case 307: // Temporary Redirect
case 303: // See Other
return true;
}
return false;
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"lib": ["esnext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
"types": ["bun-types"],
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"resolveJsonModule": true
},
"include": [
"src",
"scripts"
]
}