diff --git a/bunfig.toml b/bunfig.toml index 4d352159a6f3..ca054299db8a 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -40,6 +40,6 @@ ignorePatterns = [ ] [lint] -# `bun lint` lints src/js. +# `bun lint` lints src/js and the browser polyfills in src/node-fallbacks. config = "oxlint.json" -ignorePatterns = ["/*", "!/src", "/src/*", "!/src/js"] +ignorePatterns = ["/*", "!/src", "/src/*", "!/src/js", "!/src/node-fallbacks"] diff --git a/oxlint.json b/oxlint.json index c091afcb94c8..71721df8cd93 100644 --- a/oxlint.json +++ b/oxlint.json @@ -118,6 +118,25 @@ "rules": { "bun/no-unused-expressions": "off" } + }, + { + // Browser polyfills that `bun build --target=browser` inlines into the + // user's bundle. They are plain JS, not JSC builtins: a `$intrinsic`, + // a node global such as `process` or `Buffer`, or any other undeclared + // name is a ReferenceError in the browser. Read `globalThis.process`. + // This sees the hand-written sources only. The npm packages are in the + // built files: browser/NodePolyfills lints a bundle of all of them. + "files": ["src/node-fallbacks/*.js"], + "env": { "browser": true, "es2024": true }, + "rules": { + "no-undef": "error", + "no-unused-vars": "off", + "no-useless-rename": "off" + } + }, + { + "files": ["src/node-fallbacks/assert.js", "src/node-fallbacks/events.js", "src/node-fallbacks/stream.js"], + "env": { "commonjs": true } } ] } diff --git a/src/node-fallbacks/README.md b/src/node-fallbacks/README.md index d2c492a04aa5..39dec901d708 100644 --- a/src/node-fallbacks/README.md +++ b/src/node-fallbacks/README.md @@ -4,6 +4,10 @@ When using `bun build --target=browser`, if you attempt to import a Node.js modu For example, if you import `zlib`, the `node-fallbacks/zlib.js` file will be loaded. +## How they are built + +`build-fallbacks.ts` bundles each file for the browser. A file that assigns `module.exports` is built as CommonJS, so that `require()` of it returns that value. Any other file is an ES module, and if it has no default export, the build adds one that has every named export. The npm packages read `process` and `Buffer` as globals, which a browser does not have: the build binds `Buffer` to the sibling polyfill, and `process` to the host's when there is a Node.js-like one and to the sibling polyfill when there is none. + ## Not used by Bun's runtime These files are _not_ used by Bun's runtime. They are only used for the `bun build --target=browser` command. diff --git a/src/node-fallbacks/assert.js b/src/node-fallbacks/assert.js index 0181e54cea6c..4ab3d45cdd9b 100644 --- a/src/node-fallbacks/assert.js +++ b/src/node-fallbacks/assert.js @@ -1,25 +1,2 @@ -import * as assert from "./node_modules/assert"; - -export const { - AssertionError, - CallTracker, - deepEqual, - deepStrictEqual, - doesNotMatch, - doesNotReject, - doesNotThrow, - equal, - fail, - ifError, - match, - notDeepEqual, - notDeepStrictEqual, - notEqual, - notStrictEqual, - ok, - rejects, - strict, - strictEqual, - throws, -} = assert; -export default assert; +// CommonJS so that `require("assert")` returns the callable function. +module.exports = require("./node_modules/assert"); diff --git a/src/node-fallbacks/buffer.js b/src/node-fallbacks/buffer.js index ff4cf88000f6..6f4dd47c7be0 100644 --- a/src/node-fallbacks/buffer.js +++ b/src/node-fallbacks/buffer.js @@ -24,8 +24,10 @@ export const INSPECT_MAX_BYTES = 50; export const kMaxLength = 0x7fffffff; export const kStringMaxLength = 0x1fffffe8; -export const btoa = /* @__PURE__ */ globalThis.btoa; -export const atob = /* @__PURE__ */ globalThis.atob; +// A browser throws "Illegal invocation" when these are called as a method, so the exports do not pass `this`. +const bufferBtoa = data => globalThis.btoa(data); +const bufferAtob = data => globalThis.atob(data); +export { bufferAtob as atob, bufferBtoa as btoa }; export const File = /* @__PURE__ */ globalThis.File; export const Blob = /* @__PURE__ */ globalThis.Blob; diff --git a/src/node-fallbacks/build-fallbacks.ts b/src/node-fallbacks/build-fallbacks.ts index f13bc1c992cf..9ac0fbd98359 100644 --- a/src/node-fallbacks/build-fallbacks.ts +++ b/src/node-fallbacks/build-fallbacks.ts @@ -30,9 +30,6 @@ const firstCopyOf = new Map(); scan(fs.realpathSync("node_modules").replaceAll("\\", "/")); } -// Packages of these polyfills read the Buffer global, which a browser does not have. They get the sibling polyfill's. -const readsBufferGlobal = new Set(["crypto"]); - function browserMap(file: string) { for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) { const manifest = path.join(dir, "package.json"); @@ -70,28 +67,64 @@ const resolveForPolyfill = (polyfill: string): Bun.BunPlugin => ({ const requireShim = /var __require=\(\(x\)=>typeof require<"u"\?require:.*?is not supported'\)\}\);/; +const hostProcess = 'globalThis.process?.versions?.node&&typeof globalThis.process.nextTick=="function"'; + +// Free `process` / `Buffer` in the npm packages: rename, then bind with the line for the output format. +const injectedGlobals = [ + { + defines: ["process", "global.process"], + id: "__bun_process", + // A Node.js-like host keeps its own process. A page, also one with a partial `process` shim, gets the polyfill. + esm: `import __bun_process_polyfill from "process";var __bun_process=${hostProcess}?globalThis.process:__bun_process_polyfill;`, + cjs: `var __bun_process=${hostProcess}?globalThis.process:(m=>m.default||m)(require("process"));`, + }, + { + defines: ["Buffer"], + id: "__bun_Buffer", + esm: 'import { Buffer as __bun_Buffer } from "buffer";', + cjs: 'var __bun_Buffer = require("buffer").Buffer;', + }, +]; + +// Node.js globals that a browser does not have and that no polyfill binds. A polyfill that reads one fails the build. +const notInABrowser = ["setImmediate", "clearImmediate", "__dirname", "__filename"]; + let commands: Promise[] = []; for (const name of allFiles) { const polyfill = name.slice(0, -".js".length); + + const source = fs.readFileSync(name, "utf8"); + + // A source that sets `module.exports` stays CommonJS, so `require()` of the polyfill returns that value. + const format = /^module\.exports\s*=/m.test(source) ? "cjs" : "esm"; + + // An ES module without a default export gets one that has every named export. + const named = new Bun.Transpiler().scan(source).exports; + const needsDefault = format === "esm" && !named.includes("default"); + commands.push( Bun.build({ entrypoints: [name], + files: needsDefault ? { [path.resolve(name)]: `${source}\nexport default { ${named} };` } : {}, outdir, target: "browser", - format: name.includes("stream") ? "cjs" : "esm", + format, minify: { syntax: true, whitespace: true }, define: { "process.env.NODE_DEBUG": "false", "process.browser": "true", "global": "globalThis", - ...(readsBufferGlobal.has(polyfill) && { "Buffer": "__bun_Buffer" }), + ...Object.fromEntries(injectedGlobals.flatMap(g => g.defines.map(d => [d, g.id]))), + ...Object.fromEntries(notInABrowser.map(global => [global, `__bun_no_${global}`])), }, plugins: [resolveForPolyfill(polyfill)], metafile: true, throw: true, }).then(async ({ metafile }) => { if (!metafile) throw new Error("This bun has no Bun.build({ metafile })"); + const exported = Object.values(metafile.outputs).flatMap(output => output.exports ?? []); + if (format === "esm" && !exported.includes("default")) throw new Error(`${name} has no default export`); const inlined = Object.keys(metafile.inputs).filter(input => input.startsWith("node:")); if (inlined.length > 0) { throw new Error(`${name} bundles ${inlined.join(", ")} from the bun that runs codegen`); @@ -108,8 +141,13 @@ for (const name of allFiles) { throw new Error("Unexpected __require in " + name); } - if (outfile.includes("__bun_Buffer")) { - outfile = `import { Buffer as __bun_Buffer } from "buffer";${outfile}`; + for (const global of notInABrowser) { + if (outfile.includes(`__bun_no_${global}`)) throw new Error(`${name} reads the global \`${global}\``); + } + + for (const { id, esm, cjs } of injectedGlobals) { + if (!outfile.includes(id)) continue; + outfile = (format === "cjs" ? cjs : esm) + outfile; } for (const { path: specifier } of new Bun.Transpiler().scanImports(outfile)) { diff --git a/src/node-fallbacks/console.js b/src/node-fallbacks/console.js index 34cc54b56528..90e1819e997a 100644 --- a/src/node-fallbacks/console.js +++ b/src/node-fallbacks/console.js @@ -1 +1,24 @@ +// Hardcoded module "node:console" +export const { + assert, + clear, + count, + countReset, + debug, + dir, + dirxml, + error, + group, + groupCollapsed, + groupEnd, + info, + log, + table, + time, + timeEnd, + timeLog, + timeStamp, + trace, + warn, +} = console; export default console; diff --git a/src/node-fallbacks/domain.js b/src/node-fallbacks/domain.js index f4e83aa6b44b..b547aa9234c3 100644 --- a/src/node-fallbacks/domain.js +++ b/src/node-fallbacks/domain.js @@ -1,2 +1,3 @@ import domain from "domain-browser"; export var { create, createDomain } = domain; +export default domain; diff --git a/src/node-fallbacks/events.js b/src/node-fallbacks/events.js index 60a19d471083..ede587ec4304 100644 --- a/src/node-fallbacks/events.js +++ b/src/node-fallbacks/events.js @@ -530,15 +530,5 @@ Object.assign(EventEmitter, { listenerCount, }); -export default EventEmitter; -export { - EventEmitter, - addAbortListener, - captureRejectionSymbol, - getEventListeners, - getMaxListeners, - EventEmitter as init, - listenerCount, - once, - setMaxListeners, -}; +// CommonJS so that `require("events")` returns the constructor. +module.exports = EventEmitter; diff --git a/src/node-fallbacks/path.js b/src/node-fallbacks/path.js index bab0d6181698..0229856709bd 100644 --- a/src/node-fallbacks/path.js +++ b/src/node-fallbacks/path.js @@ -116,7 +116,7 @@ export function resolve() { var path; if (i >= 0) path = arguments[i]; else { - if (cwd === undefined) cwd = process.cwd(); + if (cwd === undefined) cwd = globalThis.process?.cwd?.() ?? "/"; path = cwd; } diff --git a/src/node-fallbacks/process.js b/src/node-fallbacks/process.js index 78a0abba4d89..e9a9724d62cb 100644 --- a/src/node-fallbacks/process.js +++ b/src/node-fallbacks/process.js @@ -1,59 +1,51 @@ // shim for using process in browser var queue = []; +var batch = []; +var index = 0; var draining = false; -var currentQueue; -var queueIndex = -1; +var scheduled = false; -function cleanUpNextTick() { - if (!draining || !currentQueue) { - return; - } - draining = false; - if (currentQueue.length) { - queue = currentQueue.concat(queue); - } else { - queueIndex = -1; - } - if (queue.length) { - drainQueue(); - } +// node runs ticks before queued promise jobs. A browser has no hook for that, so the drain is one microtask. +function scheduleDrain() { + scheduled = true; + queueMicrotask(drainQueue); } function drainQueue() { - if (draining) { - return; - } - var timeout = setTimeout(cleanUpNextTick, 0); + scheduled = false; draining = true; - var len = queue.length; - while (len) { - currentQueue = queue; - queue = []; - while (++queueIndex < len) { - if (currentQueue) { - var item = currentQueue[queueIndex]; - item.fun.apply(null, item.array); + try { + // Ticks queued mid-drain run in the same pass, like node. A fresh array per batch lets finished ticks be freed. + for (;;) { + while (index < batch.length) { + var item = batch[index++]; + item.fun.apply(undefined, item.args); } + if (!queue.length) break; + batch = queue; + queue = []; + index = 0; } - queueIndex = -1; - len = queue.length; + } finally { + // On a throw, `index` is past the callback that threw. The rest of the batch runs first in the next drain. + if (index >= batch.length) { + batch = []; + index = 0; + } + draining = false; + if (batch.length || queue.length) scheduleDrain(); } - currentQueue = null; - draining = false; - clearTimeout(timeout, 0); } -export function nextTick(fun) { - var args = new Array(arguments.length - 1); - if (arguments.length > 1) { - for (var i = 1; i < arguments.length; i++) { - args[i - 1] = arguments[i]; - } +export function nextTick(fun, ...args) { + if (typeof fun !== "function") { + var received = fun === null ? "null" : typeof fun; + var err = new TypeError('The "callback" argument must be of type function. Received ' + received); + err.code = "ERR_INVALID_ARG_TYPE"; + throw err; } queue.push({ fun, args }); - if (queue.length === 1 && !draining) { - setTimeout(drainQueue, 0); - } + if (!draining && !scheduled) scheduleDrain(); } export const title = "browser"; diff --git a/src/node-fallbacks/querystring.js b/src/node-fallbacks/querystring.js index a5b7d3e42aab..53178cee4d9c 100644 --- a/src/node-fallbacks/querystring.js +++ b/src/node-fallbacks/querystring.js @@ -1,3 +1,6 @@ import querystring_module from "querystring-es3"; -export var { unescapeBuffer, unescape, escape, stringify, encode, parse, decode } = querystring_module; +// In a classic script a top-level `var escape` replaces the page's own, so these two have other local names. +const { escape: querystringEscape, unescape: querystringUnescape } = querystring_module; +export var { unescapeBuffer, stringify, encode, parse, decode } = querystring_module; export * as default from "querystring-es3"; +export { querystringEscape as escape, querystringUnescape as unescape }; diff --git a/src/node-fallbacks/timers.js b/src/node-fallbacks/timers.js index 979b6a460e56..b80e99a88d5c 100644 --- a/src/node-fallbacks/timers.js +++ b/src/node-fallbacks/timers.js @@ -1,9 +1,72 @@ // Hardcoded module "node:timers" -export const setTimeout = globalThis.setTimeout; -export const clearTimeout = globalThis.clearTimeout; -export const setInterval = globalThis.setInterval; -export const setImmediate = globalThis.setImmediate; -export const clearInterval = globalThis.clearInterval; -export const clearImmediate = globalThis.clearImmediate; +import * as promises from "node:timers/promises"; + +// Read once, so that a page which replaces a timer global later does not change this module. +const { + setTimeout: timersHostSetTimeout, + clearTimeout: timersHostClearTimeout, + setInterval: timersHostSetInterval, + clearInterval: timersHostClearInterval, + setImmediate: timersHostSetImmediate, + clearImmediate: timersHostClearImmediate, +} = globalThis; + +// A browser throws "Illegal invocation" when a timer function is called as a method, so none of these pass `this`. +const timersSetTimeout = (...args) => timersHostSetTimeout(...args); +const timersClearTimeout = id => timersHostClearTimeout(id); +const timersSetInterval = (...args) => timersHostSetInterval(...args); +const timersClearInterval = id => timersHostClearInterval(id); +// Browsers have no setImmediate. Fall back to a zero-delay timeout. +const timersSetImmediate = timersHostSetImmediate + ? (...args) => timersHostSetImmediate(...args) + : (callback, ...args) => timersHostSetTimeout(callback, 0, ...args); +const timersClearImmediate = timersHostSetImmediate ? id => timersHostClearImmediate(id) : timersClearTimeout; + +function promiseTimer(start, clear, value, options) { + return new Promise((resolve, reject) => { + var id; + const signal = options?.signal; + const onAbort = () => { + clear(id); + const error = new Error("The operation was aborted", { cause: signal.reason }); + error.name = "AbortError"; + error.code = "ABORT_ERR"; + reject(error); + }; + if (signal?.aborted) return onAbort(); + id = start(() => { + signal?.removeEventListener("abort", onAbort); + resolve(value); + }); + signal?.addEventListener("abort", onAbort, { once: true }); + }); +} + +// `util.promisify(timers.setTimeout)` resolves with `value`, as in node. Without this it would pass the callback last. +const promisifyCustom = Symbol.for("nodejs.util.promisify.custom"); +timersSetTimeout[promisifyCustom] = (delay, value, options) => + promiseTimer(done => timersSetTimeout(done, delay), timersClearTimeout, value, options); +timersSetImmediate[promisifyCustom] = (value, options) => + promiseTimer(timersSetImmediate, timersClearImmediate, value, options); + export const _unrefActive = () => {}; -export * as promises from "node:timers/promises"; +// In a classic script a top-level `var setTimeout` replaces the page's own, so no binding here has a global's name. +export { + timersClearImmediate as clearImmediate, + timersClearInterval as clearInterval, + timersClearTimeout as clearTimeout, + promises, + timersSetImmediate as setImmediate, + timersSetInterval as setInterval, + timersSetTimeout as setTimeout, +}; +export default { + promises, + setTimeout: timersSetTimeout, + clearTimeout: timersClearTimeout, + setInterval: timersSetInterval, + clearInterval: timersClearInterval, + setImmediate: timersSetImmediate, + clearImmediate: timersClearImmediate, + _unrefActive, +}; diff --git a/src/node-fallbacks/util.js b/src/node-fallbacks/util.js index 3be2f8754cc8..2587df070e07 100644 --- a/src/node-fallbacks/util.js +++ b/src/node-fallbacks/util.js @@ -68,51 +68,33 @@ export function format(f, ...args) { // Returns a modified function which warns once by default. // If --no-deprecation is set, then it is a no-op. export function deprecate(fn, msg) { - if (typeof process === "undefined" || process?.noDeprecation === true) { + var proc = globalThis.process; + if (proc == null || proc.noDeprecation === true) { return fn; } var warned = false; function deprecated(...args) { if (!warned) { - if (process.throwDeprecation) { + if (proc.throwDeprecation) { throw new Error(msg); - } else if (process.traceDeprecation) { + } else if (proc.traceDeprecation) { console.trace(msg); } else { console.error(msg); } warned = true; } - return fn.apply(this, ...args); + return fn.apply(this, args); } return deprecated; } -// This function has been edited to be tree-shakable and minifiable -export const debuglog = /* @__PURE__ */ ((debugs = {}, debugEnvRegex = {}, debugEnv) => ( - ((debugEnv = typeof process !== "undefined" && process.env.NODE_DEBUG) && - (debugEnv = debugEnv - .replace(/[|\\{}()[\]^$+?.]/g, "\\$&") - .replace(/\*/g, ".*") - .replace(/,/g, "$|^") - .toUpperCase()), - (debugEnvRegex = new RegExp("^" + debugEnv + "$", "i"))), - set => { - set = set.toUpperCase(); - if (!debugs[set]) { - if (debugEnvRegex.test(set)) { - debugs[set] = function (...args) { - console.error("%s: %s", set, pid, format.apply(null, ...args)); - }; - } else { - debugs[set] = function () {}; - } - } - return debugs[set]; - } -))(); +// A browser build has no NODE_DEBUG, so no section prints. +export function debuglog(set) { + return function () {}; +} /** * Echos the value of a value. Tries to print the value out @@ -336,7 +318,7 @@ function formatError(value) { function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { var output = []; for (var i = 0, l = value.length; i < l; ++i) { - if (hasOwnProperty(value, String(i))) { + if (hasOwn(value, String(i))) { output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, String(i), true)); } else { output.push(""); @@ -364,7 +346,7 @@ function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { str = ctx.stylize("[Setter]", "special"); } } - if (!hasOwnProperty(visibleKeys, key)) { + if (!hasOwn(visibleKeys, key)) { name = "[" + key + "]"; } if (!str) { @@ -498,9 +480,15 @@ export function isPrimitive(arg) { ); } -// Compatibility with the buffer polyfill: +// Browsers have no Buffer global, so detect the buffer polyfill by its methods. export function isBuffer(arg) { - return arg instanceof Buffer; + return ( + typeof arg === "object" && + arg !== null && + typeof arg.copy === "function" && + typeof arg.fill === "function" && + typeof arg.readUInt8 === "function" + ); } function objectToString(o) { @@ -564,15 +552,17 @@ export function _extend(origin, add) { return origin; } -function hasOwnProperty(obj, prop) { +function hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } -export const promisify = /* @__PURE__ */ (x => ((x.custom = Symbol.for("nodejs.util.promisify.custom")), x))( +const kCustomPromisifiedSymbol = Symbol.for("nodejs.util.promisify.custom"); + +export const promisify = /* @__PURE__ */ (x => ((x.custom = kCustomPromisifiedSymbol), x))( function promisify(original) { if (typeof original !== "function") throw new TypeError('The "original" argument must be of type Function'); - if (kCustomPromisifiedSymbol && original[kCustomPromisifiedSymbol]) { + if (original[kCustomPromisifiedSymbol]) { var fn = original[kCustomPromisifiedSymbol]; if (typeof fn !== "function") { @@ -588,6 +578,14 @@ export const promisify = /* @__PURE__ */ (x => ((x.custom = Symbol.for("nodejs.u return fn; } + // A page's own timers have no `promisify.custom` function. The generic form would pass the callback last. + if (original === globalThis.setTimeout) { + return (delay, value) => new Promise(resolve => original(resolve, delay, value)); + } + if (original === globalThis.setImmediate) { + return value => new Promise(resolve => original(resolve, value)); + } + function fn(...args) { var promiseResolve, promiseReject; var promise = new Promise(function (resolve, reject) { @@ -614,13 +612,12 @@ export const promisify = /* @__PURE__ */ (x => ((x.custom = Symbol.for("nodejs.u Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); - if (kCustomPromisifiedSymbol) - Object.defineProperty(fn, kCustomPromisifiedSymbol, { - value: fn, - enumerable: false, - writable: false, - configurable: true, - }); + Object.defineProperty(fn, kCustomPromisifiedSymbol, { + value: fn, + enumerable: false, + writable: false, + configurable: true, + }); return Object.defineProperties(fn, Object.getOwnPropertyDescriptors(original)); }, ); @@ -653,16 +650,15 @@ export function callbackify(original) { } var self = this; var cb = function (...args) { - return maybeCb.apply(self, ...args); + return maybeCb.apply(self, args); }; - // In true node style we process the callback on `nextTick` with all the - // implications (stack, `uncaughtException`, `async_hooks`) + // Like process.nextTick in Node, a throw from the callback is an uncaught error, not a rejection. original.apply(this, args).then( function (ret) { - process.nextTick(cb.bind(null, null, ret)); + queueMicrotask(cb.bind(null, null, ret)); }, function (rej) { - process.nextTick(callbackifyOnRejected.bind(null, rej, cb)); + queueMicrotask(callbackifyOnRejected.bind(null, rej, cb)); }, ); } @@ -675,13 +671,3 @@ export function callbackify(original) { export const TextEncoder = /* @__PURE__ */ globalThis.TextEncoder; export const TextDecoder = /* @__PURE__ */ globalThis.TextDecoder; -export default { - TextEncoder, - TextDecoder, - promisify, - log, - inherits, - _extend, - callbackifyOnRejected, - callbackify, -}; diff --git a/src/node-fallbacks/zlib.js b/src/node-fallbacks/zlib.js index 6e264b79be6c..d320fe77bb45 100644 --- a/src/node-fallbacks/zlib.js +++ b/src/node-fallbacks/zlib.js @@ -1,2 +1,78 @@ -export * from "browserify-zlib"; -export { default } from "browserify-zlib"; +import zlib from "browserify-zlib"; + +// browserify-zlib has the Z_* values on the module only. Node also has them on `zlib.constants`. +zlib.constants = Object.fromEntries(Object.entries(zlib).filter(([key]) => key.startsWith("Z_"))); + +export const { + constants, + Z_NO_FLUSH, + Z_PARTIAL_FLUSH, + Z_SYNC_FLUSH, + Z_FULL_FLUSH, + Z_FINISH, + Z_BLOCK, + Z_TREES, + Z_OK, + Z_STREAM_END, + Z_NEED_DICT, + Z_ERRNO, + Z_STREAM_ERROR, + Z_DATA_ERROR, + Z_BUF_ERROR, + Z_NO_COMPRESSION, + Z_BEST_SPEED, + Z_BEST_COMPRESSION, + Z_DEFAULT_COMPRESSION, + Z_FILTERED, + Z_HUFFMAN_ONLY, + Z_RLE, + Z_FIXED, + Z_DEFAULT_STRATEGY, + Z_BINARY, + Z_TEXT, + Z_UNKNOWN, + Z_DEFLATED, + Z_MIN_WINDOWBITS, + Z_MAX_WINDOWBITS, + Z_DEFAULT_WINDOWBITS, + Z_MIN_CHUNK, + Z_MAX_CHUNK, + Z_DEFAULT_CHUNK, + Z_MIN_MEMLEVEL, + Z_MAX_MEMLEVEL, + Z_DEFAULT_MEMLEVEL, + Z_MIN_LEVEL, + Z_MAX_LEVEL, + Z_DEFAULT_LEVEL, + Zlib, + codes, + Deflate, + Inflate, + Gzip, + Gunzip, + DeflateRaw, + InflateRaw, + Unzip, + createDeflate, + createInflate, + createDeflateRaw, + createInflateRaw, + createGzip, + createGunzip, + createUnzip, + deflate, + deflateSync, + gzip, + gzipSync, + deflateRaw, + deflateRawSync, + unzip, + unzipSync, + inflate, + inflateSync, + gunzip, + gunzipSync, + inflateRaw, + inflateRawSync, +} = zlib; +export default zlib; diff --git a/test/bake/dev/bundle.test.ts b/test/bake/dev/bundle.test.ts index 6e7e8359aa46..afc9f8151d55 100644 --- a/test/bake/dev/bundle.test.ts +++ b/test/bake/dev/bundle.test.ts @@ -1926,3 +1926,29 @@ devTest("recovers from an unresolved css url() and html script src longer than t expect((await dev.fetch("/")).status).toBe(200); }, }); +// The client gets the browser polyfills. `node:stream` and `node:assert` are CommonJS there, and each asks for its +// sibling `process` polyfill with a `require()` of its own. +devTest("a client module uses the node:stream, node:assert and node:zlib polyfills", { + files: { + "index.html": emptyHtmlFile({ + scripts: ["index.ts"], + }), + "index.ts": ` + import { Readable } from "node:stream"; + import assert from "node:assert"; + import { gzip, gunzipSync } from "node:zlib"; + (async () => { + const chunks = []; + await new Promise(resolve => Readable.from(["a", "b"]).on("data", c => chunks.push(c)).on("end", resolve)); + let code; + try { assert.strictEqual(1, 2); } catch (e) { code = e.code; } + const text = await new Promise(resolve => gzip("hi", (error, data) => resolve(error ?? gunzipSync(data).toString()))); + console.log(["got:" + chunks, code, text].join(" ")); + })(); + `, + }, + async test(dev) { + await using c = await dev.client(); + await c.expectMessage("got:a,b ERR_ASSERTION hi"); + }, +}); diff --git a/test/bundler/bundler_browser.test.ts b/test/bundler/bundler_browser.test.ts index ef7498cd925d..b7298f7db384 100644 --- a/test/bundler/bundler_browser.test.ts +++ b/test/bundler/bundler_browser.test.ts @@ -1,5 +1,8 @@ import assert from "assert"; import { describe, expect } from "bun:test"; +import { readFileSync } from "fs"; +import { bunEnv, bunExe } from "harness"; +import { join } from "path"; import { itBundled } from "./expectBundled"; describe("bundler", () => { @@ -167,6 +170,7 @@ describe("bundler", () => { }, }); // A second run makes bun like a page that is not cross-origin isolated. node:vm is its iframe: another realm. + // A page's timers and base64 functions also reject a receiver that is not the global object. const withoutNodeGlobals = { "/without-node-globals.js": /* js */ ` import vm from "node:vm"; @@ -175,6 +179,13 @@ describe("bundler", () => { for (const name of ["process", "Buffer", "global", "setImmediate", "clearImmediate", "SharedArrayBuffer"]) { delete globalThis[name]; } + for (const name of ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "atob", "btoa"]) { + const host = globalThis[name]; + globalThis[name] = function (...args) { + if (this !== undefined && this !== globalThis) throw new TypeError("Illegal invocation"); + return host(...args); + }; + } const getRandomValues = crypto.getRandomValues.bind(crypto); crypto.getRandomValues = view => { if (isShared(view.buffer)) throw new TypeError("The view must not be shared."); @@ -184,6 +195,25 @@ describe("bundler", () => { await import("./out.js"); `, }; + // A page that loads the bundle with a classic