Compare commits

...

73 Commits

Author SHA1 Message Date
jhmaster2000
4befbc6a60 fix bun-write test 2023-12-11 14:11:31 -03:00
jhmaster2000
d1b04facce run bun fmt 2023-12-11 05:06:42 -03:00
jhmaster2000
2bff24560f polyfills: fixes for 1.0.16 2023-12-11 05:03:04 -03:00
jhmaster2000
e37a579588 Merge branch 'main' into bun-polyfills 2023-12-11 03:42:35 -03:00
jhmaster2000
c35f963677 polyfills: adapt tests for polyfills test runner 2023-12-11 03:18:42 -03:00
jhmaster2000
fb2dfb2337 polyfills: hono serve rewrite 2023-12-11 02:09:48 -03:00
jhmaster2000
b8dcf2caf8 polyfills: fix filesink test 2023-12-09 19:07:58 -03:00
jhmaster2000
e7d46e3120 polyfills: Bun.password fixes 2023-12-09 18:48:31 -03:00
jhmaster2000
5e0160552a polyfills: a failed attempt at HTMLRewriter 2023-12-08 19:20:07 -03:00
jhmaster2000
c3739c5d38 polyfills: a few misc changes 2023-12-08 17:39:46 -03:00
jhmaster2000
aa606b23df polyfills: fix which options.cwd handling 2023-12-08 15:46:29 -03:00
jhmaster2000
6f01ddc38d polyfills: fix unsafe.arrayBufferToString 2023-12-08 15:31:40 -03:00
jhmaster2000
b63fc096bf polyfills: sleep/Sync fix 2023-12-08 06:16:50 -03:00
jhmaster2000
93b32aef29 polyfills: impl reportError + spawnSync fix 2023-12-08 06:09:28 -03:00
jhmaster2000
e7019186cd polyfills: fix FileBlob.exists() + more tests 2023-12-08 04:40:42 -03:00
jhmaster2000
b55994b059 polyfills: test runner adjustments 2023-12-08 03:47:39 -03:00
jhmaster2000
57b5e00e64 polyfills: more spawn/Sync fixes 2023-12-08 03:46:56 -03:00
jhmaster2000
db7cb6fa98 polyfills: fix spawn/Sync stdout/stdin io handling 2023-12-07 19:55:13 -03:00
jhmaster2000
9342cf2080 polyfills: Bun/import.meta resolve(Sync) fixes 2023-12-07 15:44:52 -03:00
jhmaster2000
ce129594a6 polyfills: minor test runner fixes 2023-12-07 14:42:08 -03:00
jhmaster2000
666c13f964 polyfills: refactor FileBlob & Bun.write 2023-12-07 14:41:32 -03:00
jhmaster2000
71d99f5658 polyfills: stub out gc error in test runner 2023-12-06 18:34:15 -03:00
jhmaster2000
798ca1ff8c polyfills: 1.0.15 fixes 2023-12-06 18:05:39 -03:00
jhmaster2000
989a670682 bun-polyfills: bun:jsc tweaks 2023-12-02 02:29:54 -03:00
jhmaster2000
62cdc58acf polyfills: Bun.file fix & --expose-gc 2023-12-02 01:52:41 -03:00
jhmaster2000
7c841d1aa3 polyfills: bun:ffi bugfixes 2023-12-02 01:30:45 -03:00
jhmaster2000
47ee87d7e0 polyfills: fix off-by-2 error lines in test runner 2023-12-01 20:55:50 -03:00
jhmaster2000
9f0faf48db polyfills: load Bun global into test runner 2023-12-01 19:48:05 -03:00
jhmaster2000
519d53a16b polyfills: Bun.dns improvements 2023-12-01 19:26:21 -03:00
jhmaster2000
ee72c14957 polyfills: load test fns into global scope (fix*) 2023-12-01 01:13:11 -03:00
jhmaster2000
5062c2bc3a polyfills: node's --input-type flag is something 2023-12-01 00:19:47 -03:00
jhmaster2000
2d002297ce polyfills: fix Bun.spawn stdin cleanup 2023-11-30 23:57:10 -03:00
jhmaster2000
19cee77e8b polyfills: add jest-extended matchers to bun:test 2023-11-30 23:55:46 -03:00
jhmaster2000
fbdbf297e1 polyfills: fix console async iterator 2023-11-30 23:54:51 -03:00
jhmaster2000
3550d4e94c polyfills: load globals into test runner 2023-11-30 21:27:08 -03:00
jhmaster2000
f82c6013b1 polyfills: bun:sqlite 2023-11-30 21:15:41 -03:00
jhmaster2000
ee14093325 polyfills: bun:test 2023-11-30 20:42:45 -03:00
jhmaster2000
694f679817 polyfills: rework test runner 2023-11-30 20:36:17 -03:00
jhmaster2000
000c0a2109 polyfills: semver & TOML 2023-11-30 12:31:56 -03:00
jhmaster2000
10f4a4e73c polyfills: full process.versions & build fixes 2023-11-17 18:56:29 -03:00
jhmaster2000
222bfda9cc temp fix for wasm build (see desc)
src/api/schema.zig is an autogen'd file by peechy, it is currently
impossible to generate this correct file with current peechy version
a manually patched peechy was used for this version of the file commited
until peechy is properly updated, please avoid running `make wasm`
otherwise you will break the wasm build compilation
2023-11-17 17:03:27 -03:00
jhmaster2000
6b5f2b3442 Merge branch 'main' into bun-polyfills 2023-11-17 16:45:28 -03:00
jhmaster2000
05e5a5c95d polyfills: upgrade deps 2023-11-10 09:34:06 -03:00
jhmaster2000
e2bd31c9fe polyfills: add TOML and semver to the todo list 2023-11-10 09:27:10 -03:00
jhmaster2000
c4ee70ae84 Merge branch 'main' into bun-polyfills 2023-11-10 09:22:45 -03:00
jhmaster2000
3c2b222b6b bun-polyfills: a lot of stuff
* CryptoHasher(Interface)
* full rewrite and progress on Bun.serve
2023-11-03 23:09:22 -03:00
jhmaster2000
f6d40d9c9c bun-polyfills: core impl. of Bun.serve 2023-10-26 02:01:42 -03:00
jhmaster2000
195cee203a bun-polyfills: starter impl. of Bun.connect 2023-10-20 01:36:40 -03:00
jhmaster2000
39a8aa8207 bun-polyfills: starting impl. of Bun.listen 2023-10-19 20:26:06 -03:00
jhmaster2000
4cbc7a449c bun-polyfills: stdout/stderr/stdin are fixed 2023-10-18 21:01:53 -03:00
jhmaster2000
52aed4c987 bun-polyfills: impl. Blob.formData + File methods 2023-10-18 19:46:35 -03:00
jhmaster2000
2329ab8c5b bun-polyfills: ts fixes & add FileBlob properties 2023-10-18 18:44:17 -03:00
jhmaster2000
603782fc48 bun-polyfills: impl. extra Blob properties 2023-10-18 18:43:13 -03:00
jhmaster2000
5db6e3c943 bun-polyfills: enable verbatimModuleSyntax 2023-10-18 18:42:43 -03:00
jhmaster2000
d3c926dab8 bun-polyfills: update all deps 2023-10-18 00:32:48 -03:00
jhmaster2000
c58c297e8d bun-polyfills: impl. readableStreamToFormData 2023-10-18 00:20:33 -03:00
jhmaster2000
c26f06d52e bun-polyfills: impl. deepEquals/deepMatch 2023-10-17 22:29:55 -03:00
jhmaster2000
44d6e8b062 add fix to Bun.Transpiler polyfill too 2023-10-13 20:45:20 -03:00
jhmaster2000
89d8939a45 bun-polyfills: fix test loader error printing 2023-10-13 20:42:27 -03:00
jhmaster2000
f9af6d3411 bun-polyfills: WIP polyfills test runner 2023-10-13 20:31:14 -03:00
jhmaster2000
e53eb8f2d9 bun-polyfills: fix default exports 2023-10-13 20:25:44 -03:00
jhmaster2000
8fcd645bae bun-polyfills: refactor ffi module 2023-10-13 17:45:27 -03:00
jhmaster2000
91f4ba534b bun-polyfills: impl. Bun.password.*
+ fix Bun.generateHeapSnapshot & mmap
2023-10-13 05:14:35 -03:00
jhmaster2000
590672720d bun-polyfills: SyncWorker util 2023-10-13 05:10:47 -03:00
jhmaster2000
9daa9f31ea bun-polyfills: add argon2 and bcryptjs deps 2023-10-13 05:05:39 -03:00
jhmaster2000
a25bb42416 more bun:ffi polyfill progress 2023-10-09 21:57:26 -03:00
jhmaster2000
3ee6aa803b start of a bun:ffi polyfill impl. 2023-10-05 23:28:05 -03:00
jhmaster2000
b510da04d1 Bun.mmap polyfill return Buffer -> Uint8Array 2023-09-29 23:47:29 -03:00
jhmaster2000
17390b39bd impl. Bun.mmap polyfill 2023-09-29 23:37:49 -03:00
jhmaster2000
f33646e4d8 jsc.get/setRandomSeed polyfills 2023-09-29 17:49:19 -03:00
jhmaster2000
238ebd5fb9 "fix" importmeta.resolve type conflict 2023-09-29 16:55:06 -03:00
jhmaster2000
776af14918 update polyfills deps 2023-09-29 16:32:47 -03:00
jhmaster2000
fd1381b508 simplify bun.inspect polyfill 2023-09-29 16:13:26 -03:00
52 changed files with 2885 additions and 465 deletions

View File

@@ -167,6 +167,7 @@ dist
# Misc
_*
*.so
.old
.vscode
!build

Binary file not shown.

View File

@@ -2,29 +2,60 @@
"type": "module",
"name": "bun-polyfills",
"module": "src/index.ts",
"devDependencies": {
"@types/node": "^20.4.5",
"@types/which": "^3.0.0",
"bun-types": "^0.7.0",
"copyfiles": "^2.4.1"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"scripts": {
"node": "node --enable-source-maps --import ./dist/src/repl.js",
"node": "tsx --no-warnings --expose-gc --enable-source-maps --import ./dist/src/repl.js",
"test": "cd ../../test && node --no-warnings --loader ../packages/bun-polyfills/tools/bun_test_loader.mjs --import ../packages/bun-polyfills/dist/src/global/index.js --import ../packages/bun-polyfills/dist/src/global/test.js",
"test-gc": "bun run test --expose-gc",
"test-esm": "bun run test --input-type=module",
"test-esm-gc": "bun run test --input-type=module --expose-gc",
"test-all": "bun run test-esm-gc ../packages/bun-polyfills/tools/test_all.js",
"clean": "rm -rf dist",
"preprocess": "bun tools/updateversions.ts",
"build": "bun run clean && bun run preprocess && bunx tsc && bunx copyfiles \"./lib/**/*.wasm\" dist",
"build/wasm": "bun run build/zighash",
"build/zighash": "cd lib/zighash && bun run build && cd ../.."
},
"devDependencies": {
"@types/bcryptjs": "^2.4.5",
"@types/better-sqlite3": "^7.6.7",
"@types/dateformat": "^5.0.2",
"@types/node": "^20.8.6",
"@types/set-cookie-parser": "^2.4.5",
"@types/which": "^3.0.0",
"@types/ws": "^8.5.8",
"bun-types": "^1.0.6",
"copyfiles": "^2.4.1",
"tsx": "^4.1.0",
"typings-esm-loader": "^2.0.1"
},
"peerDependencies": {
"typescript": "^5.2.2"
},
"dependencies": {
"@hono/node-server": "^1.3.1",
"argon2": "^0.31.2",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^9.1.1",
"bun-wasm": "link:bun-wasm",
"chalk": "^5.3.0",
"dateformat": "^5.0.3",
"expect": "^29.7.0",
"hono": "^3.11.4",
"html-rewriter-wasm": "^0.4.1",
"isomorphic-ws": "^5.0.0",
"jest-each": "^29.7.0",
"jest-extended": "^4.0.2",
"jest-mock": "^29.7.0",
"js-md4": "^0.3.2",
"open-editor": "^4.0.0",
"koffi": "^2.6.3",
"mmap-utils": "^1.0.0",
"open-editor": "^4.1.1",
"semver": "^7.5.4",
"set-cookie-parser": "^2.6.0",
"smol-toml": "^1.1.3",
"supports-color": "^9.4.0",
"which": "^3.0.1"
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.33.0",
"which": "^4.0.0",
"ws": "^8.14.2"
}
}

View File

@@ -9,14 +9,30 @@
// };
//}
const consoleAsyncIterChunks: string[] = [];
//? Implements: for await (const line of console) { ... }
console[Symbol.asyncIterator] = async function* () {
while (true) yield await new Promise(resolve => {
process.stdin.on('data', (data: Buffer | string) => {
const str = data.toString('utf-8').replaceAll(/[\r\n]+/g, '');
resolve(str);
if (consoleAsyncIterChunks.length) {
for (const line of [...consoleAsyncIterChunks]) {
consoleAsyncIterChunks.shift();
if (!line) continue;
yield line;
}
}
while (true) {
const p = await new Promise<string[]>(resolve => {
process.stdin.once('data', (data: Buffer | string) => {
const str = data.toString('utf-8').split(/[\r\n]+/g);
resolve(str);
});
});
});
consoleAsyncIterChunks.push(...p);
for (const line of p) {
consoleAsyncIterChunks.shift();
if (!line) continue;
yield line;
}
}
} satisfies Console[typeof Symbol.asyncIterator];
//? Implements: Bun-exclusive console function

View File

@@ -0,0 +1,99 @@
/// <reference types='bun-types' />
import { pathToFileURL } from 'node:url';
import {
type ElementHandlers as WASMElementHandlers,
type DocumentHandlers as WASMDocumentHandlers,
type HTMLRewriterOptions as WASMRewriterOptions,
HTMLRewriter as WASMRewriter,
} from 'html-rewriter-wasm';
import { SyncWorker } from '../utils/sync.mjs';
import { createRequire } from 'node:module';
import { NotImplementedError } from '../utils/errors.js';
const require = createRequire(import.meta.url);
type BunElementHandlers = HTMLRewriterTypes.HTMLRewriterElementContentHandlers;
type BunDocumentHandlers = HTMLRewriterTypes.HTMLRewriterDocumentContentHandlers;
type BunRewriter = typeof HTMLRewriter;
type ElementHandlers = BunElementHandlers;
type DocumentHandlers = BunDocumentHandlers;
export const htmlRewriter = class HTMLRewriter {
#elementHandlers: [selector: string, handlers: ElementHandlers][] = [];
#documentHandlers: DocumentHandlers[] = [];
readonly #options: WASMRewriterOptions;
constructor(options: WASMRewriterOptions = {}) {
this.#options = options;
}
on(selector: string, handlers: ElementHandlers): this {
this.#elementHandlers.push([selector, handlers]);
return this;
}
onDocument(handlers: DocumentHandlers): this {
this.#documentHandlers.push(handlers);
return this;
}
transform(input: Response): Response {
throw new NotImplementedError('HTMLRewriter.transform', this.transform);
// Well, I tried, this is a bit of a mess. I'm not sure how (if even possible) to get this to work.
// As far as I can tell there is no way to make callbacks work across a worker boundary, given that
// functions are not serializable, which is a problem when the callbacks are pretty much the entire
// point of this class. Were Bun to make the transform function async, this would be a lot easier, but alas.
/*const requireModules = { 'html-rewriter-wasm': pathToFileURL(require.resolve('html-rewriter-wasm')).href };
const outerWorkerData = {
thisOptions: this.#options,
thisElementHandlers: this.#elementHandlers
.map(([selector, handlers]) => [selector, Reflect.ownKeys(handlers) as (keyof typeof handlers)[]] as const),
thisDocumentHandlers: this.#documentHandlers
.map(handlers => Reflect.ownKeys(handlers) as (keyof typeof handlers)[]),
};
const worker = new SyncWorker(requireModules, outerWorkerData);
const out = worker.sync(async (workerInput: ReadableStream<any>) => {
const { workerData } = await import('node:worker_threads') as {
workerData: typeof outerWorkerData & { resolve: Record<string, string>; };
};
const wasmrewriter = (await import(workerData.resolve['html-rewriter-wasm'])) as typeof import('html-rewriter-wasm');
const WASMRewriter = wasmrewriter.HTMLRewriter;
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const elmCallbacks: Record<string, Record<string, any>> = {};
const docCallbacks: Record<string, any> = {};
let output = '';
const rewriter = new WASMRewriter((chunk) => output += decoder.decode(chunk), workerData.thisOptions);
//for (const [selector, handlers] of workerData.thisElementHandlers) rewriter.on(selector, handlers as WASMElementHandlers);
//for (const handlers of workerData.thisDocumentHandlers) rewriter.onDocument(handlers);
for (const [selector, handlers] of workerData.thisElementHandlers) {
rewriter.on(selector, {} as WASMElementHandlers);
}
for (const handlers of workerData.thisDocumentHandlers) {
}
const reader = workerInput.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
await rewriter.write(value);
}
//await rewriter.write(encoder.encode(workerInput));
await rewriter.end();
const encoded = encoder.encode(output);
return encoded;
} finally {
rewriter.free();
reader.releaseLock();
}
}, null)(input.body!);
worker.terminate();
return new Response(out);*/
}
};
Object.defineProperty(globalThis, 'HTMLRewriter', {
value: htmlRewriter satisfies BunRewriter,
configurable: true, writable: true, enumerable: true,
});

View File

@@ -19,10 +19,12 @@ export default function polyfillImportMeta(metaIn: ImportMeta) {
dir: path.dirname(metapath),
file: path.basename(metapath),
require: require2,
// @ts-expect-error conflict with @types/node
async resolve(id: string, parent?: string) {
return this.resolveSync(id, parent);
},
resolveSync(id: string, parent?: string) {
if (id.startsWith('file://')) id = fileURLToPath(id);
return require2.resolve(id, {
paths: typeof parent === 'string' ? [
path.resolve(parent.startsWith('file://') ? fileURLToPath(parent) : parent, '..')

View File

@@ -1,9 +1,17 @@
import { version } from '../modules/bun.js';
import type { BunFile } from 'bun';
import { version, readableStreamToFormData } from '../modules/bun.js';
import './console.js';
import './process.js';
import './htmlrewriter.js';
import os from 'node:os';
//? NodeJS Blob doesn't implement Blob.json(), so we need to polyfill it.
//? Yet another globalThis alias, because why not right?
Object.defineProperty(globalThis, 'self', {
get() { return globalThis; }, set(_) { },
enumerable: true, configurable: true,
});
//? NodeJS Blob doesn't implement these, so we need to polyfill them.
Blob.prototype.json = async function json<T>(this: Blob): Promise<T> {
try {
return JSON.parse(await this.text()) as T;
@@ -12,6 +20,31 @@ Blob.prototype.json = async function json<T>(this: Blob): Promise<T> {
throw err;
}
};
Blob.prototype.formData = async function formData(this: Blob): Promise<FormData> {
if (this.type.startsWith('multipart/form-data;')) {
return new Response(this.stream(), {
headers:
//? Good one Node: https://github.com/nodejs/node/issues/42266
{ 'Content-Type': this.type.replace('webkitformboundary', 'WebkitFormBoundary') }
}).formData() as Promise<FormData>;
} else if (this.type === 'application/x-www-form-urlencoded') {
return readableStreamToFormData(this.stream());
} else {
throw new TypeError('Blob type is not well-formed multipart/form-data or application/x-www-form-urlencoded');
}
};
Reflect.set(Blob.prototype, 'readable', undefined /*satisfies BunFile['readable']*/);
Reflect.set(Blob.prototype, 'lastModified', -1 satisfies BunFile['lastModified']);
Reflect.set(Blob.prototype, 'exists', (async function exists() {
return true;
}) satisfies BunFile['exists']);
Reflect.set(Blob.prototype, 'writer', (function writer() {
throw new TypeError('Blob is detached');
}) satisfies BunFile['writer']);
//? NodeJS File doesn't implement these either
File.prototype.json = Blob.prototype.json;
File.prototype.formData = Blob.prototype.formData;
//? navigator global object polyfill
Reflect.set(globalThis, 'navigator', {
@@ -19,6 +52,11 @@ Reflect.set(globalThis, 'navigator', {
hardwareConcurrency: os.cpus().length,
});
//? reportError function polyfill
Reflect.set(globalThis, 'reportError', function reportError(err: any): void {
console.error(err);
} satisfies typeof reportError);
//? method only available in Bun
// this isn't quite accurate, but it shouldn't break anything and is currently here just for matching bun and node types
const ReadableStreamDefaultReaderPrototype = Object.getPrototypeOf(new ReadableStream().getReader());

View File

@@ -0,0 +1,30 @@
import v8 from 'v8';
import vm from 'vm';
class Random extends null {
static seed: number = NaN; //! There doesn't seem to be a way to get the initial seed from v8.
static context: vm.Context = vm.createContext({}, { name: 'NodeBun_MathRandom_Context_0' });
static readonly script: vm.Script = new vm.Script('Math.random();', { filename: 'NodeBun_MathRandom' });
static setSeed(seed: number) {
Random.seed = parseInt(seed as unknown as string) || 0;
Random.context = vm.createContext({ v8 }, { name: `NodeBun_MathRandom_Context_${Random.seed}` });
vm.runInContext(`v8.setFlagsFromString('--random_seed=${Random.seed}');`, Random.context, { filename: `NodeBun_MathRandom_SetSeed_${Random.seed}` });
}
static gen(): number {
return Random.script.runInContext(Random.context, { filename: `NodeBun_MathRandom_Gen_${Random.seed}` }) as number;
}
// lazily only apply the global patch if get/setRandomSeed is called
static PATCH_CHECK = () => {
Math.random = Random.gen;
Random.PATCH_CHECK = () => {};
}
}
export function setRandomSeed(seed: number): void {
Random.PATCH_CHECK();
return Random.setSeed(seed);
}
export function getRandomSeed(): number {
Random.PATCH_CHECK();
return Random.seed;
}

View File

@@ -5,14 +5,19 @@ if (typeof process === 'object' && process !== null) {
Reflect.set(process, 'browser', false satisfies Process['browser']);
const NULL_VERSION = '0'.repeat(39) + '1';
process.versions.bun = '0.7.1' satisfies Process['versions'][string]; // TODO: This can probably be fetched from somewhere in the repo
process.versions.webkit = NULL_VERSION satisfies Process['versions'][string];
process.versions.mimalloc = NULL_VERSION satisfies Process['versions'][string];
process.versions.libarchive = NULL_VERSION satisfies Process['versions'][string];
process.versions.picohttpparser = NULL_VERSION satisfies Process['versions'][string];
process.versions.boringssl = NULL_VERSION satisfies Process['versions'][string];
process.versions.zig = '0.10.0' satisfies Process['versions'][string];
Reflect.set(process, 'revision', NULL_VERSION satisfies Process['revision']);
/** @start_generated_code */
process.versions.boringssl = 'b275c5ce1c88bc06f5a967026d3c0ce1df2be815' satisfies Process['versions'][string];
process.versions.libarchive = 'dc321febde83dd0f31158e1be61a7aedda65e7a2' satisfies Process['versions'][string];
process.versions.mimalloc = '7968d4285043401bb36573374710d47a4081a063' satisfies Process['versions'][string];
process.versions.picohttpparser = '066d2b1e9ab820703db0837a7255d92d30f0c9f5' satisfies Process['versions'][string];
process.versions.webkit = 'a780bdf0255ae1a7ed15e4b3f31c14af705facae' satisfies Process['versions'][string];
process.versions.tinycc = '2d3ad9e0d32194ad7fd867b66ebe218dcc8cb5cd' satisfies Process['versions'][string];
process.versions.lolhtml = '8d4c273ded322193d017042d1f48df2766b0f88b' satisfies Process['versions'][string];
process.versions.c_ares = '0e7a5dee0fbb04080750cf6eabbe89d8bae87faa' satisfies Process['versions'][string];
process.versions.zig = '0.12.0-dev.1604+caae40c21' satisfies Process['versions'][string];
process.versions.bun = '1.0.16' satisfies Process['versions'][string];
Reflect.set(process, 'revision', 'e37a5795887e0b46c3b3a4adca725a9440b7b273' satisfies Process['revision']);
/** @end_generated_code */
// Doesn't work on Windows sadly
//Object.defineProperty(process, 'execPath', { value: path.resolve(root, 'cli.js') });

View File

@@ -0,0 +1,15 @@
import testPoly from '../modules/test.js';
import bun from '../modules/bun.js';
Reflect.set(globalThis, 'Bun', bun);
//! Not a polyfill
// This file is used for preloading into the polyfills test runner, due to Jest's and by extension Bun's,
// quirky global scope pollution behavior.
for (const key of Object.keys(testPoly)) {
Reflect.set(globalThis, key, Reflect.get(testPoly, key));
}
// Let's just not bother with concurrent tests for now, it'll work fine without. (Node's { concurrency: # } option is quirky)
Reflect.set(Reflect.get(globalThis, 'describe'), 'concurrent', testPoly.describe);
Reflect.set(Reflect.get(globalThis, 'test'), 'concurrent', testPoly.test);
Reflect.set(Reflect.get(globalThis, 'it'), 'concurrent', testPoly.it);

View File

@@ -1,23 +1,30 @@
import type {
BunPlugin, PluginConstraints, PluginBuilder, OnLoadCallback, OnResolveCallback, HeapSnapshot,
EditorOptions, SpawnOptions, Subprocess, SyncSubprocess, FileBlob as BunFileBlob, ArrayBufferView, Hash
BunPlugin, PluginConstraints, PluginBuilder, OnLoadCallback, OnResolveCallback, HeapSnapshot, Password,
EditorOptions, SpawnOptions, Subprocess, SyncSubprocess, FileBlob as BunFileBlob, ArrayBufferView, Hash,
CryptoHashInterface as BunCryptoHashInterface,
} from 'bun';
import { TextDecoderStream } from 'node:stream/web';
import { NotImplementedError, type SystemError } from '../utils/errors.js';
import { streamToBuffer, isArrayBufferView, isFileBlob, isOptions } from '../utils/misc.js';
import { isArrayBufferView, isFileBlob, isOptions } from '../utils/misc.js';
import dnsPolyfill from './bun/dns.js';
import { FileSink } from './bun/filesink.js';
import {
bunHash, bunHashProto,
bunHash, bunHashProto, CryptoHasher as CryptoHasherPolyfill,
MD4 as MD4Polyfill, MD5 as MD5Polyfill,
SHA1 as SHA1Polyfill, SHA224 as SHA224Polyfill,
SHA256 as SHA256Polyfill, SHA384 as SHA384Polyfill,
SHA512 as SHA512Polyfill, SHA512_256 as SHA512_256Polyfill
SHA512 as SHA512Polyfill, SHA512_256 as SHA512_256Polyfill,
} from './bun/hashes.js';
import { ArrayBufferSink as ArrayBufferSinkPolyfill } from './bun/arraybuffersink.js';
import { FileBlob, NodeJSStreamFileBlob } from './bun/fileblob.js';
import { listen as listenPolyfill } from './bun/tcp_listen.js';
import { connect as connectPolyfill } from './bun/tcp_connect.js';
import { serve as servePolyfill } from './bun/serve.js';
import TranspilerImpl from './bun/transpiler.js';
import { mmap as mmapper } from './bun/mmap.js';
import { SyncWorker } from '../utils/sync.mjs';
import fs from 'node:fs';
import os from 'node:os';
import v8 from 'node:v8';
import path from 'node:path';
import util from 'node:util';
@@ -26,28 +33,37 @@ import streams from 'node:stream';
import workers from 'node:worker_threads';
import chp, { type ChildProcess, type StdioOptions, type SpawnSyncReturns } from 'node:child_process';
import { fileURLToPath as fileURLToPathNode, pathToFileURL as pathToFileURLNode } from 'node:url';
import { expect } from 'expect';
import npm_which from 'which';
import openEditor from 'open-editor';
import bcrypt from 'bcryptjs';
import argon2 from 'argon2';
import node_semver from 'semver';
import * as smol_toml from 'smol-toml';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
export const main = path.resolve(process.cwd(), process.argv[1] ?? 'repl') satisfies typeof Bun.main;
//? These are automatically updated on build by tools/updateversions.ts, do not edit manually.
export const version = '0.7.4' satisfies typeof Bun.version;
export const revision = '56816a3ec845a4b9fc40ade34dbe5c0033433d51' satisfies typeof Bun.revision;
export const version = '1.0.16' satisfies typeof Bun.version;
export const revision = 'e37a5795887e0b46c3b3a4adca725a9440b7b273' satisfies typeof Bun.revision;
export const gc = (globalThis.gc ? (() => (globalThis.gc!(), process.memoryUsage().heapUsed)) : (() => {
const err = new Error('[bun-polyfills] Garbage collection polyfills are only available when Node.js is ran with the --expose-gc flag.');
Error.captureStackTrace(err, gc);
throw err;
})) satisfies typeof Bun.gc;
export const gc = (
globalThis.gc
? (() => (globalThis.gc!(), process.memoryUsage().heapUsed))
: process.env.BUN_POLYFILLS_TEST_RUNNER ? () => 0 : (() => {
const err = new Error('[bun-polyfills] Garbage collection polyfills are only available when Node.js is ran with the --expose-gc flag.');
Error.captureStackTrace(err, gc);
throw err;
})
) satisfies typeof Bun.gc;
//getter(bun, 'cwd', proc.cwd); //! Can't named export a getter
export const origin = '' satisfies typeof Bun.origin;
// @ts-expect-error ---
export const stdin = new NodeJSStreamFileBlob(process.stdin) satisfies typeof Bun.stdin;
// @ts-expect-error ---
export const stdout = new NodeJSStreamFileBlob(process.stdout) satisfies typeof Bun.stdout;
// @ts-expect-error ---
export const stderr = new NodeJSStreamFileBlob(process.stderr) satisfies typeof Bun.stderr;
export const argv = [process.argv0, ...process.execArgv, ...process.argv.slice(1)] satisfies typeof Bun.argv;
export const env = process.env satisfies typeof Bun.env;
@@ -62,8 +78,10 @@ Object.setPrototypeOf(hash, bunHashProto satisfies Hash);
export const unsafe = {
gcAggressionLevel: () => 0, //! no-op
arrayBufferToString: (buf) => new TextDecoder().decode(buf),
segfault: () => {
arrayBufferToString(buf) {
return new TextDecoder(buf instanceof Uint16Array ? 'utf-16' : 'utf-8').decode(buf);
},
segfault() {
const segfault = new Error();
segfault.name = 'SegfaultTest';
segfault.message = '';
@@ -74,6 +92,9 @@ export const unsafe = {
export const Transpiler = TranspilerImpl satisfies typeof Bun.Transpiler;
export const listen = listenPolyfill satisfies typeof Bun.listen;
export const connect = connectPolyfill satisfies typeof Bun.connect;
export const SHA1 = SHA1Polyfill satisfies typeof Bun.SHA1;
export const MD5 = MD5Polyfill satisfies typeof Bun.MD5;
export const MD4 = MD4Polyfill satisfies typeof Bun.MD4;
@@ -83,6 +104,10 @@ export const SHA384 = SHA384Polyfill satisfies typeof Bun.SHA384;
export const SHA256 = SHA256Polyfill satisfies typeof Bun.SHA256;
export const SHA512_256 = SHA512_256Polyfill satisfies typeof Bun.SHA512_256;
export const CryptoHasher = CryptoHasherPolyfill satisfies typeof Bun.CryptoHasher;
// This only exists as a type, but is declared as a value in bun-types.
export const CryptoHashInterface = undefined as unknown as typeof BunCryptoHashInterface<any>;
export const indexOfLine = ((data, offset) => {
if (data instanceof ArrayBuffer || data instanceof SharedArrayBuffer) data = new Uint8Array(data);
if (data instanceof DataView || !(data instanceof Uint8Array)) data = new Uint8Array(data.buffer);
@@ -99,9 +124,13 @@ peek_.status = (promise => {
export const peek = peek_ satisfies typeof Bun.peek;
export const sleep = (ms => {
return new Promise(r => setTimeout(r, ms instanceof Date ? ms.valueOf() - Date.now() : ms));
if (ms instanceof Date) ms = ms.valueOf() - Date.now();
if (typeof ms !== 'number') throw new TypeError('argument to sleep must be a number or Date');
if (ms < 0) throw new TypeError('argument to sleep must not be negative');
return new Promise(r => setTimeout(r, ms as number));
}) satisfies typeof Bun.sleep;
export const sleepSync = (ms => {
if (typeof ms !== 'number') throw new TypeError('argument to sleepSync must be a number');
if (ms < 0) throw new TypeError('argument to sleepSync must not be negative');
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}) satisfies typeof Bun.sleepSync;
@@ -109,33 +138,31 @@ export const sleepSync = (ms => {
//? This is not 1:1 matching, but no one should be relying on the exact output of this function anyway.
//? To quote Node's inspect itself: "The output of util.inspect() may change at any time and should not be depended upon programmatically."
//? Of course in Node's case some didn't listen and relied on the output of util.inspect() anyway, but hopefully this won't happen with this one.
export const inspect = ((arg: any): string => util.inspect(arg, {
breakLength: Infinity,
colors: false,
compact: true,
customInspect: false,
depth: Infinity,
getters: true,
maxArrayLength: Infinity,
maxStringLength: Infinity,
showHidden: false,
showProxy: false,
sorted: false
})) satisfies typeof Bun.inspect;
export const inspect = util.inspect satisfies typeof Bun.inspect;
export const resolveSync = ((id: string, parent: string) => import.meta.resolveSync(id, parent)) satisfies typeof Bun.resolveSync;
export const resolve = (async (id: string, parent: string) => import.meta.resolve!(id, parent)) satisfies typeof Bun.resolve;
export const resolveSync = ((id: string, parent: string) => {
const require2 = createRequire(path.join(parent, 'caller'));
if (id.startsWith('file://')) id = fileURLToPath(id);
return require2.resolve(id);
}) satisfies typeof Bun.resolveSync;
export const resolve = (async (id: string, parent: string) => {
return resolveSync(id, parent);
}) satisfies typeof Bun.resolve;
//? Yes, this is faster than new Uint8Array(Buffer.allocUnsafe(size).buffer) by about 2.5x in Node.js
export const allocUnsafe = ((size: number) => new Uint8Array(size)) satisfies typeof Bun.allocUnsafe;
export const generateHeapSnapshot = (async (): Promise<HeapSnapshot> => {
process.emitWarning('The polyfill for Bun.generateHeapShot is asynchronous, unlike the original which is synchronous.', {
type: 'BunPolyfillWarning',
code: 'BUN_POLYFILLS_ASYNC_GENERATE_HEAP_SNAPSHOT',
detail: 'This is due to v8.getHeapSnapshot() returning a stream in Node.js. This is not a bug, but a limitation of the polyfill.'
});
const raw = (await streamToBuffer(v8.getHeapSnapshot())).toString('utf8');
export const mmap = mmapper satisfies typeof Bun.mmap;
export const generateHeapSnapshot = ((): HeapSnapshot => {
const stream = v8.getHeapSnapshot();
const chunks = [];
while (true) {
const chunk = stream.read();
if (chunk === null) break;
chunks.push(chunk);
}
const raw = Buffer.concat(chunks).toString('utf8');
const json = JSON.parse(raw) as V8HeapSnapshot;
return {
version: 2,
@@ -145,8 +172,7 @@ export const generateHeapSnapshot = (async (): Promise<HeapSnapshot> => {
edgeTypes: json.snapshot.meta.edge_types.flat(),
edgeNames: json.snapshot.meta.edge_fields.flat(),
nodeClassNames: json.snapshot.meta.node_types.flat(),
};
// @ts-expect-error Refer to the above emitWarning call
} satisfies HeapSnapshot;
}) satisfies typeof Bun.generateHeapSnapshot;
//! This is a no-op in Node.js, as there is no way to shrink the V8 heap from JS as far as I know.
@@ -158,48 +184,58 @@ export const openInEditor = ((file: string, opts?: EditorOptions) => {
else openEditor(target, { editor: process.env.TERM_PROGRAM ?? process.env.VISUAL ?? process.env.EDITOR ?? 'vscode' });
}) satisfies typeof Bun.openInEditor;
export const serve = (() => { throw new NotImplementedError('Bun.serve', serve); }) satisfies typeof Bun.serve;
export const serve = servePolyfill satisfies typeof Bun.serve;
export const file = ((path: string | URL | Uint8Array | ArrayBufferLike | number, options?: BlobPropertyBag): BunFileBlob => {
if (typeof path === 'object') throw new NotImplementedError('Bun.file with typed array', file);
if (path instanceof URL) path = fileURLToPathNode(path);
else if (typeof path === 'object') {
if (path instanceof ArrayBuffer || path instanceof SharedArrayBuffer) path = new Uint8Array(path);
path = new TextDecoder().decode(path);
}
return new FileBlob(path, options);
}) satisfies typeof Bun.file;
export const write = (async (dest: BunFileBlob | PathLike, input: string | Blob | TypedArray | ArrayBufferLike | BlobPart[] | Response | BunFileBlob): ReturnType<typeof Bun.write> => {
export const write = ((
dest: BunFileBlob | PathLike,
input: string | Blob | TypedArray | ArrayBufferLike | BlobPart[] | Response | BunFileBlob,
options?
): ReturnType<typeof Bun.write> => {
if (options?.createPath ?? true) {
let destPath: string | undefined;
if (isFileBlob(dest)) destPath = dest.name;
else if (typeof dest === 'string') destPath = dest;
else if (dest instanceof URL) destPath = fileURLToPathNode(dest);
else if (dest instanceof ArrayBuffer || dest instanceof SharedArrayBuffer) destPath = new TextDecoder().decode(dest);
else destPath = new TextDecoder().decode(dest.buffer);
if (!destPath && options?.createPath) throw new Error('Cannot create a directory for a file descriptor');
else if (destPath) {
const dir = path.dirname(destPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
}
if (!isFileBlob(dest)) {
let fd: number;
if (dest instanceof ArrayBuffer || dest instanceof SharedArrayBuffer) fd = fs.openSync(Buffer.from(dest), 'w');
// bun-types thought it'd be funny to make their own URL definition which doesnt match with the correct URL definition...
else if (typeof dest === 'string' || dest instanceof URL) fd = fs.openSync(dest as import('url').URL, 'w');
else fd = fs.openSync(Buffer.from(dest.buffer), 'w');
if (input instanceof Response || input instanceof Blob) {
const data = await input.text();
return new Promise((resolve, reject) => {
fs.write(fd, data, (err, written) => err ? reject(err) : resolve(written));
});
if (typeof dest === 'string' || dest instanceof URL) dest = new FileBlob(fs.openSync(dest, 'w+'));
else {
dest = new FileBlob(fs.openSync(Buffer.from(
dest instanceof ArrayBuffer || dest instanceof SharedArrayBuffer ? dest : dest.buffer
), 'w+'));
}
if (Array.isArray(input)) {
const data = await new Blob(input).text();
return new Promise((resolve, reject) => {
fs.write(fd, data, (err, written) => err ? reject(err) : resolve(written));
});
}
return new Promise((resolve, reject) => {
if (typeof input === 'string') return fs.write(fd, input, (err, written) => err ? reject(err) : resolve(written));
if (input instanceof Uint8Array) return fs.write(fd, input, (err, written) => err ? reject(err) : resolve(written));
if (input instanceof ArrayBuffer) return fs.write(fd, new Uint8Array(input), (err, written) => err ? reject(err) : resolve(written));
if (input instanceof SharedArrayBuffer) return fs.write(fd, new Uint8Array(input), (err, written) => err ? reject(err) : resolve(written));
return write(dest, String(input)); // if all else fails, it seems Bun tries to convert to string and write that.
});
} else {
const writer = dest.writer();
}
if (Reflect.get(dest, '@@writeSlice') && isFileBlob(input)) {
const slice = Reflect.get(dest, '@@writeSlice') as number;
input = input.slice(0, slice);
}
return (async () => {
const writer = (dest as BunFileBlob).writer();
if (Array.isArray(input)) input = new Blob(input);
if (input instanceof Blob || input instanceof Response) return writer.write(await input.arrayBuffer());
// @ts-expect-error account for hono's Response monkeypatch
if (input.constructor.name === '_Response') return writer.write(await input.arrayBuffer());
if (input instanceof ArrayBuffer || input instanceof SharedArrayBuffer || ArrayBuffer.isView(input)) return writer.write(input);
if (typeof input === 'string') return writer.write(input);
else return write(dest, String(input)); // if all else fails, it seems Bun tries to convert to string and write that.
}
// if all else fails, it seems Bun tries to convert to string and write that.
else return write(dest, String(input));
})();
}) satisfies typeof Bun.write;
export const sha = SHA512_256.hash satisfies typeof Bun.sha;
@@ -217,13 +253,11 @@ export const deflateSync = zlib.deflateSync satisfies typeof Bun.deflateSync;
export const gunzipSync = zlib.gunzipSync satisfies typeof Bun.gunzipSync;
export const inflateSync = zlib.inflateSync satisfies typeof Bun.inflateSync;
export const which = ((cmd: string, options) => {
export const which = ((cmd: string, options = {}) => {
const opts: npm_which.Options = { all: false, nothrow: true };
if (options?.PATH) opts.path = options.PATH;
const result = npm_which.sync(cmd, opts) as string | null;
if (!result || !options?.cwd) return result;
if (path.normalize(result).includes(path.normalize(options.cwd))) return result;
else return null;
options.cwd ??= process.cwd();
opts.path = `${options.cwd}:${options.PATH ?? process.env.PATH ?? '/'}`;
return npm_which.sync(cmd, opts) as string | null;
}) satisfies typeof Bun.which;
export const spawn = ((...args) => {
@@ -241,26 +275,27 @@ export const spawn = ((...args) => {
opts = args[0];
Reflect.deleteProperty(opts, 'cmd');
}
if (opts.ipc) throw new NotImplementedError('Bun.spawn({ ipc })', spawn);
let stdio: StdioOptions = [];
opts.stdio ??= [undefined, undefined, undefined];
if (opts.stdin) opts.stdio[0] = opts.stdin;
if (opts.stdout) opts.stdio[1] = opts.stdout;
if (opts.stderr) opts.stdio[2] = opts.stderr;
const ioNeedsPipeHandler: [ArrayBufferView | null, ArrayBufferView | null] = [null, null];
for (let i = 1; i < 3; i++) { // this intentionally skips stdin
let std = opts.stdio[i];
if (isArrayBufferView(std)) stdio[i] = streams.Readable.fromWeb(new Blob([std]).stream());
else if (std instanceof Blob || isFileBlob(std)) stdio[i] = streams.Readable.fromWeb(std.stream());
else if (std instanceof ReadableStream) stdio[i] = streams.Readable.fromWeb(std);
else if (std instanceof Response || std instanceof Request) stdio[i] = streams.Readable.fromWeb(std.body!);
let std = opts.stdio[i] as SpawnOptions.Readable;
if (isFileBlob(std)) stdio[i] = (Reflect.get(std, '@@toStream') as () => fs.WriteStream).call(std);
else if (isArrayBufferView(std)) {
stdio[i] = 'pipe';
ioNeedsPipeHandler[i - 1] = std;
}
else stdio[i] = std;
}
let stdinSrc: typeof opts.stdio[0] = null;
if (opts.stdio[0] && typeof opts.stdio[0] !== 'string') {
stdinSrc = opts.stdio[0];
stdio[0] = 'pipe';
}
} else stdio[0] = opts.stdio[0];
const subp = chp.spawn(cmd, argv, {
cwd: opts.cwd ?? process.cwd(),
// why is this set to (string | number) on env values...
@@ -268,7 +303,6 @@ export const spawn = ((...args) => {
stdio
}) as unknown as Subprocess;
const subpAsNode = subp as unknown as ChildProcess;
const stdstreams = [subpAsNode.stdin, subpAsNode.stdout, subpAsNode.stderr] as const;
if (subpAsNode.stdout) {
const rstream = streams.Readable.toWeb(subpAsNode.stdout) as ReadableStream;
Reflect.set(rstream, 'destroy', function (this: ReadableStream, err?: Error) {
@@ -276,6 +310,14 @@ export const spawn = ((...args) => {
return this;
});
(<Mutable<Subprocess>>subp).stdout = rstream;
if (ioNeedsPipeHandler[0]) {
const dest = new Uint8Array(ioNeedsPipeHandler[0].buffer);
let offset = 0;
subpAsNode.stdout.on('data', (chunk: Uint8Array) => {
dest.set(chunk, offset);
offset += chunk.byteLength;
});
}
}
if (subpAsNode.stderr) {
const rstream = streams.Readable.toWeb(subpAsNode.stderr) as ReadableStream;
@@ -284,38 +326,53 @@ export const spawn = ((...args) => {
return this;
});
(<Mutable<Subprocess>>subp).stderr = rstream;
if (ioNeedsPipeHandler[1]) {
const dest = new Uint8Array(ioNeedsPipeHandler[1].buffer);
let offset = 0;
subpAsNode.stderr.on('data', (chunk: Uint8Array) => {
dest.set(chunk, offset);
offset += chunk.byteLength;
});
}
}
let internalStdinStream: streams.Writable;
if (subpAsNode.stdin) {
const wstream = subpAsNode.stdin;
Reflect.set(wstream, 'destroy', function (this: NodeJS.WritableStream, err?: Error) {
internalStdinStream = wstream;
(<Mutable<Subprocess>>subp).stdin = new FileSink(wstream);
Reflect.set(subp.stdin as FileSink, 'destroy', function (this: NodeJS.WritableStream, err?: Error) {
void this.end(); /* if it fails its already closed */
return this;
});
internalStdinStream = wstream;
(<Mutable<Subprocess>>subp).stdin = new FileSink(wstream);
}
Object.defineProperty(subp, 'readable', { get(this: Subprocess) { return this.stdout; } });
Object.defineProperty(subp, 'exited', {
value: new Promise((resolve, reject) => {
subpAsNode.once('exit', (code) => {
stdstreams[0]?.destroy();
stdstreams[1]?.destroy();
stdstreams[2]?.destroy();
subp.kill();
subp.unref();
subpAsNode.disconnect?.();
subpAsNode.removeAllListeners();
subpAsNode.once('exit', (code, signal) => {
opts.onExit?.(subp, code, signal && os.constants.signals[signal]);
resolve(code);
});
})
});
const unrefFn = subpAsNode.unref;
subpAsNode.unref = function unref(): void {
unrefFn.apply(this);
// unref() alone is basically useless without { detached: true } in spawn options,
// so we have to manually force it like this.
this.disconnect?.();
this.stderr?.destroy?.();
this.stdout?.destroy?.();
this.stdin?.end?.();
this.stdin?.destroy?.();
};
if (stdinSrc) subpAsNode.once('spawn', () => {
const stdinWeb = streams.Writable.toWeb(internalStdinStream);
if (isArrayBufferView(stdinSrc)) stdinSrc = new Blob([stdinSrc]);
if (stdinSrc instanceof Blob) void stdinSrc.stream().pipeTo(stdinWeb);
else if (stdinSrc instanceof Response || stdinSrc instanceof Request) void stdinSrc.body!.pipeTo(stdinWeb);
// @ts-expect-error account for Hono's Response monkeypatch
else if (stdinSrc.constructor.name === '_Response') void stdinSrc.body!.pipeTo(stdinWeb);
else if (typeof stdinSrc === 'number') void fs.createReadStream('', { fd: stdinSrc }).pipe(internalStdinStream);
else void stdinSrc;
});
@@ -343,18 +400,20 @@ export const spawnSync = ((...args): SyncSubprocess => {
opts = args[0];
Reflect.deleteProperty(opts, 'cmd');
}
if (opts.ipc) throw new NotImplementedError('Bun.spawnSync({ ipc })', spawn);
let stdio: StdioOptions = [];
opts.stdio ??= [undefined, undefined, undefined];
opts.stdio ??= ['pipe', 'pipe', 'pipe'];
if (opts.stdin) opts.stdio[0] = opts.stdin;
if (opts.stdout) opts.stdio[1] = opts.stdout;
if (opts.stderr) opts.stdio[2] = opts.stderr;
const ioNeedsPipeHandler: [ArrayBufferView | null, ArrayBufferView | null] = [null, null];
for (let i = 1; i < 3; i++) { // this intentionally skips stdin
let std = opts.stdio[i];
if (isArrayBufferView(std)) stdio[i] = streams.Readable.fromWeb(new Blob([std]).stream());
else if (std instanceof Blob || isFileBlob(std)) stdio[i] = streams.Readable.fromWeb(std.stream());
else if (std instanceof ReadableStream) stdio[i] = streams.Readable.fromWeb(std);
else if (std instanceof Response || std instanceof Request) stdio[i] = streams.Readable.fromWeb(std.body!);
let std = opts.stdio[i] as SpawnOptions.Readable;
if (isFileBlob(std)) stdio[i] = (Reflect.get(std, '@@toStream') as () => fs.WriteStream).call(std);
else if (isArrayBufferView(std)) {
stdio[i] = 'pipe';
ioNeedsPipeHandler[i - 1] = std;
}
else stdio[i] = std;
}
let input: ArrayBufferView | string | undefined;
@@ -371,11 +430,18 @@ export const spawnSync = ((...args): SyncSubprocess => {
const subp = chp.spawnSync(cmd, argv, {
cwd: opts.cwd ?? process.cwd(),
env: { ...(opts.env as Record<string, string> ?? process.env) },
stdio, input
stdio: 'pipe', input
}) as unknown as SyncSubprocess;
const subpAsNode = subp as unknown as SpawnSyncReturns<Buffer>;
if (subpAsNode.error) throw subpAsNode.error;
if (subpAsNode.stdout && ioNeedsPipeHandler[0]) {
const dest = new Uint8Array(ioNeedsPipeHandler[0].buffer);
dest.set(subpAsNode.stdout);
}
if (subpAsNode.stderr && ioNeedsPipeHandler[1]) {
const dest = new Uint8Array(ioNeedsPipeHandler[1].buffer);
dest.set(subpAsNode.stderr);
}
subp.exitCode = subpAsNode.status ?? NaN; //! not sure what Bun would return here (child killed by signal)
subp.success = subp.exitCode === 0;
return subp;
@@ -398,7 +464,39 @@ export const escapeHTML = ((input) => {
return out;
}) satisfies typeof Bun.escapeHTML;
export const readableStreamToArrayBuffer = ((stream: ReadableStream<ArrayBufferView | ArrayBufferLike>): ArrayBuffer | Promise<ArrayBuffer> => {
export const TOML = {
parse(input) {
// Bun's TOML parser seems highly non-compliant with the TOML spec and not very well tested,
// for instance it doesn't seem to support Dates and Times at all, and doesn't really handle big integers,
// the latter is a property smol-toml shares with Bun's parser, only erroring on values that are too big to fit in a JS number,
// rather than simply silently losing the precision like Bun currently does, which can lead to behavior differences in this polyfill.
// However most of this is caused by Bun's parser spec non-compliance, so this is an issue to be solved on Bun's native side.
return smol_toml.parse(input);
},
} satisfies typeof Bun.TOML;
export const semver = {
order(v1, v2) {
return node_semver.compare(v1.toString(), v2.toString());
},
satisfies(version, range) {
return node_semver.satisfies(version.toString(), range.toString());
},
} satisfies typeof Bun.semver;
export const readableStreamToFormData = (async (stream, boundary?) => {
if (boundary) {
if (typeof boundary !== 'string') boundary = new TextDecoder().decode(boundary);
// Keeping this comment in case it's a types load order case
// x@ts-expect-error @types/node Response parameters are missing ReadableStream but its supported.
return await new Response(stream, { headers: { 'content-type': `multipart/form-data; boundary="-${boundary}"` } }).formData() as FormData;
}
const fd = new FormData() as FormData;
new URLSearchParams(await readableStreamToText(stream)).forEach((v, k) => fd.set(k, v));
return fd;
}) satisfies typeof Bun.readableStreamToFormData;
export const readableStreamToArrayBuffer = ((stream) => {
return (async () => {
const sink = new ArrayBufferSink();
const reader = stream.getReader();
@@ -410,31 +508,35 @@ export const readableStreamToArrayBuffer = ((stream: ReadableStream<ArrayBufferV
return sink.end() as ArrayBuffer;
})();
}) satisfies typeof Bun.readableStreamToArrayBuffer;
export const readableStreamToText = (async (stream: ReadableStream<ArrayBufferView | ArrayBuffer>) => {
export const readableStreamToText = (async (stream) => {
let result = '';
const reader = stream.pipeThrough(new TextDecoderStream()).getReader(); ReadableStreamDefaultReader
// @ts-ignore Don't quite understand what's going wrong with these types but TextDecoderStream is supported here
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { done, value } = await reader.read();
//! for some reason "done" isnt being set to true so this is just infinitely looping at the moment... sigh
if (done || !value || !value?.length) break;
if (done) break;
result += value;
}
return result;
}) satisfies typeof Bun.readableStreamToText;
export const readableStreamToBlob = (async (stream: ReadableStream<any>) => {
export const readableStreamToBlob = (async (stream) => {
const parts = await readableStreamToArray(stream);
return new Blob(parts as BlobPart[]);
}) satisfies typeof Bun.readableStreamToBlob;
export const readableStreamToArray = (async <T = unknown>(stream: ReadableStream<T>) => {
const array = new Array<T>();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done || !value || !(<any>value)?.length) break;
if (done) break;
array.push(value as unknown as T);
}
return array;
}) satisfies typeof Bun.readableStreamToArray;
export const readableStreamToJSON = (async <T = unknown>(stream: ReadableStream<Uint8Array>) => {
const text = await readableStreamToText(stream);
try {
@@ -465,6 +567,144 @@ export const fileURLToPath = fileURLToPathNode satisfies typeof Bun.fileURLToPat
export const dns = dnsPolyfill satisfies typeof Bun.dns;
const Argon2Types = {
__proto__: null,
argon2d: argon2.argon2d,
argon2i: argon2.argon2i,
argon2id: argon2.argon2id,
} as const;
const syncAwareArgonHash = (async (password: string, algo: Password.Argon2Algorithm): Promise<Uint8Array> => {
const { workerData } = await import('node:worker_threads');
const argon2 = (await import(workerData.resolve.argon2)).default as typeof import('argon2');
return new TextEncoder().encode(await argon2.hash(password, {
type: workerData.Argon2Types[algo.algorithm] ?? (() => { throw new TypeError(`Invalid algorithm "${algo.algorithm}"`); })(),
memoryCost: algo.memoryCost ?? 65536,
timeCost: algo.timeCost ?? 2,
parallelism: 1,
version: 19,
}));
});
const syncAwareArgonVerify = (async (hash: string, password: string, algorithm: Password.AlgorithmLabel): Promise<Uint8Array> => {
const { workerData } = await import('node:worker_threads');
const argon2 = (await import(workerData.resolve.argon2)).default as typeof import('argon2');
return new Uint8Array([+await argon2.verify(hash, password, {
type: workerData.Argon2Types[algorithm] ?? (() => { throw new TypeError(`Invalid algorithm "${algorithm}"`); })(),
parallelism: 1,
version: 19,
})]);
});
export const password = {
hash(password, algorithm = 'argon2id') {
if (typeof password !== 'string') password = new TextDecoder().decode(password);
if (!password) throw new Error('password must not be empty');
const algo: Password.Argon2Algorithm | Password.BCryptAlgorithm = typeof algorithm === 'string' ? { algorithm } : algorithm;
if (algo.algorithm === 'bcrypt') {
algo.cost ??= 10;
if (algo.cost < 4 || algo.cost > 31) throw new TypeError('cost must be between 4 and 31');
if (password.length > 72) password = new TextDecoder().decode(SHA512.hash(password) as unknown as Uint8Array);
return bcrypt.hash(password, algo.cost);
} else {
const argonType = Argon2Types[algo.algorithm];
if (argonType === undefined) throw new TypeError(`Invalid algorithm "${algo.algorithm}"`);
algo.timeCost ??= 2;
algo.memoryCost ??= 64;
algo.memoryCost *= 1024;
if (algo.memoryCost < 1024 || algo.memoryCost > 0xFFFFFFFF)
throw new TypeError(`memoryCost must be between 1 and 0x3FFFFF (got ${algo.memoryCost})`);
if (!Number.isSafeInteger(algo.timeCost) || algo.timeCost! < 0) throw new TypeError('timeCost must be a positive safe integer');
return argon2.hash(password, {
type: argonType,
memoryCost: algo.memoryCost,
timeCost: algo.timeCost,
parallelism: 1,
version: 19,
});
}
},
hashSync(password, algorithm = 'argon2id') {
if (typeof password !== 'string') password = new TextDecoder().decode(password);
if (!password) throw new Error('password must not be empty');
const algo: Password.Argon2Algorithm | Password.BCryptAlgorithm = typeof algorithm === 'string' ? { algorithm } : algorithm;
if (algo.algorithm === 'bcrypt') {
algo.cost ??= 10;
if (algo.cost < 4 || algo.cost > 31) throw new TypeError('cost must be between 4 and 31');
if (password.length > 72) password = new TextDecoder().decode(SHA512.hash(password) as unknown as Uint8Array);
return bcrypt.hashSync(password, algo.cost ?? 10);
} else {
if (Argon2Types[algo.algorithm] === undefined) throw new TypeError(`Invalid algorithm "${algo.algorithm}"`);
algo.timeCost ??= 2;
algo.memoryCost ??= 64;
algo.memoryCost *= 1024;
if (algo.memoryCost < 1024 || algo.memoryCost > 0xFFFFFFFF)
throw new TypeError(`memoryCost must be between 1 and 0x3FFFFF (got ${algo.memoryCost})`);
if (!Number.isSafeInteger(algo.timeCost) || algo.timeCost < 0) throw new TypeError('timeCost must be a positive safe integer');
const requireModules = { argon2: pathToFileURL(require.resolve('argon2')).href };
// TODO: use import.meta.resolve once its unflagged and stable
//const modules = { argon2: import.meta.resolve?.('argon2') ?? '' };
const worker = new SyncWorker(requireModules, { Argon2Types });
const out = worker.sync(syncAwareArgonHash, (data) => new TextDecoder().decode(data))(password, algo);
worker.terminate();
return out;
}
},
verify(password, hash, algorithm = 'argon2id') {
if (typeof password !== 'string') password = new TextDecoder().decode(password);
if (typeof hash !== 'string') hash = new TextDecoder().decode(hash);
if (arguments.length < 2) throw new Error('password and hash must not be empty');
if (!password || !hash) return Promise.resolve(false);
if (hash[0] !== '$') throw new TypeError('Invalid hash');
if (algorithm === 'bcrypt') {
return bcrypt.compare(password, hash);
} else {
const argonType = Argon2Types[algorithm];
if (argonType === undefined) throw new TypeError(`Invalid algorithm "${algorithm}"`);
return argon2.verify(hash, password, {
type: argonType,
parallelism: 1,
version: 19,
});
}
},
verifySync(password, hash, algorithm = 'argon2id') {
if (typeof password !== 'string') password = new TextDecoder().decode(password);
if (typeof hash !== 'string') hash = new TextDecoder().decode(hash);
if (arguments.length < 2) throw new Error('password and hash must not be empty');
if (!password || !hash) return false;
if (hash[0] !== '$') throw new TypeError('Invalid hash');
if (algorithm === 'bcrypt') {
return bcrypt.compareSync(password, hash);
} else {
if (Argon2Types[algorithm] === undefined) throw new TypeError(`Invalid algorithm "${algorithm}"`);
const requireModules = { argon2: pathToFileURL(require.resolve('argon2')).href };
// TODO: use import.meta.resolve once its unflagged and stable
//const modules = { argon2: import.meta.resolve?.('argon2') ?? '' };
const worker = new SyncWorker(requireModules, { Argon2Types });
const out = worker.sync(syncAwareArgonVerify, (data) => !!data[0])(hash, password, algorithm);
worker.terminate();
return out;
}
},
} satisfies typeof Bun.password;
export const deepEquals = ((a, b) => {
try {
expect(a).toEqual(b);
} catch {
return false;
}
return true;
}) satisfies typeof Bun.deepEquals;
export const deepMatch = ((a, b) => {
try {
expect(b).toMatchObject(a as Record<string, unknown>);
} catch {
return false;
}
return true;
}) satisfies typeof Bun.deepMatch;
export const isMainThread = workers.isMainThread satisfies typeof Bun.isMainThread;
//! It may be possible to implement plugins with Node ESM loaders, but it would take some effort and have some caveats.
@@ -477,6 +717,9 @@ const dummyPluginBuilder: PluginBuilder = ({
onResolve(constraints: PluginConstraints, callback: OnResolveCallback): void {
return; // stubbed
},
module(specifier: string, callback) {
return; // stubbed
},
config: { plugins: [], entrypoints: [] },
}) satisfies PluginBuilder;
const bunPlugin = <T extends BunPlugin>(options: T) => options?.setup?.(dummyPluginBuilder) as ReturnType<T['setup']>;
@@ -498,3 +741,5 @@ export const plugin = bunPlugin satisfies typeof Bun.plugin;
});
}
});*/
export * as default from './bun.js';

View File

@@ -1,8 +1,9 @@
import type { DNSLookup } from 'bun';
import dns from 'node:dns';
const dnsObj: typeof Bun.dns = {
async lookup(hostname, options) {
const opts = { verbatim: true, all: true } as dns.LookupOptions;
const opts = { verbatim: true, all: true, family: 0, hints: 0 } as Required<dns.LookupOptions>;
if (options?.family) {
if (options.family === 'IPv4') opts.family = 4;
else if (options.family === 'IPv6') opts.family = 6;
@@ -10,10 +11,18 @@ const dnsObj: typeof Bun.dns = {
else opts.family = options.family;
}
if (options?.flags) opts.hints = options.flags;
const records = ((await dns.promises.resolveAny(hostname))
.filter(r => r.type === 'A' || r.type === 'AAAA') as (dns.AnyARecord | dns.AnyAaaaRecord)[])
.map(r => ({ address: r.address, family: r.type === 'A' ? 4 as const : 6 as const, ttl: r.ttl }));
return records;
opts.hints |= dns.V4MAPPED;
const records = await dns.promises.lookup(hostname, opts) as dns.LookupAddress[];
return await Promise.all(records.map(async r => {
const record = r as DNSLookup;
try {
record.ttl = ((await dns.promises[`resolve${record.family}`](hostname, { ttl: true })) as dns.RecordWithTtl[])
.find(r => r.address === record.address)?.ttl ?? 0;
} catch {
record.ttl = 0;
}
return record;
}));
},
// This has more properties but they're not documented on bun-types yet, oh well.
};

View File

@@ -4,7 +4,7 @@ import streams from 'node:stream';
import { ReadableStream as NodeWebReadableStream } from 'node:stream/web';
import { FileSink } from './filesink.js';
import { SystemError } from '../../utils/errors.js';
import type { FileBlob as BunFileBlob, FileSink as BunFileSink } from 'bun';
import type { BunFile, FileBlob as BunFileBlob, FileSink as BunFileSink } from 'bun';
type NodeJSStream = streams.Readable | streams.Writable;
@@ -28,6 +28,7 @@ export const NodeJSStreamFileBlob = class FileBlob extends Blob {
constructor(source: NodeJSStream, slice: [number?, number?] = [undefined, undefined], type = 'application/octet-stream') {
super(undefined, { type });
Reflect.deleteProperty(this, 'size');
Object.defineProperty(this, '@@isFileBlob', { value: true });
if (source === process.stdout || source === process.stdin || source === process.stderr) {
this.#iostream = true;
}
@@ -42,10 +43,10 @@ export const NodeJSStreamFileBlob = class FileBlob extends Blob {
readonly #slice: [number?, number?];
#size: number;
slice(begin?: number, end?: number, contentType?: string): Blob;
slice(begin?: number, contentType?: string): Blob;
slice(contentType?: string): Blob;
slice(beginOrType?: number | string, endOrType?: number | string, contentType: string = this.type): Blob {
slice(begin?: number, end?: number, contentType?: string): BunFile;
slice(begin?: number, contentType?: string): BunFile;
slice(contentType?: string): BunFile;
slice(beginOrType?: number | string, endOrType?: number | string, contentType: string = this.type): BunFile {
if (typeof beginOrType === 'string') return new FileBlob(this.#source, this.#slice, beginOrType);
if (typeof endOrType === 'string') return new FileBlob(this.#source, [beginOrType, undefined], endOrType);
return new FileBlob(this.#source, [beginOrType, endOrType], contentType);
@@ -83,16 +84,29 @@ export const NodeJSStreamFileBlob = class FileBlob extends Blob {
return JSON.parse(await this.text()) as Promise<TJSONReturnType>;
}
readonly lastModified: number = Date.now();
readable: ReadableStream<any> = undefined as any; //? broken on bun's side
async exists(): Promise<boolean> {
return false; // Yes Bun returns false for these at the time of writing
}
writer(): BunFileSink {
if (!this.#readable && !this.#iostream) throw new Error('Cannot get writer for a non-readable stream');
// @ts-expect-error stream types are just too annoying to make TS happy here but it works at runtime
return new FileSink(this.#source);
}
override get size(): number { return this.#size; }
override set size(_) { return; }
};
export class FileBlob extends Blob implements BunFileBlob {
constructor(fdOrPath: number | string, opts: BlobPropertyBag = {}) {
constructor(fdOrPath: number | string | URL, opts: BlobPropertyBag = {}) {
opts.type ??= 'application/octet-stream'; // TODO: Get MIME type from file extension
super(undefined, opts);
Reflect.deleteProperty(this, 'size');
if (Reflect.get(opts, '__data')) this.#data = Reflect.get(opts, '__data') as Blob;
Object.defineProperty(this, '@@isFileBlob', { value: true });
const slice = Reflect.get(opts, '__slice') as [number?, number?] | undefined;
if (slice) {
slice[0] &&= slice[0] | 0; // int cast
@@ -104,52 +118,71 @@ export class FileBlob extends Blob implements BunFileBlob {
}
else if (slice[0] < 0 && slice[1] < 0) this.#sliceSize = -(slice[0] - slice[1]);
else if (slice[0] >= 0 && slice[1] >= 0) this.#sliceSize = slice[1] - slice[0];
}
if (typeof fdOrPath === 'string') try {
this.#fd = fs.openSync(fdOrPath, 'r+');
} catch (err) {
this.#error = err as SystemError;
Object.defineProperty(this, '@@writeSlice', { value: this.#sliceSize - slice[0] });
}
else {
this.#fd = fdOrPath;
this.#error = Reflect.get(opts, '__error') as SystemError | undefined;
}
if (!this.#error) {
const rstream = fs.createReadStream('', { fd: this.#fd, start: this.#slice[0], end: this.#slice[1] });
this.#readable = streams.Readable.toWeb(rstream);
this.#fdOrPath = fdOrPath;
this.#instancedTime = Date.now();
try {
this.#instancedSize = typeof this.#fdOrPath === 'number'
? fs.fstatSync(this.#fdOrPath).size
: fs.statSync(this.#fdOrPath).size;
} catch {
this.#instancedSize = 0;
}
this.name = typeof fdOrPath === 'string' ? fdOrPath : (
// for now, this seems to be what Bun does, but this is problematic for Windows, so we'll see how this goes
fdOrPath instanceof URL ? fdOrPath.pathname : undefined
);
}
readonly #readable?: NodeWebReadableStream;
readonly #error?: SystemError;
readonly #instancedTime: number;
readonly #instancedSize: number;
readonly #slice: [number?, number?] = [];
readonly #sliceSize: number = 0;
readonly #fd: number = NaN;
#data?: Blob;
#fdOrPath: string | number | URL;
readonly name?: string;
#read() {
if (this.#error) throw this.#error;
const read = fs.readFileSync(this.#fd);
this.#data = new Blob([read.subarray(...this.#slice)], { type: this.type });
//! package-internal use only
protected ['@@toStream']() {
const fd = typeof this.#fdOrPath === 'number' ? this.#fdOrPath : fs.openSync(this.#fdOrPath, 'w+');
const wstream = fs.createWriteStream('', { fd, start: this.#slice[0] });
return wstream;
}
//! Bun 0.2 seems to return undefined for this, this might not be accurate or it's broken on Bun's side
#read(): Blob {
const read = fs.readFileSync(this.#fdOrPath);
return new Blob([read.subarray(...this.#slice)], { type: this.type });
}
//! Bun seems to return undefined for this, this might not be accurate or it's broken on Bun's side
get readable(): ReadableStream<any> {
if (this.#error) throw this.#error;
return this.#readable! as ReadableStream;
return undefined as any;
//const fd = typeof this.#pathlikeOrFd === 'number' ? this.#pathlikeOrFd : fs.openSync(this.#pathlikeOrFd, 'r');
//const rstream = fs.createReadStream('', { fd, start: this.#slice[0], end: this.#slice[1] });
//return streams.Readable.toWeb(rstream);
}
get lastModified(): number {
if (this.#error) throw this.#error;
return fs.fstatSync(this.#fd).mtimeMs;
try {
return typeof this.#fdOrPath === 'number'
? fs.fstatSync(this.#fdOrPath).mtimeMs
: fs.statSync(this.#fdOrPath).mtimeMs;
} catch {
return this.#instancedTime; // Bun seems to fallback to when the Bun.file was created
}
}
async exists(): Promise<boolean> {
return !this.#error;
try {
if (typeof this.#fdOrPath !== 'number') return fs.statSync(this.#fdOrPath).isFile();
return fs.fstatSync(this.#fdOrPath).isFile();
} catch {
return false;
}
}
writer(): BunFileSink {
if (this.#error) throw this.#error;
return new FileSink(this.#fd);
const fdOrPath = this.#fdOrPath;
return new FileSink(typeof fdOrPath === 'string' || fdOrPath instanceof URL ? fs.openSync(fdOrPath, 'w+') : fdOrPath);
}
// TODO: what's contentType?
@@ -162,34 +195,28 @@ export class FileBlob extends Blob implements BunFileBlob {
contentType = end;
end = undefined;
}
return new FileBlob(this.#fd, {
__error: this.#error,
return new FileBlob(this.#fdOrPath, {
__slice: [begin, end],
__data: this.#data?.slice(begin, end),
} as BlobPropertyBag);
}
override arrayBuffer(): Promise<ArrayBuffer> {
if (!this.#data) this.#read();
return new Blob([this.#data ?? '']).arrayBuffer();
return new Blob([this.#read() ?? '']).arrayBuffer();
}
override text(): Promise<string> {
if (!this.#data) this.#read();
return new Blob([this.#data ?? '']).text();
return new Blob([this.#read() ?? '']).text();
}
override json(): Promise<any>;
override json<TJSONReturnType = unknown>(): Promise<TJSONReturnType>;
override json<TJSONReturnType = unknown>(): Promise<TJSONReturnType> | Promise<any> {
if (!this.#data) this.#read();
return new Blob([this.#data ?? '']).json();
return new Blob([this.#read() ?? '']).json();
}
override stream(): NodeJS.ReadableStream;
override stream(): ReadableStream<Uint8Array>;
override stream(): ReadableStream<Uint8Array> | NodeJS.ReadableStream {
if (!this.#data) this.#read();
return new Blob([this.#data ?? '']).stream();
return new Blob([this.#read() ?? '']).stream();
}
override get size(): number {
return this.#data?.size ?? (this.#sliceSize || 0);
return this.#instancedSize <= (this.#sliceSize || 0) ? this.#sliceSize : this.#instancedSize;
}
}

View File

@@ -1,7 +1,7 @@
import type { CryptoHashInterface, DigestEncoding, Hash } from 'bun';
import type { CryptoHashInterface, DigestEncoding, Hash, SupportedCryptoAlgorithms, CryptoHasher as BunCryptoHasher } from 'bun';
import nodecrypto from 'node:crypto';
import os from 'node:os';
import md4, { Md4 } from 'js-md4';
import md4, { type Md4 } from 'js-md4';
import { wyhash, adler32, crc32, cityhash32, cityhash64, murmur32v3, murmur64v2, murmur32v2 } from '../../../lib/zighash/index.mjs';
export const bunHash = ((data, seed = 0): bigint => wyhash(data, BigInt(seed))) as typeof Bun.hash;
@@ -69,20 +69,48 @@ abstract class BaseHash<T> implements CryptoHashInterface<T> {
}
return encodingOrHashInto;
}
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray { return '' };
static readonly byteLength: number;
abstract readonly byteLength: number;
}
export class CryptoHasher extends BaseHash<CryptoHasher> implements BunCryptoHasher {
constructor(algorithm: SupportedCryptoAlgorithms) {
super(algorithm);
this.algorithm = algorithm;
this.byteLength = nodecrypto.createHash(algorithm).digest().byteLength;
}
#state: StringOrBuffer[] = [];
byteLength: number;
algorithm: SupportedCryptoAlgorithms;
static algorithms: SupportedCryptoAlgorithms[] = nodecrypto.getHashes() as SupportedCryptoAlgorithms[];
override update(data: StringOrBuffer): CryptoHasher {
this.#state.push(data);
return super.update(data);
}
static hash(algo: SupportedCryptoAlgorithms, data: StringOrBuffer, encoding?: DigestEncoding | undefined): string;
static hash(algo: SupportedCryptoAlgorithms, data: StringOrBuffer, hashInto?: TypedArray | undefined): TypedArray;
static hash(algo: SupportedCryptoAlgorithms, data: StringOrBuffer, encodingOrHashInto?: TypedArray | DigestEncoding | undefined): string | TypedArray {
const instance = new this(algo); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
copy(): BunCryptoHasher {
const copy = new CryptoHasher(this.algorithm);
copy.#state = this.#state.slice();
for (const state of this.#state) copy.update(state);
return copy;
}
}
export class SHA1 extends BaseHash<SHA1> {
constructor() { super('sha1'); }
static override readonly byteLength = 20;
override readonly byteLength = 20;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
@@ -110,9 +138,9 @@ export class MD4 extends BaseHash<MD4> {
}
static override readonly byteLength = 16;
override readonly byteLength = 16;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
@@ -121,9 +149,9 @@ export class MD5 extends BaseHash<MD5> {
constructor() { super('md5'); }
static override readonly byteLength = 16;
override readonly byteLength = 16;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
@@ -132,9 +160,9 @@ export class SHA224 extends BaseHash<SHA224> {
constructor() { super('sha224'); }
static override readonly byteLength = 28;
override readonly byteLength = 28;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
@@ -143,9 +171,9 @@ export class SHA512 extends BaseHash<SHA512> {
constructor() { super('sha512'); }
static override readonly byteLength = 64;
override readonly byteLength = 64;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
@@ -154,9 +182,9 @@ export class SHA384 extends BaseHash<SHA384> {
constructor() { super('sha384'); }
static override readonly byteLength = 48;
override readonly byteLength = 48;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
@@ -165,9 +193,9 @@ export class SHA256 extends BaseHash<SHA256> {
constructor() { super('sha256'); }
static override readonly byteLength = 32;
override readonly byteLength = 32;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}
@@ -176,9 +204,9 @@ export class SHA512_256 extends BaseHash<SHA512_256> {
constructor() { super('sha512-256'); }
static override readonly byteLength = 32;
override readonly byteLength = 32;
static override hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static override hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static override hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
static hash(data: StringOrBuffer, encoding?: DigestEncoding): string;
static hash(data: StringOrBuffer, hashInto?: TypedArray): TypedArray;
static hash(data: StringOrBuffer, encodingOrHashInto?: DigestEncoding | TypedArray): string | TypedArray {
const instance = new this(); instance.update(data);
return instance.digest(encodingOrHashInto as DigestEncoding & TypedArray);
}

View File

@@ -0,0 +1,33 @@
import fs from 'fs';
type mmapper = typeof import('mmap-utils')['default'];
type MapProtectionFlags = Parameters<mmapper['map']>[1];
let mmapper: mmapper | null = null;
try {
// TS is having some trouble resolving these types properly, it thinks the module is on .default.default (???)
mmapper = (await import('mmap-utils')).default as unknown as mmapper;
} catch {
// Error will be thrown when mmap is used
}
//? The opts object may also support "size" and "offset" properties, but these are not documented in bun-types yet.
export const mmap: typeof Bun.mmap = (path, opts = {}): Uint8Array => {
if (!mmapper) {
const err = new Error('Bun.mmap is not available due to uninitialized mmapper dependency.');
Error.captureStackTrace(err, mmap);
throw err;
}
if (opts.shared === undefined) opts.shared = true;
if (opts.sync === undefined) opts.sync = false;
//? The sync option is ignored by Bun on MacOS and errors on Linux, so might as well just ignore it for now.
//if (opts.sync) throw new NotImplementedError('Bun.mmap(..., { sync: true })', mmap);
const fd = fs.openSync(path as fs.PathLike, 'r+');
const size = fs.fstatSync(fd).size;
return new Uint8Array(mmapper.map(
size,
<MapProtectionFlags>(mmapper.PROT_READ | mmapper.PROT_WRITE),
opts.shared ? mmapper.MAP_SHARED : mmapper.MAP_PRIVATE,
fd
).buffer);
};

View File

@@ -0,0 +1,235 @@
/// <reference types="node" />
import type {
Serve, TLSServeOptions, UnixTLSServeOptions, TLSWebSocketServeOptions, UnixTLSWebSocketServeOptions,
Server as BunServer, ArrayBufferView, SocketAddress, WebSocketHandler, ServerWebSocket, WebSocketCompressor
} from 'bun';
import { serve as honoServe } from '@hono/node-server';
import { WebSocketServer, type AddressInfo } from 'ws';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import http from 'node:http';
import { requestRemoteIPSymbol, requestUpgradedSymbol, toWebRequest } from '../../utils/webconv.js';
export function serve<T>(options: Serve<T>): BunServer {
return new Server(options);
}
serve satisfies typeof Bun.serve;
class Server<T = undefined> implements BunServer {
constructor(options: Serve<T>) {
const listenOn = ('unix' in options && typeof options.unix === 'string')
? { hostname: '', port: undefined, unix: options.unix }
: {
hostname: String(options.hostname ?? '0.0.0.0'),
port: +(options.port ?? process.env.PORT ?? 3000),
};
const ws = 'websocket' in options ? options.websocket : null;
const tls = isTLS(options) ? options : null;
this.development = !!options.development ?? process.env.NODE_ENV !== 'production';
this.hostname = listenOn.hostname ?? '';
this.id = options.id ?? '';
this.url = new URL(`${tls ? 'https' : 'http'}://${listenOn.hostname || 'localhost'}:${listenOn.port ?? 0}`); // TODO
// missing from bun-types (?) + untested if these values are right yet
this.protocol = ws ? (tls ? 'wss' : 'ws') : (tls ? 'https' : 'http');
// privates
this.#ws = ws;
this.#tls = tls;
//this.#unix = listenOn.unix;
this.#maxReqBodySize = +(options.maxRequestBodySize || 128 * 1024 * 1024);
this.#onError = options.error;
this.#onRequest = options.fetch;
if (!this.#onRequest) throw new TypeError('Expected fetch() to be a function');
if (tls?.ca instanceof Blob) tls.ca = tls.ca.name;
if (tls?.ca instanceof Array) tls.ca = tls.ca.map((ca) => ca instanceof Blob ? ca.name! : ca);
if (tls?.cert instanceof Blob) tls.cert = tls.cert.name;
if (tls?.cert instanceof Array) tls.cert = tls.cert.map((cert) => cert instanceof Blob ? cert.name! : cert);
if (tls?.key instanceof Blob) tls.key = tls.key.name;
if (tls?.key instanceof Array) tls.key = tls.key.map((key) => key instanceof Blob ? key.name! : key);
this.#server = honoServe({
serverOptions: {
ca: tls?.ca as string | Buffer | (string | Buffer)[] | undefined,
cert: tls?.cert as string | Buffer | (string | Buffer)[] | undefined,
dhparam: tls?.dhParamsFile ? fs.readFileSync(tls.dhParamsFile) : undefined,
key: tls?.key as string | Buffer | (string | Buffer)[] | undefined,
passphrase: tls?.passphrase,
},
hostname: listenOn.hostname,
port: listenOn.port,
fetch: async (request) => {
this.pendingRequests++;
const response = await this.#onRequest(request, this);
this.pendingRequests--;
return response;
},
}, (info) => { }) as http.Server;
this.#server.listen(listenOn.port, listenOn.hostname);
this.#server.on('error', (error) => {
if (this.#onError) this.#onError(error);
});
this.#server.on('upgrade', (req, duplex, head) => {
this.#onRequest(toWebRequest(req, undefined, this.#maxReqBodySize, true), this);
});
this.#wss = new WebSocketServer({
server: this.#server,
perMessageDeflate: typeof ws?.perMessageDeflate === 'boolean'
? ws.perMessageDeflate : !!ws?.perMessageDeflate?.compress || !!ws?.perMessageDeflate?.decompress,
backlog: ws?.backpressureLimit,
// @ts-expect-error untyped "maxPayload" option but it's in the docs
maxPayload: ws?.maxPayloadLength,
});
this.#wss.on('connection', (socket, req) => {
this.pendingWebSockets++;
this.#ws?.open?.(toBunSocket(socket, this));
if (this.#ws?.close) socket.onclose = (event) => {
this.#ws?.close?.(toBunSocket(socket, this), event.code, event.reason);
this.pendingWebSockets--;
};
if (this.#ws?.message) socket.onmessage = (event) => this.#ws?.message?.(toBunSocket(socket, this), event.data);
if (this.#ws?.ping) socket.addEventListener('ping', (event) => this.#ws?.ping?.(toBunSocket(socket, this), event.data));
if (this.#ws?.pong) socket.addEventListener('pong', (event) => this.#ws?.pong?.(toBunSocket(socket, this), event.data));
});
}
#wss: WebSocketServer;
#server: http.Server;
#ws: WebSocketHandler<T> | null;
#tls: TLSOptions<T> | null;
//#unix?: string;
#maxReqBodySize: number;
#onError?: Serve<T>['error'];
#onRequest: Serve<T>['fetch'];
#closed = false;
development: boolean;
hostname: string;
get port(): number {
const addrinfo = this.#server.address();
const port = typeof addrinfo === 'string' ? -1 : addrinfo?.port!;
return port === -1 ? undefined as unknown as number : port;
}
id: string;
url: URL;
protocol: string; //? see note in constructor
pendingRequests = 0;
pendingWebSockets = 0;
fetch(request: string | Request): Response | Promise<Response> {
if (typeof request === 'string') request = new Request(request);
return this.#onRequest(request, this) as Response | Promise<Response>;
}
publish(topic: string, data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer, compress?: boolean): number {
this.#wss.clients.forEach((client) => {
if (client.readyState !== 1) return;
const bunSocket = Reflect.get(client, '@@asBunSocket') as BunSocket<T> | undefined;
if (!bunSocket) throw new Error('Internal error: Expected client to have a BunSocket reference');
if (bunSocket.isSubscribed(topic)) bunSocket.send(data, compress);
});
return 0;
}
upgrade<T = undefined>(request: Request, options?: { headers?: HeadersInit; data?: T; }): boolean {
return Reflect.get(request, requestUpgradedSymbol) ?? false;
}
requestIP(request: Request): SocketAddress | null {
const addrinfo = Reflect.get(request, requestRemoteIPSymbol) as AddressInfo & { family: 'IPv4' | 'IPv6'; } | undefined;
if (addrinfo) return addrinfo;
else return null;
}
reload(options: Serve): void {
this.#onRequest = options.fetch ?? this.#onRequest;
this.#onError = options.error ?? this.#onError;
}
stop(closeActiveConnections?: boolean): void {
this.#closed = true;
if (closeActiveConnections) this.#wss.clients.forEach((client) => client.close());
this.#wss.close();
this.#server.close();
}
};
type TLSOptions<T> = TLSServeOptions | UnixTLSServeOptions | TLSWebSocketServeOptions<T> | UnixTLSWebSocketServeOptions<T>;
function isTLS<T>(options: Serve<T>): options is TLSOptions<T> {
return (
'tls' in options || 'serverNames' in options
|| 'keyFile' in options || 'certFile' in options || 'caFile' in options
|| 'key' in options || 'cert' in options || 'ca' in options
|| 'passphrase' in options || 'dhParamsFile' in options
|| 'serverName' in options || 'lowMemoryMode' in options || 'secureOptions' in options
);
}
function generateSecWSAccept(secWSKey: string) {
return createHash('sha1')
.update(secWSKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
.digest('base64');
}
class BunSocket<T extends any> implements ServerWebSocket {
#ws: WebSocket;
#server: Server<T>;
constructor(socket: WebSocket, server: Server<T>) {
this.#ws = socket;
this.#server = server;
Reflect.set(socket, '@@asBunSocket', this);
}
send(data: string | BufferSource, compress?: boolean | undefined): number {
this.#ws.send(data);
return typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : data.byteLength;
}
sendText(data: string, compress?: boolean | undefined): number {
this.#ws.send(data);
return Buffer.byteLength(data, 'utf8');
}
sendBinary(data: BufferSource, compress?: boolean | undefined): number {
this.#ws.send(data);
return data.byteLength;
}
close(code?: number | undefined, reason?: string | undefined): void {
this.#ws.close(code, reason);
}
terminate(): void {
this.#ws.terminate();
}
ping(data?: string | BufferSource | undefined): number {
this.#ws.ping(data);
return typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : data?.byteLength ?? 0;
}
pong(data?: string | BufferSource | undefined): number {
this.#ws.pong(data);
return typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : data?.byteLength ?? 0;
}
publish(topic: string, data: string | BufferSource, compress?: boolean | undefined): number {
return this.#server.publish(topic, data, compress);
}
publishText(topic: string, data: string, compress?: boolean | undefined): number {
return this.publish(topic, data, compress);
}
publishBinary(topic: string, data: BufferSource, compress?: boolean | undefined): number {
return this.publish(topic, data, compress);
}
subscribe(topic: string): void {
this.#subscribedTopics.add(topic);
}
unsubscribe(topic: string): void {
this.#subscribedTopics.delete(topic);
}
isSubscribed(topic: string): boolean {
return this.#subscribedTopics.has(topic);
}
cork(callback: (ws: ServerWebSocket<T>) => T): T {
return callback(this);
}
get remoteAddress(): string {
return this.#ws.url;
};
get readyState(): WebSocketReadyState {
return this.#ws.readyState;
};
#subscribedTopics = new Set<string>();
binaryType?: 'nodebuffer' | 'arraybuffer' | 'uint8array' | undefined;
// @ts-expect-error generic mess
data: T;
}
function toBunSocket<T>(socket: WebSocket, server: Server<T>) {
return new BunSocket<T>(socket, server);
}

View File

@@ -0,0 +1,84 @@
import type Bun from 'bun';
import net from 'node:net';
export async function connect<T = undefined>(options: Bun.TCPSocketConnectOptions<T> | Bun.UnixSocketOptions<T>): Promise<Bun.Socket<T>> {
return new Socket<T>(options);
}
connect satisfies typeof Bun.connect;
export class Socket<T = undefined> extends net.Socket implements Bun.Socket<T> {
constructor(options: Bun.TCPSocketConnectOptions<T> | Bun.UnixSocketOptions<T>) {
super();
this.data = options.data!;
try {
if (options.socket.close) this.on('close', (hadError) => options.socket.close!(this));
if (options.socket.error) this.on('error', (err) => options.socket.error!(this, err));
if (options.socket.end) this.on('end', () => options.socket.end!(this));
if (options.socket.drain) this.on('drain', () => options.socket.drain!(this));
if (options.socket.data) this.on('data', (buf) => {
let data: SharedArrayBuffer | ArrayBuffer | Uint8Array | Buffer = buf;
if (options.socket.binaryType === 'arraybuffer') data = buf.buffer;
else if (options.socket.binaryType === 'uint8array') data = new Uint8Array(buf.buffer);
options.socket.data!(this, data as Buffer);
});
if (options.socket.open) this.on('ready', () => options.socket.open!(this));
if (options.socket.timeout) this.on('timeout', () => options.socket.timeout!(this));
if (options.socket.handshake) throw new Error('Handshake not implemented'); // tls.TLSSocket 'secureConnection' event
if ('unix' in options) this.connect({ path: options.unix });
else this.connect({ port: options.port, host: options.hostname }); // TODO: options.tls
} catch (err) {
if (options.socket.connectError) options.socket.connectError(this, err as Error);
throw err;
}
}
shutdown(halfClose?: boolean): void {
this.allowHalfOpen = halfClose ?? false;
this.end();
}
flush(): void { /* no-op */ }
reload(handler: Bun.SocketHandler<unknown, 'buffer'>): void {
// TODO
// This more or less just acts as a configuration changer, which node sockets can do on the fly without a full reload.
throw new Error('Method not implemented.');
}
// @ts-expect-error impossible to make TS happy here, it gets torn between "extends net.Socket" and "implements Bun.Socket"
override write(data: string | BufferSource, byteOffset: number = 0, byteLength?: number): number {
const toWrite = typeof data === 'string'
? data.substr(byteOffset, byteLength)
: new Uint8Array(
('buffer' in data ? data.buffer.slice(data.byteOffset, data.byteLength) : data).slice(byteOffset, byteLength && byteOffset + byteLength)
);
return super.write(toWrite), toWrite.length;
}
// @ts-expect-error impossible to make TS happy here, it gets torn between "extends net.Socket" and "implements Bun.Socket"
override end(data?: string | BufferSource, byteOffset?: number, byteLength?: number): number;
// @ts-expect-error ^
override end(): void;
// @ts-expect-error ^
override end(data?: string | BufferSource, byteOffset: number = 0, byteLength?: number): number | void {
if (!data) return void super.end();
const toWrite = typeof data === 'string'
? data.substr(byteOffset, byteLength)
: new Uint8Array(
('buffer' in data ? data.buffer.slice(data.byteOffset, data.byteLength) : data).slice(byteOffset, byteLength && byteOffset + byteLength)
);
return super.end(toWrite), toWrite.length;
}
// @ts-expect-error impossible to make TS happy here, it gets torn between "extends net.Socket" and "implements Bun.Socket"
override timeout(seconds: number): void {
super.setTimeout(seconds * 1000);
}
// @ts-expect-error impossible to make TS happy here, it gets torn between "extends net.Socket" and "implements Bun.Socket"
get readyState(): 'open' | 'closing' | 'closed' {
if (
super.readyState === 'open' ||
super.readyState === 'readOnly' ||
super.readyState === 'writeOnly'
) return 'open';
else return 'closed';
}
declare remoteAddress: string;
declare localPort: number;
data: T;
}

View File

@@ -0,0 +1,97 @@
import type Bun from 'bun';
import net from 'node:net';
export function listen<T = undefined>(options: Bun.TCPSocketListenOptions<T>): Bun.TCPSocketListener<T>;
export function listen<T = undefined>(options: Bun.UnixSocketOptions<T>): Bun.UnixSocketListener<T>;
export function listen<T = undefined>(options: Bun.TCPSocketListenOptions<T> | Bun.UnixSocketOptions<T>): Bun.TCPSocketListener<T> | Bun.UnixSocketListener<T> {
if ('unix' in options) return new UnixSocketListener<T>(options);
else return new TCPSocketListener<T>(options);
}
listen satisfies typeof Bun.listen;
class SocketListener<T = undefined> extends net.Server implements Bun.SocketListener<T> {
constructor(options: Bun.TCPSocketListenOptions<T> | Bun.UnixSocketOptions<T>) {
super();
this.data = options.data!;
this.on('drop', (data) => {
const socket = new net.Socket();
if (data) {
Object.defineProperty(socket, 'localPort', { value: data.localPort });
Object.defineProperty(socket, 'localFamily', { value: data.localFamily });
Object.defineProperty(socket, 'localAddress', { value: data.localAddress });
Object.defineProperty(socket, 'remotePort', { value: data.remotePort });
Object.defineProperty(socket, 'remoteFamily', { value: data.remoteFamily });
Object.defineProperty(socket, 'remoteAddress', { value: data.remoteAddress });
}
if (options.socket.connectError) options.socket.connectError(toBunSocket<T>(socket), new Error('Connection dropped'));
else throw new Error('Connection dropped');
});
this.on('connection', (socket) => {
this.#connections.add(socket);
socket.on('close', () => {
options.socket.close?.(toBunSocket<T>(socket));
this.#connections.delete(socket);
});
if (options.socket.error) socket.on('error', (err) => options.socket.error!(toBunSocket<T>(socket), err));
if (options.socket.end) socket.on('end', () => options.socket.end!(toBunSocket<T>(socket)));
if (options.socket.drain) socket.on('drain', () => options.socket.drain!(toBunSocket<T>(socket)));
if (options.socket.data) socket.on('data', (buf) => {
let data: SharedArrayBuffer | ArrayBuffer | Uint8Array | Buffer = buf;
if (options.socket.binaryType === 'arraybuffer') data = buf.buffer;
else if (options.socket.binaryType === 'uint8array') data = new Uint8Array(buf.buffer);
options.socket.data!(toBunSocket<T>(socket), data as Buffer);
});
if (options.socket.open) socket.on('ready', () => options.socket.open!(toBunSocket<T>(socket)));
if (options.socket.timeout) socket.on('timeout', () => options.socket.timeout!(toBunSocket<T>(socket)));
if (options.socket.handshake) throw new Error('Handshake not implemented'); // tls.TLSSocket 'secureConnection' event
});
if ('unix' in options) this.listen(options.unix);
else this.listen(options.port, options.hostname);
}
#connections: Set<net.Socket> = new Set();
stop(closeActiveConnections?: boolean): void {
if (closeActiveConnections) {
this.#connections.forEach((socket) => socket.destroy());
}
this.close();
}
reload(options: Pick<Partial<Bun.SocketOptions<unknown>>, 'socket'>): void {
// TODO
// This more or less just acts as a configuration changer, which node sockets can do on the fly without a full reload.
throw new Error('Method not implemented.');
}
data: T;
}
export class TCPSocketListener<T = undefined> extends SocketListener<T> implements Bun.TCPSocketListener<T> {
get port(): number {
const addrinfo = this.address();
if (addrinfo === null) return NaN;
if (typeof addrinfo === 'string') return Number(addrinfo.split(':').at(-1));
else return addrinfo.port;
}
get hostname(): string {
const addrinfo = this.address();
if (addrinfo === null) return '';
if (typeof addrinfo === 'string') return addrinfo.split(':')[0];
else return addrinfo.address;
}
}
export class UnixSocketListener<T = undefined> extends SocketListener<T> implements Bun.UnixSocketListener<T> {
get unix(): string {
const addrinfo = this.address();
if (addrinfo === null) return '';
if (typeof addrinfo === 'string') return addrinfo;
else return addrinfo.address + ':' + addrinfo.port;
}
};
// TODO
function toBunSocket<T>(socket: net.Socket) {
return socket as unknown as Bun.Socket<T>;
}

View File

@@ -1,6 +1,6 @@
import type { JavaScriptLoader, TranspilerOptions, Transpiler as BunTranspiler, Import } from 'bun';
import type { Message } from 'bun-wasm/schema';
import { transformSync, scan, init } from 'bun-wasm';
import { Message } from 'bun-wasm/schema';
import $ from 'chalk';
await init();
@@ -78,7 +78,7 @@ function formatBuildErrors(buildErrors: Message[], caller: Transpiler[keyof Tran
const loc = err.data.location;
const str = `${$.redBright('error')}${$.gray(':')} ${$.bold(err.data.text)}\n` +
(loc
? `${highlightErrorChar(loc.line_text, loc.offset)}\n` +
? `${highlightErrorChar(loc.line_text, loc.column)}\n` +
$.redBright.bold('^'.padStart(loc.column)) + '\n' +
`${$.bold(loc.file)}${$.gray(':')}${$.yellowBright(loc.line)}${$.gray(':')}${$.yellowBright(loc.column)} ${$.gray(loc.offset)}`
: ''

View File

@@ -0,0 +1,332 @@
import { endianness } from 'node:os';
import util from 'node:util';
import koffi from 'koffi';
import type bunffi from 'bun:ffi';
const LE = endianness() === 'LE';
koffi.alias('f32', 'float');
koffi.alias('f64', 'double');
koffi.alias('i8', 'int8_t');
koffi.alias('i16', 'int16_t');
koffi.alias('i32', 'int32_t');
koffi.alias('i64', 'int64_t');
koffi.alias('i64_fast', 'int64_t');
koffi.alias('u8', 'uint8_t');
koffi.alias('u16', 'uint16_t');
koffi.alias('u32', 'uint32_t');
koffi.alias('u64', 'uint64_t');
koffi.alias('u64_fast', 'uint64_t');
koffi.alias('usize', 'uint64_t');
koffi.alias('callback', 'void*');
koffi.alias('function', 'void*');
koffi.alias('cstring', 'uint8_t*');
koffi.alias('pointer', 'void*');
koffi.alias('ptr', 'void*');
export enum FFIType {
char = 0,
i8 = 1,
int8_t = 1,
u8 = 2,
uint8_t = 2,
i16 = 3,
int16_t = 3,
u16 = 4,
uint16_t = 4,
int = 5,
i32 = 5,
int32_t = 5,
u32 = 6,
uint32_t = 6,
i64 = 7,
int64_t = 7,
u64 = 8,
uint64_t = 8,
f64 = 9,
double = 9,
f32 = 10,
float = 10,
bool = 11,
ptr = 12,
pointer = 12,
void = 13,
cstring = 14,
i64_fast = 15,
u64_fast = 16,
function = 17,
};
FFIType satisfies typeof bunffi.FFIType;
function bunffiTypeToKoffiType(type: bunffi.FFITypeOrString = 'void'): string {
if (typeof type === 'number') return FFIType[type];
else return type;
}
/**
* Koffi/Node.js don't seem to have a way to get the pointer address of a value, so we have to track them ourselves,
* while also making up fake addresses for values that are created on the JS side, but ensuring that they're unique.
*/
const ptrsToValues = new Map<bunffi.Pointer, unknown>();
let fakePtr = 4;
export const suffix = (
process.platform === 'darwin' ? 'dylib' : (process.platform === 'win32' ? 'dll' : 'so')
) satisfies typeof bunffi.suffix;
export const dlopen = (<Fns extends Record<string, bunffi.Narrow<bunffi.FFIFunction>>>(name: string, symbols: Fns) => {
const lib = koffi.load(name);
const outsyms = {} as bunffi.ConvertFns<typeof symbols>;
for (const [sym, def] of Object.entries(symbols) as [string, bunffi.FFIFunction][]) {
const returnType = bunffiTypeToKoffiType(def.returns);
const argTypes = def.args?.map(bunffiTypeToKoffiType) ?? [];
const rawfn = lib.func(
sym,
returnType,
argTypes,
);
Reflect.set(
outsyms,
sym,
function (...args: any[]) {
args.forEach((arg, i) => {
if (typeof arg === 'number' && (argTypes[i] === 'ptr' || argTypes[i] === 'pointer')) {
const ptrVal = ptrsToValues.get(arg);
if (!ptrVal) throw new Error(
`Untracked pointer ${arg} in ffi function call ${sym}, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
args[i] = ptrVal;
}
});
const rawret = rawfn(...args);
if (returnType === 'function' || returnType === 'pointer' || returnType === 'ptr') {
const ptrAddr = Number(koffi.address(rawret));
ptrsToValues.set(ptrAddr, rawret);
return ptrAddr;
}
if (returnType === 'cstring') {
const ptrAddr = Number(koffi.address(rawret));
ptrsToValues.set(ptrAddr, rawret);
return new CString(ptrAddr);
}
if (['usize', 'u64', 'i64', 'uint64_t', 'int64_t'].includes(returnType)) {
return BigInt(rawret); // ensure BigInt
}
return rawret;
}
);
const fn = Reflect.get(outsyms, sym);
Reflect.set(fn, 'native', fn); // Not really accurate but should be fine... hopefully...
}
return {
close() { lib.unload(); },
symbols: outsyms,
};
}) satisfies typeof bunffi.dlopen;
export const linkSymbols = (<Fns extends Record<string, bunffi.Narrow<bunffi.FFIFunction>>>(symbols: Fns) => {
const linked = {} as bunffi.ConvertFns<typeof symbols>;
for (const [sym, def] of Object.entries(symbols) as [string, bunffi.FFIFunction][]) {
if (!def.ptr) throw new Error('ffi.linkSymbols requires a non-null pointer');
Reflect.set(linked, sym, CFunction(def as typeof def & { ptr: bunffi.Pointer; }));
}
return {
close() { },
symbols: linked,
};
}) satisfies typeof bunffi.linkSymbols;
export const viewSource = ((symsOrCb, isCb) => {
// Impossible to polyfill, but we preserve the important properties of the function:
// 1. Returns string if the 2nd argument is true, or an array of strings if it's false/unset.
// 2. The string array has the same length as there are keys in the given symbols object.
const stub = '/* [native code] */' as const;
return isCb ? stub : Object.keys(symsOrCb).map(() => stub) as any; // any cast to suppress type error due to non-overload syntax
}) satisfies typeof bunffi.viewSource;
export const toBuffer = ((ptr, bOff, bLen) => {
const arraybuffer = toArrayBuffer(ptr, bOff, bLen);
return Buffer.from(arraybuffer);
}) satisfies typeof bunffi.toBuffer;
//! Problem: these arraybuffer views are not mapped to the native memory, so they can't be used to modify the memory.
export const toArrayBuffer = ((ptr, byteOff?, byteLen?) => {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.toArrayBuffer, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return view as ArrayBuffer; // ?
const arraybuffer = (() => {
if (util.types.isExternal(view)) {
if (byteLen === undefined) {
let bytes = [], byte, off = 0;
do {
byte = koffi.decode(view, off++, 'unsigned char[]', 1);
bytes.push(byte[0]);
} while (byte[0]);
bytes.pop();
return new Uint8Array(bytes).buffer as ArrayBuffer; // ?
} else {
return koffi.decode(view, byteOff ?? 0, 'unsigned char[]', byteLen).buffer;
}
}
if (byteOff === undefined) return (view as DataView).buffer;
return (view as DataView).buffer.slice(byteOff, byteOff + (byteLen ?? (view as DataView).byteLength));
})();
Object.defineProperty(arraybuffer, '@@bun-polyfills.ffi.ptr', { value: ptr });
return arraybuffer;
}) satisfies typeof bunffi.toArrayBuffer;
export const ptr = ((view, byteOffset = 0) => {
const known = [...ptrsToValues.entries()].find(([_, v]) => v === view);
if (known) return known[0];
if (Reflect.get(view, '@@bun-polyfills.ffi.ptr')) return Reflect.get(view, '@@bun-polyfills.ffi.ptr');
if (ArrayBuffer.isView(view) && Reflect.get(view.buffer, '@@bun-polyfills.ffi.ptr')) return Reflect.get(view.buffer, '@@bun-polyfills.ffi.ptr');
const ptr = fakePtr;
fakePtr += (view.byteLength + 3) & ~0x3;
if (!byteOffset) {
ptrsToValues.set(ptr, view);
return ptr;
} else {
const view2 = new DataView(
(view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) ? view : view.buffer,
byteOffset, view.byteLength
);
ptrsToValues.set(ptr + byteOffset, view2);
return ptr + byteOffset;
}
}) satisfies typeof bunffi.ptr;
export const CFunction = (function CFunction(sym): CallableFunction & { close(): void; } {
if (!sym.ptr) throw new Error('ffi.CFunction requires a non-null pointer');
const fnName = `anonymous__${sym.ptr.toString(16).replaceAll('.', '_')}_${Math.random().toString(16).slice(2)}`;
const fnSig = koffi.proto(fnName, bunffiTypeToKoffiType(sym.returns), sym.args?.map(bunffiTypeToKoffiType) ?? []);
const fnPtr = ptrsToValues.get(sym.ptr);
if (!fnPtr) throw new Error(
`Untracked pointer ${sym.ptr} in ffi.CFunction, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
const fn = koffi.decode(fnPtr, fnSig);
fn.close = () => { };
return fn;
}) satisfies typeof bunffi.CFunction;
export class CString extends String implements bunffi.CString {
constructor(ptr: bunffi.Pointer, bOff?: number, bLen?: number) {
const buf = toBuffer(ptr, bOff, bLen);
const str = buf.toString('ascii');
super(str);
this.ptr = ptr;
this.#buffer = buf.buffer as ArrayBuffer; // ?
}
close() { };
ptr: bunffi.Pointer;
byteOffset?: number;
byteLength?: number;
#buffer: ArrayBuffer;
get arrayBuffer(): ArrayBuffer { return this.#buffer; };
};
// TODO
export class JSCallback implements bunffi.JSCallback {
constructor(cb: (...args: any[]) => any, def: bunffi.FFIFunction) {
throw new Error('ffi.JSCallback is not implemented');
}
readonly ptr: bunffi.Pointer | null = null;
readonly threadsafe: boolean = false;
close() {
Reflect.set(this, 'ptr', null);
};
};
export const read = {
f32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.f32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getFloat32(bOff, LE);
return koffi.decode(view, bOff, 'f32');
},
f64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.f64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getFloat64(bOff, LE);
return koffi.decode(view, bOff, 'f64');
},
i8(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i8, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt8(bOff);
return koffi.decode(view, bOff, 'i8');
},
i16(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i16, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt16(bOff, LE);
return koffi.decode(view, bOff, 'i16');
},
i32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt32(bOff, LE);
return koffi.decode(view, bOff, 'i32');
},
i64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getBigInt64(bOff, LE);
return BigInt(koffi.decode(view, bOff, 'i64'));
},
intptr(ptr, bOff = 0) {
return this.i32(ptr, bOff);
},
ptr(ptr, bOff = 0) {
const u64 = this.u64(ptr, bOff);
const masked = u64 & 0x7FFFFFFFFFFFFn;
return Number(masked);
},
u8(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u8, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint8(bOff);
return koffi.decode(view, bOff, 'u8');
},
u16(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u16, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint16(bOff, LE);
return koffi.decode(view, bOff, 'u16');
},
u32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint32(bOff, LE);
return koffi.decode(view, bOff, 'u32');
},
u64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getBigUint64(bOff, LE);
return BigInt(koffi.decode(view, bOff, 'u64'));
},
} satisfies typeof bunffi.read;
export * as default from './ffi.js';

View File

@@ -1,6 +1,5 @@
import type jsc from 'bun:jsc';
import v8 from 'node:v8';
//import { setRandomSeed, getRandomSeed } from './mathrandom.js';
import { NotImplementedError, getCallSites } from '../utils/errors.js';
import { gc } from './bun.js';
@@ -8,10 +7,13 @@ const STUB = () => void 0;
function jscSerialize(value: any, options?: { binaryType: 'nodebuffer'; }): Buffer;
function jscSerialize(value: any, options?: { binaryType?: 'arraybuffer'; }): SharedArrayBuffer;
function jscSerialize(value: any, options?: { binaryType?: string }): Buffer | SharedArrayBuffer {
const serialized = v8.serialize(value);
function jscSerialize(value: any, options?: { binaryType?: string; }): Buffer | SharedArrayBuffer {
const serialized = v8.serialize(value instanceof SharedArrayBuffer ? new Uint8Array(value) : value);
if (options?.binaryType === 'nodebuffer') return serialized;
else return new SharedArrayBuffer(serialized.byteLength);
const sab = new SharedArrayBuffer(serialized.byteLength);
const sabView = new Uint8Array(sab);
sabView.set(serialized);
return sab;
}
// TODO: Investigate ways of making these the actual JSC serialization format (probably Bun WASM)
// TODO: whilst this works for common use-cases like Node <-> Node it still does not make it
@@ -59,9 +61,6 @@ export const startRemoteDebugger = STUB satisfies typeof jsc.startRemoteDebugger
//! this is a really poor polyfill but it's better than nothing
export const getProtectedObjects = (() => { return [globalThis]; }) satisfies typeof jsc.getProtectedObjects;
export const getRandomSeed = 0; // TODO
export const setRandomSeed = 0; // TODO
export const heapSize = (() => { return v8.getHeapStatistics().used_heap_size; }) satisfies typeof jsc.heapSize;
export const heapStats = (() => {
const stats = v8.getHeapStatistics();
@@ -83,7 +82,7 @@ export const isRope = (() => false) satisfies typeof jsc.isRope;
export const memoryUsage = (() => {
const stats = v8.getHeapStatistics();
const resUse = process.resourceUsage();
const resUse = (process as unknown as NodeJS.Process).resourceUsage();
return {
current: stats.malloced_memory,
peak: stats.peak_malloced_memory,
@@ -106,6 +105,8 @@ export const reoptimizationRetryCount = ((...args) => args.length ? 0 : void 0 a
export const profile = (() => {
throw new NotImplementedError('jsc.profile is not polyfillable', STUB, true);
}) satisfies typeof jsc.profile;
export const optimizeNextInvocation = (() => {
throw new NotImplementedError('jsc.optimizeNextInvocation is not polyfillable', STUB, true);
}) satisfies typeof jsc.optimizeNextInvocation;
export const optimizeNextInvocation = STUB satisfies typeof jsc.optimizeNextInvocation; // no-op
export { setRandomSeed, getRandomSeed } from '../global/mathrandom.js';
export * as default from './jsc.js';

View File

@@ -0,0 +1,386 @@
import type * as bun_sqlite from 'bun:sqlite';
import bsqlite3 from 'better-sqlite3';
/*
Not sure where to leave this note so I'll leave it here for now:
There will be an incompatibility in queries that can be executed due to the way better-sqlite3 handles double quotes:
https://github.com/WiseLibs/better-sqlite3/issues/1092#issuecomment-1782321118
The possible solutions are:
- Fork better-sqlite3 and recompile it without SQLITE_DQS=0
- Make Bun's SQLite module use SQLITE_DQS=0 (personally I think this is the better solution going forward)
*/
export const constants = {
SQLITE_OPEN_READONLY: 0x00000001 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_READWRITE: 0x00000002 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_CREATE: 0x00000004 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_DELETEONCLOSE: 0x00000008 /* VFS only */,
SQLITE_OPEN_EXCLUSIVE: 0x00000010 /* VFS only */,
SQLITE_OPEN_AUTOPROXY: 0x00000020 /* VFS only */,
SQLITE_OPEN_URI: 0x00000040 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_MEMORY: 0x00000080 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_MAIN_DB: 0x00000100 /* VFS only */,
SQLITE_OPEN_TEMP_DB: 0x00000200 /* VFS only */,
SQLITE_OPEN_TRANSIENT_DB: 0x00000400 /* VFS only */,
SQLITE_OPEN_MAIN_JOURNAL: 0x00000800 /* VFS only */,
SQLITE_OPEN_TEMP_JOURNAL: 0x00001000 /* VFS only */,
SQLITE_OPEN_SUBJOURNAL: 0x00002000 /* VFS only */,
SQLITE_OPEN_SUPER_JOURNAL: 0x00004000 /* VFS only */,
SQLITE_OPEN_NOMUTEX: 0x00008000 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_FULLMUTEX: 0x00010000 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_SHAREDCACHE: 0x00020000 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_PRIVATECACHE: 0x00040000 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_WAL: 0x00080000 /* VFS only */,
SQLITE_OPEN_NOFOLLOW: 0x01000000 /* Ok for sqlite3_open_v2() */,
SQLITE_OPEN_EXRESCODE: 0x02000000 /* Extended result codes */,
SQLITE_PREPARE_PERSISTENT: 0x01,
SQLITE_PREPARE_NORMALIZE: 0x02,
SQLITE_PREPARE_NO_VTAB: 0x04,
};
let controllers;
export class Statement implements bun_sqlite.Statement {
constructor(raw: bsqlite3.Statement, boundParams?: bun_sqlite.SQLQueryBindings | bun_sqlite.SQLQueryBindings[]) {
this.#raw = raw;
if (boundParams) {
this.#boundParams = Array.isArray(boundParams) ? boundParams : [boundParams];
this.#raw.bind(...this.#boundParams);
}
}
isFinalized = false;
#boundParams: bun_sqlite.SQLQueryBindings[] = [];
#raw;
get native() {
return this.#raw;
}
toJSON() {
return {
sql: this.toString(),
isFinalized: this.isFinalized,
paramsCount: this.paramsCount,
columnNames: this.columnNames,
};
}
get [Symbol.toStringTag]() {
return `'${this.toString()}'`;
}
toString() {
return this.#raw.source; // TODO: Does better-sqlite3 really not have a way to get the expanded string? This doesn't update when params are bound...
}
get<R>(...args: unknown[]): R | null {
this.#ASSERT_NOT_FINALIZED();
const wrap = (() => {
if (args.length === 0) return this.#raw.get() as R;
const arg0 = args[0];
return (!Array.isArray(arg0) && (!arg0 || typeof arg0 !== 'object' || ArrayBuffer.isView(arg0))
? this.#raw.get(args) : this.#raw.get(...args)) as R;
})();
if (wrap === undefined) return null;
if (Buffer.isBuffer(wrap)) return new Uint8Array(wrap.buffer, wrap.byteOffset, wrap.byteLength) as unknown as R;
if (typeof wrap === 'object' && wrap && 'blobby' in wrap && Buffer.isBuffer(wrap.blobby))
wrap.blobby = new Uint8Array(wrap.blobby.buffer, wrap.blobby.byteOffset, wrap.blobby.byteLength) as unknown as R;
return wrap;
}
all<R extends any[]>(...args: unknown[]): R {
this.#ASSERT_NOT_FINALIZED();
const wrapList = (() => {
if (args.length === 0) return this.#raw.all() as R;
const arg0 = args[0];
if (!Array.isArray(arg0) && (!arg0 || typeof arg0 !== 'object' || ArrayBuffer.isView(arg0))) return this.#raw.all(args) as R;
for (const arg of args) {
if (typeof arg === 'object' && arg && !Array.isArray(arg)) {
const keys = Object.keys(arg);
for (const key of keys) {
if (key[0] === '$' || key[0] === '@') {
const value = Reflect.get(arg, key);
Reflect.deleteProperty(arg, key);
Reflect.set(arg, key.slice(1), value);
}
}
}
}
try {
return this.#raw.all(...args) as R;
} catch (e) {
const err = e as Error;
// better-sqlite3 insists that queries that return no data use run(), but Bun doesn't care.
if (err?.message?.includes?.('This statement does not return data.')) return [] as unknown as R;
else throw err;
}
})();
let i = -1;
for (const wrap of wrapList) {
i++;
if (Buffer.isBuffer(wrap)) wrapList[i] = new Uint8Array(wrap.buffer, wrap.byteOffset, wrap.byteLength) as unknown as R;
if (typeof wrap === 'object' && wrap && 'blobby' in wrap && Buffer.isBuffer(wrap.blobby))
wrap.blobby = new Uint8Array(wrap.blobby.buffer, wrap.blobby.byteOffset, wrap.blobby.byteLength) as unknown as R;
}
return wrapList;
}
values(...args: unknown[]): ReturnType<bun_sqlite.Statement['values']> {
return this.all(...args).map((value) => Object.values(value));
}
run(...args: unknown[]): void {
this.#ASSERT_NOT_FINALIZED();
if (args.length === 0) return void this.#raw.run();
const arg0 = args[0];
if (args.length === 1 && typeof arg0 === 'string' && !arg0.trim()) throw new Error('Query contained no valid SQL statement; likely empty query.');
if (!Array.isArray(arg0) && (!arg0 || typeof arg0 !== 'object' || ArrayBuffer.isView(arg0))) return void this.#raw.run(args);
for (const arg of args) {
if (typeof arg === 'object' && arg && !Array.isArray(arg)) {
const keys = Object.keys(arg);
for (const key of keys) {
if (key[0] === '$' || key[0] === '@') {
const value = Reflect.get(arg, key);
Reflect.deleteProperty(arg, key);
Reflect.set(arg, key.slice(1), value);
}
}
}
}
this.#raw.run(...args);
}
get columnNames() {
this.#ASSERT_NOT_FINALIZED();
return this.#raw.columns().map((column) => column.name);
}
get paramsCount() {
this.#ASSERT_NOT_FINALIZED();
return this.#boundParams.length;
}
#ASSERT_NOT_FINALIZED() {
if (this.isFinalized) throw new Error('Statement is finalized');
}
finalize() {
this.isFinalized = true;
}
}
Statement satisfies typeof bun_sqlite.Statement;
let cachedCount = Symbol.for('Bun.Database.cache.count');
export class Database implements bun_sqlite.Database {
constructor(filenameGiven: string | Buffer = ':memory:', options?: ConstructorParameters<typeof bun_sqlite.Database>[1]) {
if (typeof options === 'number') {
const flags: number = options;
options = {};
options.readonly = !!(flags & constants.SQLITE_OPEN_READONLY);
options.create = !!(flags & constants.SQLITE_OPEN_CREATE);
options.readwrite = !!(flags & constants.SQLITE_OPEN_READWRITE);
}
options ??= { readonly: false, create: true, readwrite: true };
if (typeof filenameGiven !== 'string') {
if (ArrayBuffer.isView(filenameGiven)) {
this.#handle = Database.#deserialize(filenameGiven, options.readonly);
this.filename = ':memory:';
return;
}
throw new TypeError(`Expected 'filename' to be a string, got '${typeof filenameGiven}'`);
}
const filename = filenameGiven.trim() || ':memory:';
const anonymous = filename === '' || filename === ':memory:';
if (anonymous && options.readonly) throw new Error('Cannot open an anonymous database in read-only mode.');
this.#handle = bsqlite3(anonymous ? ':memory:' : filename, options);
this.filename = filename;
}
#handle;
#handleID: number = crypto.getRandomValues(new Uint32Array(1))[0];
#cachedQueriesKeys: string[] = [];
#cachedQueriesLengths: number[] = [];
#cachedQueriesValues: Statement[] = [];
filename;
get handle() {
return this.#handleID;
}
get inTransaction() {
return this.#handle.inTransaction;
}
static open(filename?: string, options?: number | { readonly?: boolean, create?: boolean, readwrite?: boolean; }) {
return new Database(filename, options);
}
loadExtension(ext?: string, entryPoint?: string) {
return this.#handle.loadExtension(ext!);
}
serialize(optionalName: string) {
return this.#handle.serialize({ attached: optionalName || 'main' });
}
static #deserialize(serialized: Buffer, readonly = false) {
return new bsqlite3(serialized, { readonly });
}
static deserialize(serialized: Buffer, isReadOnly = false) {
return new Database(serialized, isReadOnly ? constants.SQLITE_OPEN_READONLY : 0);
}
static setCustomSQLite(path: string) {
if (process.platform === 'darwin') throw new Error('Not implemented');
else return false;
}
close() {
this.clearQueryCache();
return this.#handle.close();
}
clearQueryCache() {
for (let item of this.#cachedQueriesValues) {
item.finalize();
}
this.#cachedQueriesKeys.length = 0;
this.#cachedQueriesValues.length = 0;
this.#cachedQueriesLengths.length = 0;
}
run<ParamsType extends bun_sqlite.SQLQueryBindings[]>(sqlQuery: string, ...bindings: ParamsType[]): void {
if (!sqlQuery.trim()) throw new Error('Query contained no valid SQL statement; likely empty query.');
if (bindings.length === 0) return void this.#handle.exec(sqlQuery);
const prepared = this.prepare(sqlQuery, bindings as unknown as bun_sqlite.SQLQueryBindings[]);
prepared.run();
}
exec<ParamsType extends bun_sqlite.SQLQueryBindings[]>(sqlQuery: string, ...bindings: ParamsType[]): void {
this.run(sqlQuery, ...bindings);
}
prepare(query: string, params: Parameters<bun_sqlite.Database['prepare']>[1]) {
return new Statement(this.#handle.prepare(query), params);
}
static MAX_QUERY_CACHE_SIZE = 20;
get [cachedCount]() {
return this.#cachedQueriesKeys.length;
}
query(query: string) {
if (typeof query !== 'string') {
throw new TypeError(`Expected 'query' to be a string, got '${typeof query}'`);
}
if (query.length === 0) {
throw new Error('SQL query cannot be empty.');
}
const willCache = this.#cachedQueriesKeys.length < Database.MAX_QUERY_CACHE_SIZE;
let index = this.#cachedQueriesLengths.indexOf(query.length);
while (index !== -1) {
if (this.#cachedQueriesKeys[index] !== query) {
index = this.#cachedQueriesLengths.indexOf(query.length, index + 1);
continue;
}
let stmt = this.#cachedQueriesValues[index];
if (stmt.isFinalized) {
return (this.#cachedQueriesValues[index] = this.prepare(
query,
undefined,
));
}
return stmt;
}
let stmt = this.prepare(query, undefined);
if (willCache) {
this.#cachedQueriesKeys.push(query);
this.#cachedQueriesLengths.push(query.length);
this.#cachedQueriesValues.push(stmt);
}
return stmt;
}
transaction(fn: Parameters<bun_sqlite.Database['transaction']>[0]): ReturnType<bun_sqlite.Database['transaction']> {
if (typeof fn !== 'function') throw new TypeError('Expected first argument to be a function');
const db = this;
const controller = getController(db);
const properties = {
default: { value: wrapTransaction(fn, db, controller.default) },
deferred: { value: wrapTransaction(fn, db, controller.deferred) },
immediate: { value: wrapTransaction(fn, db, controller.immediate) },
exclusive: { value: wrapTransaction(fn, db, controller.exclusive) },
database: { value: this, enumerable: true },
};
Object.defineProperties(properties.default.value, properties);
Object.defineProperties(properties.deferred.value, properties);
Object.defineProperties(properties.immediate.value, properties);
Object.defineProperties(properties.exclusive.value, properties);
// @ts-expect-error cast
return properties.default.value;
}
}
const getController = (db: Database) => {
let controller = (controllers ||= new WeakMap()).get(db);
if (!controller) {
const shared = {
commit: db.prepare('COMMIT', undefined),
rollback: db.prepare('ROLLBACK', undefined),
savepoint: db.prepare('SAVEPOINT `\t_bs3.\t`', undefined),
release: db.prepare('RELEASE `\t_bs3.\t`', undefined),
rollbackTo: db.prepare('ROLLBACK TO `\t_bs3.\t`', undefined),
};
controllers.set(
db,
(controller = {
default: Object.assign({ begin: db.prepare('BEGIN', undefined) }, shared),
deferred: Object.assign({ begin: db.prepare('BEGIN DEFERRED', undefined) }, shared),
immediate: Object.assign({ begin: db.prepare('BEGIN IMMEDIATE', undefined) }, shared),
exclusive: Object.assign({ begin: db.prepare('BEGIN EXCLUSIVE', undefined) }, shared),
}),
);
}
return controller;
};
const wrapTransaction = (fn: Function, db: Database, { begin, commit, rollback, savepoint, release, rollbackTo }: any) =>
function transaction(this: any, ...args: any[]) {
let before, after, undo;
if (db.inTransaction) {
before = savepoint;
after = release;
undo = rollbackTo;
} else {
before = begin;
after = commit;
undo = rollback;
}
try {
before.run();
const result = fn.apply(this, args);
after.run();
return result;
} catch (ex) {
if (db.inTransaction) {
undo.run();
if (undo !== rollback) after.run();
}
throw ex;
}
};
export default {
Database,
Statement,
constants,
default: Database,
get native() {
throw new Error('bun-polyfills does not polyfill exposed native sqlite bindings.');
},
} satisfies typeof bun_sqlite;

View File

@@ -0,0 +1,139 @@
import _dateFormat from 'dateformat';
const dateFormat = _dateFormat as unknown as typeof import('dateformat').default; // 10/10 types :D
import { expect as jestexpect } from 'expect';
import _jesteach from 'jest-each';
import extendedMatchers from 'jest-extended';
const jesteach = _jesteach.default as unknown as typeof import('jest-each').default; // bad types, again...
import * as jestmock from 'jest-mock';
import nodetest from 'node:test';
import chp from 'node:child_process';
import { promisify } from 'node:util';
import type { Mock } from 'bun:test';
const exec = promisify(chp.exec);
const bunmock: typeof import('bun:test')['mock'] = function mock(fn) {
return jestmock.fn(fn);
};
bunmock.module = (id, factory) => { }; // TODO: Need to integrate this with the ESM loader somehow.
bunmock.restore = () => { }; // TODO
const bundescribe: typeof import('bun:test')['describe'] = (name, fn) => nodetest.describe(name, fn);
bundescribe.only = (name, fn) => nodetest.describe.only(name, fn);
bundescribe.todo = (name, fn) => nodetest.describe.todo(name, fn);
bundescribe.skip = (name, fn) => nodetest.describe.skip(name, fn);
bundescribe.skipIf = (condition) => condition ? nodetest.describe.skip : nodetest.describe;
bundescribe.if = (condition) => condition ? nodetest.describe : () => void 0;
bundescribe.each = (table: any) => {
return (title: string, suite: AnyFunction) => jesteach(table).describe(title, suite);
};
jestexpect.extend(extendedMatchers);
const bunExpect = jestexpect as unknown as typeof import('bun:test')['expect'];
bunExpect.unreachable = (msg) => {
if (msg instanceof Error) throw msg;
else throw new Error(msg ?? 'Unreachable code reached');
};
const bunit: typeof import('bun:test')['it'] = (name, fn) => {
nodetest.it(name, fn.length ? (t, done) => void fn(done) : async (t) => void await (fn as () => Promise<void>)());
};
bunit.only = (name, fn) => nodetest.only(name, fn.length ? (t, done) => void fn(done) : async (t) => void await (fn as () => Promise<void>)());
bunit.todo = (name, fn) => nodetest.todo(name, fn?.length ? (t, done) => void fn?.(done) : async (t) => void await (fn as () => Promise<void>)());
bunit.skip = (name, fn) => nodetest.skip(name, fn.length ? (t, done) => void fn(done) : async (t) => void await (fn as () => Promise<void>)());
bunit.if = (condition) => condition ? bunit : () => void 0;
bunit.skipIf = (condition) => condition ? bunit.skip : bunit;
bunit.each = (table: any) => {
return (title: string, test: AnyFunction) => jesteach(table).it(title, test);
};
const testModule = {
// This entire function is overall very hacky and little tested for now.
// Maybe it would be better to just monkeypatch the relevant JS apis instead?
setSystemTime(now?: Date | number) {
if (process.platform === 'linux') {
const sudo = ''; // process.getuid?.() === 0 ? '' : 'sudo ';
if (typeof now === 'undefined') {
exec(`${sudo}timedatectl set-ntp true`);
return this;
}
//? Doesn't work on non-systemd distros, nor WSL by default...
exec(
`${sudo}timedatectl set-ntp false && ` +
`${sudo}date -s "${dateFormat(now, "UTC:mm/dd/yyyy HH:MM:ss")}" --utc && ` +
`${sudo}hwclock -w --utc`
);
} else if (process.platform === 'win32') {
const Win32DateFormat = (() => {
try {
const stdout = chp.execSync('date');
return stdout.toString('utf8').match(/Enter the new date: \((.+)\)/)?.[1] ?? 'dd-mm-yy';
} catch (e) {
const err = e as { stdout: Buffer; };
return err.stdout.toString('utf8').match(/Enter the new date: \((.+)\)/)?.[1] ?? 'dd-mm-yy';
}
})();
if (typeof now === 'undefined') {
// TODO: How to reset system time on Windows? Below might work but is messy and needs admin...
/* net stop w32time
w32tm /unregister
w32tm /register
net start w32time
w32tm /resync */
return this;
}
exec(
`date ${dateFormat(now, Win32DateFormat)} && ` +
`time ${dateFormat(now, "HH:MM:ss")}`
);
} else throw new Error(`Unsupported platform for setSystemTime: ${process.platform}`); // TODO: How to set system time on MacOS? Can't test for now :(
return this;
},
spyOn<T extends object, K extends keyof T>(obj: T, methodOrPropertyValue: K): Mock<T[K] extends AnyFunction ? T[K] : never> {
const mock = jestmock.spyOn(
obj,
// @ts-expect-error jest has a really convoluted type for this that isnt worth trying to replicate
methodOrPropertyValue,
) as jestmock.Spied<any>;
// @ts-expect-error same reason as above
return mock;
},
beforeAll(fn) {
nodetest.before(fn.length ? (s, done) => void fn(done) : (s) => void (fn as () => void)());
},
beforeEach(fn) {
nodetest.beforeEach(fn.length ? (s, done) => void fn(done) : (s) => void (fn as () => void)());
},
afterAll(fn) {
nodetest.after(fn.length ? (s, done) => void fn(done) : (s) => void (fn as () => void)());
},
afterEach(fn) {
nodetest.afterEach(fn.length ? (s, done) => void fn(done) : (s) => void (fn as () => void)());
},
mock: bunmock,
jest: {
restoreAllMocks() {
bunmock.restore();
},
fn(func) {
return jestmock.fn(func);
},
},
describe: bundescribe,
test: bunit,
it: bunit,
expect: bunExpect, // TODO: this is not fully compatible, needs finer grained implementation
} satisfies typeof import('bun:test');
export const setSystemTime = testModule.setSystemTime;
export const spyOn = testModule.spyOn;
export const beforeAll = testModule.beforeAll;
export const beforeEach = testModule.beforeEach;
export const afterAll = testModule.afterAll;
export const afterEach = testModule.afterEach;
export const mock = testModule.mock;
export const jest = testModule.jest;
export const describe = testModule.describe;
export const test = testModule.test;
export const it = testModule.it;
export const expect = testModule.expect;
export default testModule;

View File

@@ -1,5 +1,7 @@
import bun from './index.js';
import * as jsc from './modules/jsc.js';
import * as ffi from './modules/ffi.js';
import * as sqlite from './modules/sqlite.js';
// This file serves two purposes:
// 1. It is the entry point for using the Bun global in the REPL. (--import this file)
@@ -8,23 +10,11 @@ import * as jsc from './modules/jsc.js';
globalThis.Bun = bun as typeof bun & {
// TODO: Missing polyfills
readableStreamToFormData: typeof import('bun').readableStreamToFormData;
deepEquals: typeof import('bun').deepEquals;
deepMatch: typeof import('bun').deepMatch;
Glob: typeof import('bun').Glob;
build: typeof import('bun').build;
mmap: typeof import('bun').mmap;
connect: typeof import('bun').connect;
listen: typeof import('bun').listen;
password: typeof import('bun').password;
CryptoHashInterface: typeof import('bun').CryptoHashInterface;
CryptoHasher: typeof import('bun').CryptoHasher;
FileSystemRouter: typeof import('bun').FileSystemRouter;
//? Polyfilled but with broken types (See each one in ./src/modules/bun.ts for details)
generateHeapSnapshot: typeof import('bun').generateHeapSnapshot;
stdout: typeof import('bun').stdout;
stderr: typeof import('bun').stderr;
stdin: typeof import('bun').stdin;
};
Reflect.set(globalThis, 'jsc', jsc);
Reflect.set(globalThis, 'ffi', ffi);
Reflect.set(globalThis, 'sqlite', sqlite);

View File

@@ -23,7 +23,7 @@ export function streamToBuffer(stream: streams.Readable | streams.Duplex): Promi
}
export function isArrayBufferView(value: any): value is ArrayBufferView {
return value !== null && typeof value === 'object' &&
return value !== null && typeof value === 'object' &&
value.buffer instanceof ArrayBuffer && typeof value.byteLength === 'number' && typeof value.byteOffset === 'number';
}
@@ -32,5 +32,5 @@ export function isOptions(options: any): options is SpawnOptions.OptionsObject {
}
export function isFileBlob(blob: any): blob is FileBlob {
return blob instanceof Blob && Reflect.get(blob, 'readable') instanceof ReadableStream && typeof Reflect.get(blob, 'writer') === 'function';
return blob instanceof Blob && Reflect.get(blob, '@@isFileBlob');
}

View File

@@ -0,0 +1,104 @@
/*! Modified version of: to-sync. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
// @ts-check
import { Worker } from 'node:worker_threads';
/**
* Why are we here? Just to suffer?
*
* This abomination of a class allows you to call an async function... synchronously.
*
* This is used for polyfills that are sync in Bun but need async functions in Node to work.
* So far all polyfills that needed this were fairly performance-insensitive, so it was fine, but
* if you need to use this for something that needs to be fast, you should probably reconsider.
*
* ## Usage Rules
* - The called function MUST follow the constraints of code running in a worker thread.
* - The called function MUST be async. If a non-async function is called and throws an error, there will be a hang.
* - The called function MUST return a `Uint8Array` or a superclass.
* - The called function MUST not import external modules by name (See below).
* - Remember to `terminate()` the worker when you're done with it.
*
* ## External Modules
* External modules are ones in `node_modules`, Node builtins and file imports are both fine, but for external modules
* you need to pass a map of module names to their fully resolved absolute file URLs to the SyncWorker constructor, as
* workers can't resolve modules by name themselves. Use `require.resolve` or `import.meta.resolve` to get the absolute file URL of a module.
*/
export class SyncWorker extends Worker {
/**
* @param {Record<string, string>=} modules Map of external module names to their fully resolved absolute file URLs,
* use in the worker code as `workerData.resolve.{moduleName}`
* @param {Record<string, unknown>=} workerData Extra data to pass to the worker thread
* @param {AbortSignal=} signal Terminate the worker thread if a signal is aborted
*/
constructor(modules = {}, workerData = {}, signal) {
// Create the worker thread
const mod = new URL('sync_worker.mjs', import.meta.url);
super(mod, { workerData: { ...workerData, resolve: modules } });
super.on('error', console.error);
super.on('messageerror', console.error);
// Create a shared buffer to communicate with the worker thread
this.#ab = new SharedArrayBuffer(8192);
this.#data = new Uint8Array(this.#ab, 8);
this.#int32 = new Int32Array(this.#ab);
signal?.addEventListener('abort', () => super.terminate());
}
#ab;
#data;
#int32;
/**
* Read the notes on the {@link SyncWorker} class before using this.
* @template {(...args: any[]) => any} I
* @template {((result: Uint8Array) => any) | null} F
* @param {I} fn
* @param {F} formatter
* @returns {(...args: Parameters<I>) => F extends null ? (ReturnType<I> extends Promise<infer V> ? V : ReturnType<I>) : ReturnType<F>}
*/
sync(fn, formatter) {
const source = 'export default ' + fn.toString();
const mc = new MessageChannel();
const localPort = mc.port1;
const remotePort = mc.port2;
super.postMessage({ port: remotePort, code: source, ab: this.#ab }, [remotePort]);
return (/** @type {unknown[]} */ ...args) => {
Atomics.store(this.#int32, 0, 0);
localPort.postMessage(args); // Send the arguments to the worker thread
Atomics.wait(this.#int32, 0, 0); // Wait for the worker thread to send the result back
// Two first values in the shared buffer are the number of bytes left to read and
// the second value is a boolean indicating if the result was successful or not.
let bytesLeft = this.#int32[0];
const ok = this.#int32[1];
if (bytesLeft === -1) return new Uint8Array(0);
// Allocate a new Uint8Array to store the result
const result = new Uint8Array(bytesLeft);
let offset = 0;
// Read the result from the shared buffer
while (bytesLeft > 0) {
// Read all the data that is available in the SharedBuffer
const part = this.#data.subarray(0, Math.min(bytesLeft, this.#data.byteLength));
result.set(part, offset); // Copy the data to the result
offset += part.byteLength; // Update the offset
if (offset === result.byteLength) break; // If we have read all the data, break the loop
Atomics.notify(this.#int32, 0); // Notify the worker thread that we are ready to receive more data
Atomics.wait(this.#int32, 0, bytesLeft); // Wait for the worker thread to send more data
bytesLeft -= part.byteLength; // Update the number of bytes left to read
}
if (ok) return formatter ? formatter(result) : result;
const str = new TextDecoder().decode(result);
const err = JSON.parse(str);
const error = new Error(err.message);
error.stack = err.stack
?.replace(/ \(data:text\/javascript,.+:(\d+):(\d+)\)$/gm, ' (sync worker thread:$1:$2)')
?.replace(/at data:text\/javascript,.+:(\d+):(\d+)$/gm, 'at (sync worker thread:$1:$2)');
throw error;
};
};
}

View File

@@ -0,0 +1,50 @@
/*! Modified version of: to-sync. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
// @ts-check
import wt from 'node:worker_threads';
const textEncoder = new TextEncoder();
wt.parentPort?.on('message', async evt => {
/** @type {{ port: MessagePort, code: string, ab: SharedArrayBuffer }} */
const { port, code, ab } = evt;
const data = new Uint8Array(ab, 8);
const int32 = new Int32Array(ab, 0, 2);
const url = "data:text/javascript," + encodeURIComponent(code);
const { default: fn } = await import(url);
port.on('message', async (/** @type {unknown[]} */ evt) => {
const args = evt;
const [u8, ok] = await Promise.resolve(fn(...args))
.then((/** @type {unknown} */ r) => {
if (!(r instanceof Uint8Array)) throw new Error('result must be a Uint8Array, got: ' + typeof r);
return /** @type {const} */([r, 1]);
})
.catch((/** @type {Error} */ e) => {
const err = JSON.stringify({
message: e?.message || e,
stack: e?.stack
});
const r = textEncoder.encode(err);
return /** @type {const} */([r, 0]);
});
int32[1] = ok;
let bytesLeft = u8.byteLength;
let offset = 0;
if (bytesLeft === 0) {
int32[0] = -1;
Atomics.notify(int32, 0);
}
while (bytesLeft > 0) {
int32[0] = bytesLeft;
const chunkSize = Math.min(bytesLeft, data.byteLength);
data.set(u8.subarray(offset, offset + chunkSize), 0);
Atomics.notify(int32, 0);
if (bytesLeft === chunkSize) break;
Atomics.wait(int32, 0, bytesLeft);
bytesLeft -= chunkSize;
offset += chunkSize;
}
});
});

View File

@@ -0,0 +1,120 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import { splitCookiesString } from 'set-cookie-parser';
// Convert node:http Request/Response objects to/from their web equivalents
// Credits to the SvelteKit team (Modified)
// https://github.com/sveltejs/kit/blob/8d1ba04825a540324bc003e85f36559a594aadc2/packages/kit/src/exports/node/index.js
export const requestNodeResSymbol = Symbol('bun-polyfills.serve.nodeReq');
export const requestRemoteIPSymbol = Symbol('bun-polyfills.serve.remoteIP');
export const requestUpgradedSymbol = Symbol('bun-polyfills.serve.upgraded');
export const toWebRequest = (nodeReq: IncomingMessage, nodeRes?: ServerResponse, bodySizeLimit?: number, upgraded = false): Request => {
const webReq = new Request('http://' + nodeReq.headers.host! + nodeReq.url, {
duplex: 'half',
method: nodeReq.method,
headers: nodeReq.headers as Record<string, string>,
body: getRawBody(nodeReq, bodySizeLimit),
});
Reflect.set(webReq, requestRemoteIPSymbol, {
address: nodeReq.socket.remoteAddress, port: nodeReq.socket.remotePort, family: nodeReq.socket.remoteFamily,
});
Reflect.set(webReq, requestNodeResSymbol, nodeRes);
Reflect.set(webReq, requestUpgradedSymbol, upgraded);
return webReq;
};
export const sendWebResponse = (nodeRes: ServerResponse, webRes: Response): void => {
const headers = Object.fromEntries(webRes.headers);
const cookies: string[] = webRes.headers.has('set-cookie') ? splitCookiesString(webRes.headers.get('set-cookie')!) : [];
try {
nodeRes.writeHead(webRes.status, { ...headers, 'set-cookie': cookies });
} catch {
// Headers already modified, assume this is an upgraded request
}
if (!webRes.body) return void nodeRes.end();
if (webRes.body.locked) {
nodeRes.write(
'Fatal error: Response body is locked. ' +
`This can happen when the response was already read (for example through 'response.json()' or 'response.text()').`
);
return void nodeRes.end();
}
const reader = webRes.body.getReader();
if (nodeRes.destroyed) return void reader.cancel();
const cancel = (error?: Error) => {
nodeRes.off('close', cancel);
nodeRes.off('error', cancel);
// If the reader has already been interrupted with an error earlier,
// then it will appear here, it is useless, but it needs to be caught.
reader.cancel(error).catch(() => { });
if (error) nodeRes.destroy(error);
};
nodeRes.on('close', cancel);
nodeRes.on('error', cancel);
next();
async function next() {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!nodeRes.write(value)) return void nodeRes.once('drain', next);
}
nodeRes.end();
} catch (error) {
cancel(error instanceof Error ? error : new Error(String(error)));
}
}
};
class HTTPError extends Error {
constructor(status: number, reason: string) {
super(reason);
this.status = status;
}
status: number;
get reason() { return super.message; }
}
function getRawBody(req: IncomingMessage, bodySizeLimit?: number): ReadableStream | null {
const h = req.headers;
if (!h['content-type']) return null;
const contentLength = Number(h['content-length']);
// check if no request body
if ((req.httpVersionMajor === 1 && isNaN(contentLength) && h['transfer-encoding'] == null) || contentLength === 0) return null;
let length = contentLength;
if (bodySizeLimit) {
if (!length) length = bodySizeLimit;
else if (length > bodySizeLimit) throw new HTTPError(413, `Received content-length of ${length}, but only accept up to ${bodySizeLimit} bytes.`);
}
if (req.destroyed) {
const readable = new ReadableStream();
return readable.cancel(), readable;
}
let size = 0;
let cancelled = false;
return new ReadableStream({
start(controller) {
req.on('error', (error) => (cancelled = true, controller.error(error)));
req.on('end', () => cancelled || controller.close());
req.on('data', (chunk) => {
if (cancelled) return;
size += chunk.length;
if (size > length) return cancelled = true, controller.error(
new HTTPError(413, `request body size exceeded ${contentLength ? "'content-length'" : 'BODY_SIZE_LIMIT'} of ${length}`)
);
controller.enqueue(chunk);
if (controller.desiredSize === null || controller.desiredSize <= 0) req.pause();
});
},
pull() { req.resume(); },
cancel(reason) { cancelled = true, req.destroy(reason); },
});
}

View File

@@ -0,0 +1,181 @@
// @ts-check
/// <reference types="typings-esm-loader" />
/// <reference types="bun-types" />
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'node:path';
import fs from 'node:fs';
import $ from 'chalk';
import bunwasm from 'bun-wasm';
import { TransformResponseStatus } from 'bun-wasm/schema';
const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'test');
const tsconfigPath = path.resolve(testRoot, 'tsconfig.json');
/** @type {Record<string, string[]>} */
let tsconfigPaths = {};
if (fs.existsSync(tsconfigPath)) {
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8'));
tsconfigPaths = tsconfig.compilerOptions.paths;
} else {
throw new Error('No tsconfig.json found at: ' + tsconfigPath);
}
await bunwasm.init();
const NO_STACK = () => void 0;
const decoder = new TextDecoder('utf-8');
const libRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'src');
const knownBunModules = ['sqlite', 'ffi', 'jsc', 'test', 'wrap'];
/** @type {string} */
let mainURL;
/** @type {resolve} */
export async function resolve(specifier, context, nextResolve) {
if (context.parentURL === undefined) mainURL = specifier;
if (specifier === 'bun') return { url: pathToFileURL(path.resolve(libRoot, 'modules', 'bun.js')).href, format: 'module', shortCircuit: true };
if (specifier.startsWith('bun:')) {
const module = specifier.slice(4);
if (!knownBunModules.includes(module)) {
const err = new Error(`[bun-polyfills] Unknown or unimplemented bun module "${specifier}" imported from "${context.parentURL}"`);
Error.captureStackTrace(err, NO_STACK);
throw err;
}
if (module === 'wrap') return { url: 'bun:wrap@' + context.parentURL, format: 'module', shortCircuit: true };
return { url: pathToFileURL(path.resolve(libRoot, 'modules', module + '.js')).href, format: 'module', shortCircuit: true };
}
// Not the really an accurate way to do this, but it works for the test suite usages
if (Object.keys(tsconfigPaths).includes(specifier)) {
const paths = tsconfigPaths[specifier];
const resolved = paths.map(p => pathToFileURL(path.resolve(testRoot, p)).href);
specifier = resolved[0];
}
//console.debug('trying to resolve', specifier, 'from', context.parentURL);
/** @type {Resolve.Return | Error} */
let next;
/** @type {string} */
let format;
try {
next = await nextResolve(specifier, context);
if (next.shortCircuit || next.format === 'builtin' || next.format === 'wasm') return next;
specifier = next.url;
format = next.format ?? 'module';
} catch (err) {
next = err;
format = 'module';
}
//console.debug('resolved', specifier, 'from', context.parentURL, 'to', Reflect.get(next, 'url') ?? next);
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file://')) {
if (!specifier.startsWith('file://')) {
const parent = context.parentURL ? fileURLToPath(context.parentURL) : process.cwd();
specifier = pathToFileURL(path.resolve(path.dirname(parent), specifier)).href;
}
const specifierPath = fileURLToPath(specifier);
const exists = fs.existsSync(specifierPath);
if (specifier.endsWith('.ts') && exists) return { url: specifier, format: 'ts' + format, shortCircuit: true };
if (specifier.endsWith('.js') && exists) return { url: specifier, format, shortCircuit: true };
if (specifier.endsWith('.ts') && fs.existsSync(specifierPath.slice(0, -3) + '.js')) return { url: specifier.slice(0, -3) + '.js', format, shortCircuit: true };
if (specifier.endsWith('.js') && fs.existsSync(specifierPath.slice(0, -3) + '.ts')) return { url: specifier.slice(0, -3) + '.ts', format: 'ts' + format, shortCircuit: true };
if (fs.existsSync(specifierPath + '.ts')) return { url: specifier + '.ts', format: 'ts' + format, shortCircuit: true };
if (fs.existsSync(specifierPath + '.js')) return { url: specifier + '.js', format, shortCircuit: true };
if (fs.existsSync(specifierPath + '.json')) return { url: specifier + '.json', format: 'json', shortCircuit: true };
if (fs.existsSync(specifierPath + '/index.ts')) return { url: specifier + '/index.ts', format: 'ts' + format, shortCircuit: true };
if (fs.existsSync(specifierPath + '/index.js')) return { url: specifier + '/index.js', format, shortCircuit: true };
if (fs.existsSync(specifierPath + '/index.json')) return { url: specifier + '/index.json', format: 'json', shortCircuit: true };
}
if (next instanceof Error) throw next;
else return next;
}
const APPLY_IMPORT_META_POLYFILL = (/*js*/`
;(await import("${pathToFileURL(path.resolve(libRoot, 'global', 'importmeta.js')).href}")).default(import.meta);` +
/*js*/`Reflect.set(globalThis, 'require', (await import('node:module')).createRequire(import.meta.url));`
).trim();
/** @type {load} */
export async function load(url, context, nextLoad) {
//console.debug('Loading', url, 'with context', context);
if (url.startsWith('bun:wrap@')) {
return {
shortCircuit: true, format: 'module', source: /*js*/`
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url.slice(9));
export const __require = require;
export default new Proxy({
__require: require,
}, {
get(target, prop) {
return target[prop];
},
});`
};
}
if (context.format === 'tsmodule' || context.format === 'tscommonjs') {
const filepath = fileURLToPath(url);
const src = fs.readFileSync(filepath, 'utf-8');
const transform = bunwasm.transformSync(src, path.basename(filepath), 'ts');
if (transform.status === TransformResponseStatus.fail) {
if (transform.errors.length) {
throw formatBuildErrors(transform.errors);
} else {
const err = new Error('Unknown transform error');
Error.captureStackTrace(err, NO_STACK);
throw err;
}
}
const jsSrc = decoder.decode(transform.files[0].data);
// For debugging purposes:
if (process.env.BUN_POLYFILLS_DUMP_TEST_JS)
fs.writeFileSync(`/tmp/bun-polyfills-testrunner-transformed--${url.split('/').at(-1)}.js`, jsSrc);
return {
shortCircuit: true,
format: /** @type {ModuleFormat} */(context.format.slice(2)),
source: (context.format === 'tsmodule' ? (url.includes('/bun-polyfills/') ? '' : APPLY_IMPORT_META_POLYFILL) : '') + jsSrc,
};
}
if (context.format === 'json') context.importAssertions.type = 'json';
if (context.format === 'commonjs' && !url.includes('/node_modules/')) context.format = 'module';
const loaded = await nextLoad(url, context);
if (url.startsWith('file://') && loaded.format === 'module') {
const src = typeof loaded.source === 'string' ? loaded.source : decoder.decode(loaded.source);
return {
shortCircuit: true,
format: 'module',
source: (url.includes('/bun-polyfills/') ? '' : APPLY_IMPORT_META_POLYFILL) + src
};
}
else return loaded;
}
/** @type {globalPreload} */
export function globalPreload(ctx) {
return /*js*/`
process.env.BUN_POLYFILLS_TEST_RUNNER = 1;
process.execArgv.push('--input-type=module');
`;
}
/** @param {import('bun-wasm/schema').Message[]} buildErrors */
function formatBuildErrors(buildErrors) {
const formatted = buildErrors.map(err => {
const loc = err.data.location;
const str = `${$.redBright('error')}${$.gray(':')} ${$.bold(err.data.text)}\n` +
(loc
? `${highlightErrorChar(loc.line_text, loc.column)}\n` +
$.redBright.bold('^'.padStart(loc.column)) + '\n' +
`${$.bold(loc.file)}${$.gray(':')}${$.yellowBright(loc.line)}${$.gray(':')}${$.yellowBright(loc.column)} ${$.gray(loc.offset)}`
: ''
);
const newerr = new Error(str);
newerr.name = 'BuildError';
newerr.stack = str;
return newerr;
});
const aggregate = new AggregateError(formatted, `Input code has ${formatted.length} error${formatted.length === 1 ? '' : 's'}`);
Error.captureStackTrace(aggregate, NO_STACK);
aggregate.name = 'BuildFailed';
return aggregate;
}
/**
* @param {string} str
* @param {number} at */
function highlightErrorChar(str, at) {
return str.slice(0, at) + $.red(str[at]) + str.slice(at + 1);
}

View File

@@ -0,0 +1,43 @@
// @ts-nocheck allowImportingTsExtensions
// Run with "bun run test-all"
await import('../../../test/js/bun/console/console-iterator.test.ts');
await import('../../../test/js/bun/dns/resolve-dns.test.ts');
await import('../../../test/js/bun/ffi/ffi.test.js');
// TODO: http
await import('../../../test/js/bun/io/bun-write.test.js');
await import('../../../test/js/bun/jsc/bun-jsc.test.js');
// TODO: net
// plugin: N/A
await import('../../../test/js/bun/spawn/exit-code.test.ts');
await import('../../../test/js/bun/spawn/spawn-streaming-stdin.test.ts');
await import('../../../test/js/bun/spawn/spawn-streaming-stdout.test.ts');
await import('../../../test/js/bun/spawn/spawn.test.ts');
await import('../../../test/js/bun/sqlite/sqlite.test.ts');
// stream: N/A
// test: N/A
await import('../../../test/js/bun/util/arraybuffersink.test.ts');
await import('../../../test/js/bun/util/bun-file-exists.test.js');
await import('../../../test/js/bun/util/bun-isMainThread.test.js');
await import('../../../test/js/bun/util/concat.test.js');
await import('../../../test/js/bun/util/error-gc-test.test.js');
await import('../../../test/js/bun/util/escapeHTML.test.js');
await import('../../../test/js/bun/util/file-type.test.ts');
await import('../../../test/js/bun/util/filesink.test.ts');
await import('../../../test/js/bun/util/fileUrl.test.js');
await import('../../../test/js/bun/util/hash.test.js');
await import('../../../test/js/bun/util/index-of-line.test.ts');
//await import('../../../test/js/bun/util/inspect.test.js'); //? Can't run because of JSX :(
await import('../../../test/js/bun/util/mmap.test.js');
await import('../../../test/js/bun/util/password.test.ts');
await import('../../../test/js/bun/util/peek.test.ts');
await import('../../../test/js/bun/util/readablestreamtoarraybuffer.test.ts');
await import('../../../test/js/bun/util/sleepSync.test.ts');
await import('../../../test/js/bun/util/unsafe.test.js');
await import('../../../test/js/bun/util/which.test.ts');
// TODO: websocket
await import('../../../test/js/bun/globals.test.js');
// this test has to be last to run due to some weird sync/async issues with the polyfills' test runner
await import('../../../test/js/bun/resolve/import-meta.test.js');
export { };

View File

@@ -1,21 +1,17 @@
/// <reference types="bun-types" />
import path from 'path';
const abort = (...msg: string[]): never => (console.error(...msg), process.exit(1));
const makefilePath = path.resolve(import.meta.dir, '../../../Makefile');
const makefile = Bun.file(makefilePath);
if (!await makefile.exists()) abort('Makefile not found at', makefilePath);
const cmakelistsPath = path.resolve(import.meta.dir, '../../../CMakeLists.txt');
const cmakelistsFile = Bun.file(cmakelistsPath);
if (!await cmakelistsFile.exists()) abort('CMakeLists.txt not found at', cmakelistsPath);
const makefileContent = await makefile.text();
const matched = makefileContent.match(/^BUN_BASE_VERSION\s*=\s*(\d+.\d+)/m);
if (!matched) abort('Could not find BUN_BASE_VERSION in Makefile');
const cmakelists = await cmakelistsFile.text();
const matchBunVer = cmakelists.match(/^set\(Bun_VERSION\s+"(.+)"\)/m);
if (!matchBunVer) abort('Could not find Bun_VERSION in CMakeLists.txt');
const buildidPath = path.resolve(import.meta.dir, '../../../src/build-id');
const buildid = Bun.file(buildidPath);
if (!await buildid.exists()) abort('Build ID file not found at', buildidPath);
const [, BUN_BASE_VERSION] = matched!;
const BUN_VERSION = `${BUN_BASE_VERSION}.${await buildid.text()}`.trim();
const BUN_VERSION = matchBunVer![1].trim();
const bunTsPath = path.resolve(import.meta.dir, '../src/modules/bun.ts');
const bunTs = Bun.file(bunTsPath);
@@ -23,7 +19,7 @@ if (!await bunTs.exists()) abort('bun.ts source file not found at', bunTsPath);
const bunTsContent = await bunTs.text();
const bunTsContentNew = bunTsContent.replace(
/^export const version = '.+' satisfies typeof Bun.version;$/m,
/^export const version = '.+' satisfies typeof Bun\.version;$/m,
`export const version = '${BUN_VERSION}' satisfies typeof Bun.version;`
);
if (bunTsContentNew !== bunTsContent) console.info('Updated Bun.version polyfill to', BUN_VERSION);
@@ -33,9 +29,44 @@ if (!git.success) abort('Could not get git HEAD commit hash');
const BUN_REVISION = git.stdout.toString('utf8').trim();
const bunTsContentNewer = bunTsContentNew.replace(
/^export const revision = '.+' satisfies typeof Bun.revision;$/m,
/^export const revision = '.+' satisfies typeof Bun\.revision;$/m,
`export const revision = '${BUN_REVISION}' satisfies typeof Bun.revision;`
);
if (bunTsContentNewer !== bunTsContentNew) console.info('Updated Bun.revision polyfill to', BUN_REVISION);
Bun.write(bunTs, bunTsContentNewer);
const processTsPath = path.resolve(import.meta.dir, '../src/global/process.ts');
const processTsFile = Bun.file(processTsPath);
if (!await processTsFile.exists()) abort('process.ts source file not found at', processTsPath);
const processTsContent = await processTsFile.text();
const genVerListPath = path.resolve(import.meta.dir, '../../../src/generated_versions_list.zig');
const genVerListFile = Bun.file(genVerListPath);
if (!await genVerListFile.exists()) abort('generated_versions_list.zig source file not found at', genVerListPath);
const codegenLines: string[] = [];
const genVerList = await genVerListFile.text();
for (const match of genVerList.matchAll(/^pub const (?<name>\w+) = "(?<version>.+)";$/gm)) {
const { name, version } = match.groups!;
if (name === 'zlib') continue;
codegenLines.push(` process.versions.${name} = '${version}' satisfies Process['versions'][string];`);
}
const buildZigPath = path.resolve(import.meta.dir, '../../../build.zig');
const buildZigFile = Bun.file(buildZigPath);
if (!await buildZigFile.exists()) abort('build.zig source file not found at', buildZigPath);
const buildZig = await buildZigFile.text();
const matchZigVer = buildZig.match(/^const recommended_zig_version = "(.+)";$/m);
if (!matchZigVer) abort('Could not find recommended_zig_version in build.zig');
const ZIG_VERSION = matchZigVer![1].trim();
Bun.write(processTsFile, processTsContent.replace(
/\/\*\*\s*@start_generated_code\s*\*\/[^]*?\/\*\*\s*@end_generated_code\s*\*\//,
`/** @start_generated_code */
${codegenLines.join('\n')}
process.versions.zig = '${ZIG_VERSION}' satisfies Process['versions'][string];
process.versions.bun = '${BUN_VERSION}' satisfies Process['versions'][string];
Reflect.set(process, 'revision', '${BUN_REVISION}' satisfies Process['revision']);
/** @end_generated_code */`
));

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"module": "nodenext",
"target": "esnext",
"moduleResolution": "nodenext",
"moduleDetection": "force",
@@ -10,6 +10,7 @@
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"inlineSourceMap": true,
"allowJs": true,
"outDir": "dist",

View File

@@ -69,7 +69,7 @@ pub const Reader = struct {
return try this.read(length);
},
u16, u32, i8, i16, i32 => {
return std.mem.readIntSliceNative(T, this.read(length * @sizeOf(T)));
return std.mem.readInt(T, this.read(length * @sizeOf(T))[0 .. length * @sizeOf(T)], @import("builtin").cpu.arch.endian());
},
[:0]const u8, []const u8 => {
var i: u32 = 0;
@@ -121,7 +121,7 @@ pub const Reader = struct {
pub inline fn readInt(this: *Self, comptime T: type) !T {
var slice = try this.read(@sizeOf(T));
return std.mem.readIntSliceNative(T, slice);
return std.mem.readInt(T, slice[0..@sizeOf(T)], @import("builtin").cpu.arch.endian());
}
pub inline fn readBool(this: *Self) !bool {
@@ -147,7 +147,7 @@ pub const Reader = struct {
return try this.readArray([]u8);
},
u16, u32, i8, i16, i32 => {
return std.mem.readIntSliceNative(T, try this.read(@sizeOf(T)));
return std.mem.readInt(T, (try this.read(@sizeOf(T)))[0..@sizeOf(T)], @import("builtin").cpu.arch.endian());
},
else => {
switch (comptime @typeInfo(T)) {

View File

@@ -16,7 +16,7 @@ describe("should work for static input", () => {
for (let input of inputs) {
it(input.replaceAll("\n", "\\n"), () => {
const { stdout } = spawnSync({
cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run.ts"],
cmd: [bunExe(), ...process.execArgv, import.meta.dir + "/" + "console-iterator-run.ts"],
stdin: Buffer.from(input),
env: {
BUN_DEBUG_QUIET_LOGS: "1",
@@ -41,7 +41,7 @@ describe("should work for streaming input", () => {
for (let input of inputs) {
it(input.replaceAll("\n", "\\n"), async () => {
const proc = spawn({
cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run.ts"],
cmd: [bunExe(), ...process.execArgv, import.meta.dir + "/" + "console-iterator-run.ts"],
stdin: "pipe",
stdout: "pipe",
env: {
@@ -65,7 +65,7 @@ describe("should work for streaming input", () => {
// https://github.com/oven-sh/bun/issues/5175
it("can use the console iterator more than once", async () => {
const proc = spawn({
cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run-2.ts"],
cmd: [bunExe(), ...process.execArgv, import.meta.dir + "/" + "console-iterator-run-2.ts"],
stdin: "pipe",
stdout: "pipe",
env: {

View File

@@ -55,7 +55,7 @@ describe("dns", () => {
expect(expectedAddress(address)).toBeTruthy();
expect(family).toBeInteger();
if (expectedFamily !== undefined) {
expect(family).toBe(expectedFamily);
expect(family).toBe(expectedFamily as 4 | 6);
}
expect(ttl).toBeInteger();
}
@@ -69,7 +69,7 @@ describe("dns", () => {
);
const answers = results.flat();
expect(answers).toBeArray();
expect(answers.length).toBeGreaterThan(10);
expect(answers.length).toBeGreaterThanOrEqual(10);
withoutAggressiveGC(() => {
for (const { family, address, ttl } of answers) {
expect(address).toBeString();
@@ -82,7 +82,7 @@ describe("dns", () => {
test.each(invalidHostnames)("%s", hostname => {
// @ts-expect-error
expect(dns.lookup(hostname, { backend })).rejects.toMatchObject({
code: "DNS_ENOTFOUND",
code: process.env.BUN_POLYFILLS_TEST_RUNNER ? "ENOTFOUND" : "DNS_ENOTFOUND",
});
});
// TODO: causes segfaults

View File

@@ -25,7 +25,7 @@ const dlopen = (...args) => {
};
const ok = existsSync("/tmp/bun-ffi-test." + suffix);
it("ffi print", async () => {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("ffi print", async () => {
await Bun.write(
import.meta.dir + "/ffi.test.fixture.callback.c",
viewSource(
@@ -377,7 +377,7 @@ function ffiRunner(fast) {
getDeallocatorBuffer,
},
close,
} = dlopen("/tmp/bun-ffi-test.dylib", types);
} = dlopen(`/tmp/bun-ffi-test.${suffix}`, types);
it("primitives", () => {
Bun.gc(true);
expect(returns_true()).toBe(true);
@@ -470,13 +470,15 @@ function ffiRunner(fast) {
expect(typeof cptr === "number").toBe(true);
expect(does_pointer_equal_42_as_int32_t(cptr)).toBe(true);
const buffer = toBuffer(cptr, 0, 4);
expect(buffer.readInt32(0)).toBe(42);
expect(buffer.readInt32LE(0)).toBe(42);
expect(new DataView(toArrayBuffer(cptr, 0, 4), 0, 4).getInt32(0, true)).toBe(42);
expect(ptr(buffer)).toBe(cptr);
expect(new CString(cptr, 0, 1).toString()).toBe("*");
expect(identity_ptr(cptr)).toBe(cptr);
const second_ptr = ptr(new Buffer(8));
expect(identity_ptr(second_ptr)).toBe(second_ptr);
if (!process.env.BUN_POLYFILLS_TEST_RUNNER) {
expect(identity_ptr(second_ptr)).toBe(second_ptr);
}
});
it("CFunction", () => {
@@ -502,7 +504,7 @@ function ffiRunner(fast) {
"void*": null,
};
it("JSCallback", () => {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("JSCallback", () => {
var toClose = new JSCallback(
input => {
return input;
@@ -517,7 +519,7 @@ function ffiRunner(fast) {
expect(toClose.ptr === null).toBe(true);
});
describe("callbacks", () => {
describe.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("callbacks", () => {
// Return types, 1 argument
for (let [returnName, returnValue] of Object.entries(typeMap)) {
it("fn(" + returnName + ") " + returnName, () => {
@@ -551,7 +553,7 @@ function ffiRunner(fast) {
}
});
describe("threadsafe callback", done => {
describe.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("threadsafe callback", done => {
// 1 arg, threadsafe
for (let [name, value] of Object.entries(typeMap)) {
it("fn(" + name + ") " + name, async () => {
@@ -954,6 +956,9 @@ test("can open more than 63 symbols", () => {
});
expect(Object.keys(lib.symbols).length).toBe(65);
expect(lib.symbols.strcasecmp(Buffer.from("ciro"), Buffer.from("CIRO"))).toBe(0);
expect(lib.symbols.strlen(Buffer.from("bunbun", "ascii"))).toBe(6n);
// This seems to be flaky with koffi for some reason, but only when running all tests together. (?)
if (!process.env.BUN_POLYFILLS_TEST_RUNNER) {
expect(lib.symbols.strcasecmp(Buffer.from("ciro"), Buffer.from("CIRO"))).toBe(0);
expect(lib.symbols.strlen(Buffer.from("bunbun", "ascii"))).toBe(6n);
}
});

View File

@@ -5,8 +5,9 @@ import path from "path";
it("extendable", () => {
const classes = [Blob, TextDecoder, TextEncoder, Request, Response, Headers, HTMLRewriter, Bun.Transpiler, Buffer];
for (let Class of classes) {
if (process.env.BUN_POLYFILLS_TEST_RUNNER && Class === Buffer) continue;
var Foo = class extends Class {};
var bar = Class === Request ? new Request({ url: "https://example.com" }) : new Foo();
var bar = Class === Request ? new Request("https://example.com") : new Foo();
expect(bar instanceof Class).toBe(true);
expect(!!Class.prototype).toBe(true);
expect(typeof Class.prototype).toBe("object");
@@ -24,12 +25,14 @@ it("writable", () => {
["Event", Event],
["DOMException", DOMException],
["EventTarget", EventTarget],
["ErrorEvent", ErrorEvent],
["ErrorEvent", process.env.BUN_POLYFILLS_TEST_RUNNER ? null : ErrorEvent],
["CustomEvent", CustomEvent],
["CloseEvent", CloseEvent],
["CloseEvent", process.env.BUN_POLYFILLS_TEST_RUNNER ? null : CloseEvent],
["File", File],
];
for (let [name, Class] of classes) {
if (!Class) continue;
if (process.env.BUN_POLYFILLS_TEST_RUNNER && name === "Response") continue;
globalThis[name] = 123;
expect(globalThis[name]).toBe(123);
globalThis[name] = Class;
@@ -51,7 +54,12 @@ it("name", () => {
["File", File],
];
for (let [name, Class] of classes) {
expect(Class.name).toBe(name);
if (process.env.BUN_POLYFILLS_TEST_RUNNER) {
if (Class.name.startsWith("_"))
expect(Class.name.slice(1)).toBe(name); // _Request, _Response, _Headers... why Node? Just why.
else if (Class.name.endsWith("2")) expect(Class.name.slice(0, -1)).toBe(name); // Response2 monkeypatch by Hono
else expect(Class.name).toBe(name);
} else expect(Class.name).toBe(name);
}
});
@@ -61,7 +69,7 @@ describe("File", () => {
expect(file.name).toBe("bar.txt");
expect(file.type).toBe("text/plain;charset=utf-8");
expect(file.size).toBe(3);
expect(file.lastModified).toBe(0);
if (!process.env.BUN_POLYFILLS_TEST_RUNNER) expect(file.lastModified).toBe(0);
});
it("constructor with lastModified", () => {
@@ -77,7 +85,7 @@ describe("File", () => {
expect(file.name).toBe("undefined");
expect(file.type).toBe("");
expect(file.size).toBe(3);
expect(file.lastModified).toBe(0);
if (!process.env.BUN_POLYFILLS_TEST_RUNNER) expect(file.lastModified).toBe(0);
});
it("constructor throws invalid args", () => {
@@ -129,14 +137,14 @@ describe("File", () => {
expect(foo.name).toBe("bar.txt");
expect(foo.type).toBe("text/plain;charset=utf-8");
expect(foo.size).toBe(3);
expect(foo.lastModified).toBe(0);
if (!process.env.BUN_POLYFILLS_TEST_RUNNER) expect(foo.lastModified).toBe(0);
expect(await foo.text()).toBe("foo");
});
});
it("globals are deletable", () => {
const { stdout, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "run", path.join(import.meta.dir, "deletable-globals-fixture.js")],
cmd: [bunExe(), ...process.execArgv, path.join(import.meta.dir, "deletable-globals-fixture.js")],
env: bunEnv,
stderr: "inherit",
});

View File

@@ -80,7 +80,7 @@ it("Bun.file not found returns ENOENT", async () => {
await gcTick();
});
it("Bun.write file not found returns ENOENT, issue#6336", async () => {
it("Bun.write file not found returns ENOENT, issue #6336", async () => {
const dst = Bun.file(path.join(tmpdir(), "does/not/exist.txt"));
try {
await gcTick();
@@ -330,7 +330,7 @@ it("#2674", async () => {
const file = path.join(import.meta.dir, "big-stdout.js");
const { stderr, stdout, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "run", file],
cmd: [bunExe(), ...process.execArgv, file],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
@@ -359,13 +359,13 @@ if (process.platform === "linux") {
const dest = join(tempdir, "Bun.write.dest.blob");
try {
fs.writeFileSync(src, buffer.buffer);
fs.writeFileSync(src, buffer);
expect(fs.existsSync(dest)).toBe(false);
const { exitCode } = Bun.spawnSync({
stdio: ["inherit", "inherit", "inherit"],
cmd: [bunExe(), join(import.meta.dir, "./bun-write-exdev-fixture.js"), src, dest],
cmd: [bunExe(), ...process.execArgv, join(import.meta.dir, "./bun-write-exdev-fixture.js"), src, dest],
env: {
...bunEnv,
BUN_CONFIG_DISABLE_COPY_FILE_RANGE: "1",
@@ -394,13 +394,13 @@ if (process.platform === "linux") {
const dest = join(tempdir, "Bun.write.dest.blob");
try {
fs.writeFileSync(src, buffer.buffer);
fs.writeFileSync(src, buffer);
expect(fs.existsSync(dest)).toBe(false);
const { exitCode } = Bun.spawnSync({
stdio: ["inherit", "inherit", "inherit"],
cmd: [bunExe(), join(import.meta.dir, "./bun-write-exdev-fixture.js"), src, dest],
cmd: [bunExe(), ...process.execArgv, join(import.meta.dir, "./bun-write-exdev-fixture.js"), src, dest],
env: {
...bunEnv,
BUN_CONFIG_DISABLE_COPY_FILE_RANGE: "1",
@@ -426,7 +426,7 @@ describe("ENOENT", () => {
await Bun.write(file, "contents", ...opts);
expect(fs.existsSync(file)).toBe(true);
} finally {
fs.rmSync(dir, { force: true });
fs.rmSync(dir, { force: true, recursive: true });
}
});
};
@@ -441,8 +441,8 @@ describe("ENOENT", () => {
const dir = `${tmpdir()}/fs.test.js/${Date.now()}-1/bun-write/ENOENT`;
const file = join(dir, "file");
try {
expect(async () => await Bun.write(file, "contents", { createPath: false })).toThrow(
"No such file or directory",
expect(async () => await Bun.write(file, "contents", { createPath: false })).rejects.toThrow(
process.env.BUN_POLYFILLS_TEST_RUNNER ? /^ENOENT:/ : "No such file or directory",
);
expect(fs.existsSync(file)).toBe(false);
} finally {
@@ -452,9 +452,15 @@ describe("ENOENT", () => {
it("throws when given a file descriptor", async () => {
const file = Bun.file(123);
expect(async () => await Bun.write(file, "contents", { createPath: true })).toThrow(
"Cannot create a directory for a file descriptor",
);
if (process.env.BUN_POLYFILLS_TEST_RUNNER) {
expect(async () => await Bun.write(file, "contents", { createPath: true })).rejects.toThrow(
"Cannot create a directory for a file descriptor",
);
} else {
expect(async () => await Bun.write(file, "contents", { createPath: true })).toThrow(
"Cannot create a directory for a file descriptor",
);
}
});
});
});

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "bun:test";
import {
describe as jscDescribe,
describeArray,
jscDescribe,
jscDescribeArray,
serialize,
deserialize,
gcAndSweep,
@@ -37,11 +37,11 @@ describe("bun:jsc", () => {
return j;
}
it("describe", () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("describe", () => {
expect(jscDescribe([])).toBeDefined();
});
it("describeArray", () => {
expect(describeArray([1, 2, 3])).toBeDefined();
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("describeArray", () => {
expect(jscDescribeArray([1, 2, 3])).toBeDefined();
});
it("gcAndSweep", () => {
expect(gcAndSweep()).toBeGreaterThan(0);
@@ -72,11 +72,11 @@ describe("bun:jsc", () => {
it("setRandomSeed", () => {
expect(setRandomSeed(2)).toBeUndefined();
});
it("isRope", () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("isRope", () => {
expect(isRope("a" + 123 + "b")).toBe(true);
expect(isRope("abcdefgh")).toBe(false);
});
it("callerSourceOrigin", () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("callerSourceOrigin", () => {
expect(callerSourceOrigin()).toBe(import.meta.url);
});
it("noFTL", () => {});
@@ -86,7 +86,7 @@ describe("bun:jsc", () => {
expect(optimizeNextInvocation(count)).toBeUndefined();
count();
});
it("numberOfDFGCompiles", () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("numberOfDFGCompiles", () => {
expect(numberOfDFGCompiles(count)).toBeGreaterThan(0);
});
it("releaseWeakRefs", () => {
@@ -154,7 +154,7 @@ describe("bun:jsc", () => {
it("serialize (binaryType: 'nodebuffer')", () => {
const serialized = serialize({ a: 1 }, { binaryType: "nodebuffer" });
expect(serialized).toBeInstanceOf(Buffer);
expect(serialized.buffer).toBeInstanceOf(SharedArrayBuffer);
expect(serialized.buffer).toBeInstanceOf(process.env.BUN_POLYFILLS_TEST_RUNNER ? ArrayBuffer : SharedArrayBuffer);
expect(deserialize(serialized)).toStrictEqual({ a: 1 });
const nested = serialize(serialized);
expect(deserialize(deserialize(nested))).toStrictEqual({ a: 1 });

View File

@@ -1,4 +1,4 @@
import { listen, connect, TCPSocketListener, SocketHandler } from "bun";
import { listen, connect, type TCPSocketListener, type SocketHandler } from "bun";
import { describe, expect, it } from "bun:test";
import { expectMaxObjectTypeCount } from "harness";

View File

@@ -4,13 +4,14 @@ import { bunEnv, bunExe } from "harness";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import * as Module from "node:module";
import { join } from "node:path";
// @ts-expect-error tsconfig warning
import sync from "./require-json.json";
const { path, dir } = import.meta;
it("import.meta.main", () => {
const { exitCode } = spawnSync({
cmd: [bunExe(), "run", join(import.meta.dir, "./main-test-script.js")],
cmd: [bunExe(), ...process.execArgv, join(import.meta.dir, "./main-test-script.js")],
env: bunEnv,
stderr: "inherit",
stdout: "inherit",
@@ -59,38 +60,41 @@ it("Module.createRequire does not use file url as the referrer (err message chec
} catch (e) {
expect(e.name).not.toBe("UnreachableError");
expect(e.message).not.toInclude("file:///");
expect(e.message).toInclude('"whaaat"');
expect(e.message).toInclude('"' + import.meta.path + '"');
expect(e.message).toInclude("whaaat");
expect(e.message).toInclude(import.meta.path);
}
});
it("require with a query string works on dynamically created content", () => {
rmSync("/tmp/bun-test-import-meta-dynamic-dir", {
recursive: true,
force: true,
});
try {
const require = Module.createRequire("/tmp/bun-test-import-meta-dynamic-dir/foo.js");
try {
require("./bar.js?query=123.js");
} catch (e) {
expect(e.name).toBe("ResolveMessage");
}
mkdirSync("/tmp/bun-test-import-meta-dynamic-dir", { recursive: true });
writeFileSync("/tmp/bun-test-import-meta-dynamic-dir/bar.js", "export default 'hello';", "utf8");
expect(require("./bar.js?query=123.js").default).toBe("hello");
} catch (e) {
throw e;
} finally {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)(
"require with a query string works on dynamically created content",
() => {
rmSync("/tmp/bun-test-import-meta-dynamic-dir", {
recursive: true,
force: true,
});
}
});
try {
const require = Module.createRequire("/tmp/bun-test-import-meta-dynamic-dir/foo.js");
try {
require("./bar.js?query=123.js");
} catch (e) {
expect(e.name).toBe(process.env.BUN_POLYFILLS_TEST_RUNNER ? "Error" : "ResolveMessage");
}
mkdirSync("/tmp/bun-test-import-meta-dynamic-dir", { recursive: true });
writeFileSync("/tmp/bun-test-import-meta-dynamic-dir/bar.js", "export default 'hello';", "utf8");
expect(require("./bar.js?query=123.js").default).toBe("hello");
} catch (e) {
throw e;
} finally {
rmSync("/tmp/bun-test-import-meta-dynamic-dir", {
recursive: true,
force: true,
});
}
},
);
it("import.meta.require (json)", () => {
expect(import.meta.require("./require-json.json").hello).toBe(sync.hello);
@@ -123,11 +127,11 @@ it("Module._cache", () => {
expect(!!expected).toBe(true);
});
it("Module._resolveFilename()", () => {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("Module._resolveFilename()", () => {
expect(Module._resolveFilename).toBeUndefined();
});
it("Module.createRequire(file://url).resolve(file://url)", () => {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("Module.createRequire(file://url).resolve(file://url)", () => {
const expected = Bun.resolveSync("./require-json.json", import.meta.dir);
const createdRequire = Module.createRequire(import.meta.url);
@@ -144,7 +148,7 @@ it("import.meta.require.resolve", () => {
expect(result).toBe(expected);
});
it("import.meta.require (javascript)", () => {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("import.meta.require (javascript)", () => {
expect(import.meta.require("./require-js.js").hello).toBe(sync.hello);
const require = Module.createRequire(import.meta.path);
expect(require("./require-js.js").hello).toBe(sync.hello);
@@ -154,7 +158,7 @@ it("import() require + TLA", async () => {
expect((await import("./import-require-tla.js")).foo).toBe("bar");
});
it("import.meta.require (javascript, live bindings)", () => {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)("import.meta.require (javascript, live bindings)", () => {
var Source = import.meta.require("./import.live.decl.js");
// require transpiles to import.meta.require
@@ -187,14 +191,14 @@ it("import.meta.require (javascript, live bindings)", () => {
});
it("import.meta.dir", () => {
expect(dir.endsWith("/bun/test/js/bun/resolve")).toBe(true);
expect(dir.endsWith("/test/js/bun/resolve")).toBe(true);
});
it("import.meta.path", () => {
expect(path.endsWith("/bun/test/js/bun/resolve/import-meta.test.js")).toBe(true);
expect(path.endsWith("/test/js/bun/resolve/import-meta.test.js")).toBe(true);
});
it('require("bun") works', () => {
it.skipIf(process.env.BUN_POLYFILLS_TEST_RUNNER)('require("bun") works', () => {
expect(require("bun")).toBe(Bun);
});
@@ -221,7 +225,7 @@ it("require.resolve error code", () => {
it("import non exist error code", async () => {
try {
await import("node:missing");
await import("missing");
throw 1;
} catch (e) {
expect(e.code).toBe("ERR_MODULE_NOT_FOUND");

View File

@@ -15,7 +15,7 @@ test("spawn can write to stdin multiple chunks", async () => {
const tmperr = join(tmpdir(), "stdin-repro-error.log." + i);
const proc = spawn({
cmd: [bunExe(), import.meta.dir + "/stdin-repro.js"],
cmd: [bunExe(), ...process.execArgv, import.meta.dir + "/stdin-repro.js"],
stdout: "pipe",
stdin: "pipe",
stderr: Bun.file(tmperr),

View File

@@ -9,7 +9,7 @@ test("spawn can read from stdout multiple chunks", async () => {
for (let i = 0; i < 100; i++) {
await (async function () {
const proc = spawn({
cmd: [bunExe(), import.meta.dir + "/spawn-streaming-stdout-repro.js"],
cmd: [bunExe(), ...process.execArgv, import.meta.dir + "/spawn-streaming-stdout-repro.js"],
stdin: "ignore",
stdout: "pipe",
stderr: "ignore",

View File

@@ -1,7 +1,7 @@
import { ArrayBufferSink, readableStreamToText, spawn, spawnSync, write } from "bun";
import { describe, expect, it } from "bun:test";
import { gcTick as _gcTick, bunExe, bunEnv } from "harness";
import { rmSync, writeFileSync } from "node:fs";
import { rmSync, writeFileSync, readFileSync } from "node:fs";
import path from "path";
for (let [gcTick, label] of [
@@ -53,7 +53,7 @@ for (let [gcTick, label] of [
cmd: ["echo", "hi"],
cwd: "./this-should-not-exist",
});
}).toThrow("No such file or directory");
}).toThrow(process.env.BUN_POLYFILLS_TEST_RUNNER ? "spawnSync echo ENOENT" : "No such file or directory");
});
});
@@ -103,7 +103,7 @@ for (let [gcTick, label] of [
});
gcTick();
await exited;
expect(require("fs").readFileSync("/tmp/out.123.txt", "utf8")).toBe(hugeString);
expect(readFileSync("/tmp/out.123.txt", "utf8")).toBe(hugeString);
gcTick();
});
@@ -132,45 +132,49 @@ for (let [gcTick, label] of [
gcTick();
});
it("check exit code from onExit", async () => {
for (let i = 0; i < 1000; i++) {
var exitCode1, exitCode2;
await new Promise<void>(resolve => {
var counter = 0;
spawn({
cmd: ["ls"],
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
onExit(subprocess, code) {
exitCode1 = code;
counter++;
if (counter === 2) {
resolve();
}
},
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)(
"check exit code from onExit",
async () => {
for (let i = 0; i < 1000; i++) {
var exitCode1, exitCode2;
await new Promise<void>(resolve => {
var counter = 0;
spawn({
cmd: ["ls"],
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
onExit(subprocess, code) {
exitCode1 = code;
counter++;
if (counter === 2) {
resolve();
}
},
});
spawn({
cmd: ["false"],
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
onExit(subprocess, code) {
exitCode2 = code;
counter++;
if (counter === 2) {
resolve();
}
},
});
});
spawn({
cmd: ["false"],
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
onExit(subprocess, code) {
exitCode2 = code;
counter++;
if (counter === 2) {
resolve();
}
},
});
});
expect(exitCode1).toBe(0);
expect(exitCode2).toBe(1);
}
}, 60_000_0);
expect<number>(exitCode1).toBe(0);
expect<number>(exitCode2).toBe(1);
}
},
60_000_0,
);
// FIXME: fix the assertion failure
it.skip("Uint8Array works as stdout", () => {
@@ -330,7 +334,8 @@ for (let [gcTick, label] of [
await prom;
});
it("stdin can be read and stdout can be written", async () => {
// skipped on polyfills because of https://github.com/nodejs/node/issues/21941
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("stdin can be read and stdout can be written", async () => {
const proc = spawn({
cmd: ["bash", import.meta.dir + "/bash-echo.sh"],
stdout: "pipe",
@@ -451,10 +456,10 @@ for (let [gcTick, label] of [
}
});
describe("ipc", () => {
describe.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("ipc", () => {
it("the subprocess should be defined and the child should send", done => {
gcTick();
const returned_subprocess = spawn([bunExe(), path.join(__dirname, "bun-ipc-child.js")], {
const returned_subprocess = spawn([bunExe(), ...process.execArgv, path.join(__dirname, "bun-ipc-child.js")], {
ipc: (message, subProcess) => {
expect(subProcess).toBe(returned_subprocess);
expect(message).toBe("hello");
@@ -469,7 +474,7 @@ for (let [gcTick, label] of [
gcTick();
const parentMessage = "I am your father";
const childProc = spawn([bunExe(), path.join(__dirname, "bun-ipc-child-respond.js")], {
const childProc = spawn([bunExe(), ...process.execArgv, path.join(__dirname, "bun-ipc-child-respond.js")], {
ipc: (message, subProcess) => {
expect(message).toBe(`pong:${parentMessage}`);
subProcess.kill();
@@ -489,29 +494,33 @@ for (let [gcTick, label] of [
cmd: ["echo", "hi"],
cwd: "./this-should-not-exist",
});
}).toThrow("No such file or directory");
}).toThrow(process.env.BUN_POLYFILLS_TEST_RUNNER ? "spawnSync echo ENOENT" : "No such file or directory");
});
});
});
}
if (!process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD) {
it("with BUN_FEATURE_FLAG_FORCE_WAITER_THREAD", async () => {
const result = spawnSync({
cmd: [bunExe(), "test", import.meta.path],
env: {
...bunEnv,
// Both flags are necessary to force this condition
"BUN_FEATURE_FLAG_FORCE_WAITER_THREAD": "1",
"BUN_GARBAGE_COLLECTOR_LEVEL": "1",
},
});
if (result.exitCode !== 0) {
console.error(result.stderr.toString());
console.log(result.stdout.toString());
}
expect(result.exitCode).toBe(0);
}, 60_000);
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)(
"with BUN_FEATURE_FLAG_FORCE_WAITER_THREAD",
async () => {
const result = spawnSync({
cmd: [bunExe(), "test", import.meta.path],
env: {
...bunEnv,
// Both flags are necessary to force this condition
"BUN_FEATURE_FLAG_FORCE_WAITER_THREAD": "1",
"BUN_GARBAGE_COLLECTOR_LEVEL": "1",
},
});
if (result.exitCode !== 0) {
console.error(result.stderr.toString());
console.log(result.stdout.toString());
}
expect(result.exitCode).toBe(0);
},
60_000,
);
}
describe("spawn unref and kill should not hang", () => {
@@ -529,7 +538,7 @@ describe("spawn unref and kill should not hang", () => {
expect().pass();
});
it("unref", async () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("unref", async () => {
for (let i = 0; i < 100; i++) {
const proc = spawn({
cmd: ["sleep", "0.001"],
@@ -543,7 +552,7 @@ describe("spawn unref and kill should not hang", () => {
expect().pass();
});
it("kill and unref", async () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("kill and unref", async () => {
for (let i = 0; i < 100; i++) {
const proc = spawn({
cmd: ["sleep", "0.001"],
@@ -558,7 +567,7 @@ describe("spawn unref and kill should not hang", () => {
expect().pass();
});
it("unref and kill", async () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("unref and kill", async () => {
for (let i = 0; i < 100; i++) {
const proc = spawn({
cmd: ["sleep", "0.001"],
@@ -576,7 +585,7 @@ describe("spawn unref and kill should not hang", () => {
it("should not hang after unref", async () => {
const proc = spawn({
cmd: [bunExe(), path.join(import.meta.dir, "does-not-hang.js")],
cmd: [bunExe(), ...process.execArgv, path.join(import.meta.dir, "does-not-hang.js")],
});
await proc.exited;
@@ -624,7 +633,7 @@ async function runTest(sleep: string, order = ["sleep", "kill", "unref", "exited
expect().pass();
}
describe("should not hang", () => {
describe.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("should not hang", () => {
for (let sleep of ["0.001", "0"]) {
describe("sleep " + sleep, () => {
for (let order of [
@@ -653,7 +662,7 @@ describe("should not hang", () => {
}
});
it("#3480", async () => {
it.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("#3480", async () => {
try {
var server = Bun.serve({
port: 0,

View File

@@ -12,16 +12,22 @@ it("Database.open", () => {
Database.open("/this/database/does/not/exist.sqlite", constants.SQLITE_OPEN_READWRITE);
throw new Error("Expected an error to be thrown");
} catch (error) {
expect(error.message).toBe("unable to open database file");
expect(error.message).toBe(
process.env.BUN_POLYFILLS_TEST_RUNNER
? "Cannot open database because the directory does not exist"
: "unable to open database file",
);
}
// in a file which doesn't exist
try {
Database.open(`/tmp/database-${Math.random()}.sqlite`, constants.SQLITE_OPEN_READWRITE);
throw new Error("Expected an error to be thrown");
} catch (error) {
expect(error.message).toBe("unable to open database file");
}
if (!process.env.BUN_POLYFILLS_TEST_RUNNER)
try {
// not sure why this is supposed to error, better-sqlite3 has no issue with it?
Database.open(`/tmp/database-${Math.random()}.sqlite`, constants.SQLITE_OPEN_READWRITE);
throw new Error("Expected an error to be thrown");
} catch (error) {
expect(error.message).toBe("unable to open database file");
}
// in a file which doesn't exist
try {
@@ -32,12 +38,14 @@ it("Database.open", () => {
}
// in a file which doesn't exist
try {
Database.open(`/tmp/database-${Math.random()}.sqlite`, { readwrite: true });
throw new Error("Expected an error to be thrown");
} catch (error) {
expect(error.message).toBe("unable to open database file");
}
if (!process.env.BUN_POLYFILLS_TEST_RUNNER)
try {
// not sure why this is supposed to error, better-sqlite3 has no issue with it? (x2)
Database.open(`/tmp/database-${Math.random()}.sqlite`, { readwrite: true });
throw new Error("Expected an error to be thrown");
} catch (error) {
expect(error.message).toBe("unable to open database file");
}
// create works
{
@@ -54,7 +62,7 @@ it("Database.open", () => {
it("upsert cross-process, see #1366", () => {
const dir = realpathSync(tmpdir()) + "/";
const { exitCode } = spawnSync([bunExe(), import.meta.dir + "/sqlite-cross-process.js"], {
const { exitCode } = spawnSync([bunExe(), ...process.execArgv, import.meta.dir + "/sqlite-cross-process.js"], {
env: {
SQLITE_DIR: dir,
},
@@ -136,8 +144,8 @@ it("int52", () => {
it("typechecks", () => {
const db = Database.open(":memory:");
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
db.exec('INSERT INTO test (name) VALUES ("Hello")');
db.exec('INSERT INTO test (name) VALUES ("World")');
db.exec("INSERT INTO test (name) VALUES ('Hello')");
db.exec("INSERT INTO test (name) VALUES ('World')");
const q = db.prepare("SELECT * FROM test WHERE (name = ?)");
@@ -217,8 +225,8 @@ it("db.query supports TypedArray", () => {
it("supports serialize/deserialize", () => {
const db = Database.open(":memory:");
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
db.exec('INSERT INTO test (name) VALUES ("Hello")');
db.exec('INSERT INTO test (name) VALUES ("World")');
db.exec("INSERT INTO test (name) VALUES ('Hello')");
db.exec("INSERT INTO test (name) VALUES ('World')");
const input = db.serialize();
const db2 = new Database(input);
@@ -282,8 +290,8 @@ it("db.query()", () => {
var q = db.query("SELECT * FROM test WHERE name = ?");
expect(q.get("Hello") === null).toBe(true);
db.exec('INSERT INTO test (name) VALUES ("Hello")');
db.exec('INSERT INTO test (name) VALUES ("World")');
db.exec("INSERT INTO test (name) VALUES ('Hello')");
db.exec("INSERT INTO test (name) VALUES ('World')");
var rows = db.query("SELECT * FROM test WHERE name = ?").all(["Hello"]);
@@ -337,7 +345,9 @@ it("db.query()", () => {
try {
db.query("SELECT * FROM test where (name = ? OR name = ?)").all("Hello");
} catch (e) {
expect(e.message).toBe("Expected 2 values, got 1");
expect(e.message).toBe(
process.env.BUN_POLYFILLS_TEST_RUNNER ? "Too few parameter values were provided" : "Expected 2 values, got 1",
);
}
// named parameters
@@ -401,7 +411,9 @@ it("db.transaction()", () => {
]);
throw new Error("Should have thrown");
} catch (exception) {
expect(exception.message).toBe("constraint failed");
expect(exception.message).toBe(
process.env.BUN_POLYFILLS_TEST_RUNNER ? "UNIQUE constraint failed: cats.name" : "constraint failed",
);
}
expect(db.inTransaction).toBe(false);
@@ -423,7 +435,7 @@ it("inlineCapacity #987", async () => {
const path = "/tmp/bun-987.db";
if (!existsSync(path)) {
const arrayBuffer = await (await fetch("https://github.com/oven-sh/bun/files/9265429/logs.log")).arrayBuffer();
writeFileSync(path, arrayBuffer);
writeFileSync(path, new Uint8Array(arrayBuffer));
}
const db = new Database(path);
@@ -551,8 +563,8 @@ describe("Database.run", () => {
var q = db.query("SELECT * FROM test WHERE name = ?");
expect(q.get("Hello") === null).toBe(true);
db.exec('INSERT INTO test (name) VALUES ("Hello")');
db.exec('INSERT INTO test (name) VALUES ("World")');
db.exec("INSERT INTO test (name) VALUES ('Hello')");
db.exec("INSERT INTO test (name) VALUES ('World')");
try {
db.run(" ");

View File

@@ -4,12 +4,15 @@ import { bunEnv, bunExe } from "harness";
test("Bun.isMainThread", () => {
expect(Bun.isMainThread).toBeTrue();
const { stdout, exitCode } = Bun.spawnSync({
cmd: [bunExe(), import.meta.resolveSync("./main-worker-file.js")],
stderr: "inherit",
stdout: "pipe",
env: bunEnv,
});
expect(exitCode).toBe(0);
expect(stdout.toString()).toBe("isMainThread true\nisMainThread false\n");
if (!process.env.BUN_POLYFILLS_TEST_RUNNER) {
// can be removed once node has web Worker support
const { stdout, exitCode } = Bun.spawnSync({
cmd: [bunExe(), ...process.execArgv, import.meta.resolveSync("./main-worker-file.js")],
stderr: "inherit",
stdout: "pipe",
env: bunEnv,
});
expect(exitCode).toBe(0);
expect(stdout.toString()).toBe("isMainThread true\nisMainThread false\n");
}
});

View File

@@ -69,7 +69,7 @@ describe("FileSink", () => {
return path;
}
for (let isPipe of [true, false] as const) {
for (let isPipe of [!process.env.BUN_POLYFILLS_TEST_RUNNER, false] as const) {
describe(isPipe ? "pipe" : "file", () => {
for (const [input, expected, label] of fixtures) {
var getPathOrFd = () => (isPipe ? getFd(label) : getPath(label));

View File

@@ -1,6 +1,6 @@
import { test, expect, describe } from "bun:test";
import { Password, password } from "bun";
import { password } from "bun";
const placeholder = "hey";
@@ -195,7 +195,7 @@ for (let algorithmValue of algorithms) {
describe(algorithmValue ? algorithmValue : "default", () => {
const hash = (value: string | TypedArray) => {
return algorithmValue ? password.hashSync(value, algorithmValue as any) : password.hashSync(value);
return algorithmValue ? password.hash(value, algorithmValue as any) : password.hash(value);
};
const hashSync = (value: string | TypedArray) => {
@@ -212,7 +212,8 @@ for (let algorithmValue of algorithms) {
for (let input of [placeholder, Buffer.from(placeholder)]) {
describe(typeof input === "string" ? "string" : "buffer", () => {
test("password sync", () => {
// The polyfills skip this test because it hangs the test runner for some reason, but it works in normal usage
test.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("password sync", () => {
const hashed = hashSync(input);
expect(hashed).toStartWith(prefix);
expect(verifySync(input, hashed)).toBeTrue();
@@ -245,7 +246,7 @@ for (let algorithmValue of algorithms) {
async function runSlowBCryptTest() {
const algorithm = { algorithm: "bcrypt", cost: 4 } as const;
const hashed = await password.hash(input, algorithm);
const prefix = "$" + "2b";
const prefix = "$" + (process.env.BUN_POLYFILLS_TEST_RUNNER ? "2a" : "2b");
expect(hashed).toStartWith(prefix);
expect(await password.verify(input, hashed, "bcrypt")).toBeTrue();
expect(() => password.verify(hashed, input, "bcrypt")).toThrow();

View File

@@ -1,7 +1,7 @@
import { peek } from "bun";
import { expect, test } from "bun:test";
test("peek", () => {
test.skipIf(!!process.env.BUN_POLYFILLS_TEST_RUNNER)("peek", () => {
const promise = Promise.resolve(true);
// no await necessary!

View File

@@ -5,7 +5,7 @@ import { bunEnv, bunExe } from "harness";
test("reportError", () => {
const cwd = import.meta.dir;
const { stderr } = spawnSync({
cmd: [bunExe(), new URL("./reportError.ts", import.meta.url).pathname],
cmd: [bunExe(), ...process.execArgv, new URL("./reportError.ts", import.meta.url).pathname],
cwd,
env: {
...bunEnv,

View File

@@ -1,7 +1,7 @@
import { test, expect } from "bun:test";
import { which } from "bun";
import { mkdtempSync, rmSync, chmodSync, mkdirSync, unlinkSync, realpathSync } from "node:fs";
import { writeFileSync, rmSync, chmodSync, mkdirSync, unlinkSync, realpathSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
@@ -76,13 +76,12 @@ test("which", () => {
});
function writeFixture(path: string) {
var fs = require("fs");
try {
fs.unlinkSync(path);
unlinkSync(path);
} catch (e) {}
var script_name = path;
var script_content = "echo Hello world!";
fs.writeFileSync(script_name, script_content);
fs.chmodSync(script_name, "755");
writeFileSync(script_name, script_content);
chmodSync(script_name, "755");
}