diff --git a/lib/cmd/vite/plugins/browser-compat.js b/lib/cmd/vite/plugins/browser-compat.js index 175b905..f185ba1 100644 --- a/lib/cmd/vite/plugins/browser-compat.js +++ b/lib/cmd/vite/plugins/browser-compat.js @@ -223,17 +223,45 @@ var _util = { inherits, promisify, deprecate, inspect, isString, isArray, isObje export default _util; `; +// Browser stub for Node's `buffer`. +// +// Real Buffer work only happens server-side; the browser code paths that reach +// these APIs are guarded by isNode()/process.browser and never actually run. +// Two non-obvious constraints shape this stub — both learned from real crashes +// in the kiln-edit bundle: +// +// 1. `Buffer` must be a FUNCTION with a real `.prototype`, never a plain +// object. Libraries such as safe-buffer subclass it via +// `SafeBuffer.prototype = Object.create(Buffer.prototype)`. A plain-object +// Buffer has an `undefined` prototype, so that line throws +// "Object prototype may only be an Object or null: undefined" at module +// evaluation — taking down the whole kiln-edit bundle before Kiln boots. +// +// 2. It must expose from/alloc/allocUnsafe/allocUnsafeSlow. safe-buffer +// feature-detects all four; when they are present it simply re-exports this +// stub (`module.exports = buffer`) and never reaches the Object.create +// subclassing branch above. Omitting any one sends it down that branch. +// +// A real global Buffer polyfill (globalThis.Buffer) is preferred when the page +// provides one; otherwise we fall back to the function-shaped stub below. const BUFFER_STUB = ` -var _Buffer = typeof globalThis !== 'undefined' && globalThis.Buffer - ? globalThis.Buffer - : { - isBuffer: function() { return false; }, - from: function(data) { return typeof data === 'string' ? { toString: function() { return data; }, length: data.length } : []; }, - alloc: function(size) { return new Uint8Array(size); }, - concat: function(list) { return list.reduce(function(a, b) { return Array.from(a).concat(Array.from(b)); }, []); }, - }; +function _clayBuffer(arg, encodingOrOffset, length) { + return _clayBuffer.from(arg, encodingOrOffset, length); +} +_clayBuffer.isBuffer = function() { return false; }; +_clayBuffer.from = function(data) { + return typeof data === 'string' + ? { toString: function() { return data; }, length: data.length } + : (data && typeof data.length === 'number' ? Array.prototype.slice.call(data) : []); +}; +_clayBuffer.alloc = function(size) { return new Uint8Array(size > 0 ? size : 0); }; +_clayBuffer.allocUnsafe = function(size) { return new Uint8Array(size > 0 ? size : 0); }; +_clayBuffer.allocUnsafeSlow = function(size) { return new Uint8Array(size > 0 ? size : 0); }; +_clayBuffer.concat = function(list) { return (list || []).reduce(function(a, b) { return Array.from(a).concat(Array.from(b)); }, []); }; +var _Buffer = (typeof globalThis !== 'undefined' && globalThis.Buffer) ? globalThis.Buffer : _clayBuffer; export var Buffer = _Buffer; -export default { Buffer: _Buffer }; +export function SlowBuffer(size) { return _Buffer.alloc(size > 0 ? size : 0); } +export default { Buffer: _Buffer, SlowBuffer: SlowBuffer }; `; // node-fetch v1/v2 have no browser field; stub to native fetch so server-only diff --git a/lib/cmd/vite/plugins/browser-compat.test.js b/lib/cmd/vite/plugins/browser-compat.test.js index c78b1f8..78fd33b 100644 --- a/lib/cmd/vite/plugins/browser-compat.test.js +++ b/lib/cmd/vite/plugins/browser-compat.test.js @@ -26,6 +26,35 @@ function runPlugin(id, customStubs) { return { resolvedId: resolved, code: plugin.load(resolved) }; } +/** + * Execute one of the hand-written ESM stub strings in a sandbox and return its + * exports. The `globalThis` seen by the stub is fully controllable so tests can + * exercise the browser path (no real global Buffer) rather than Node's built-in. + * + * Only the minimal export forms used by the stubs in this file are supported: + * `export default X`, `export var NAME = X`, and `export function NAME(...)`. + * + * @param {string} code - the ESM stub source returned by plugin.load() + * @param {Object} globalStub - object substituted for `globalThis` inside the stub + * @returns {Object} the collected exports (default under `.default`) + */ +function evalEsmStub(code, globalStub) { + const registrations = []; + const body = code + .replace(/export default /g, '__exports.default = ') + .replace(/export function (\w+)/g, (_m, name) => { registrations.push(name); return `function ${name}`; }) + .replace(/export var (\w+) =/g, (_m, name) => { registrations.push(name); return `var ${name} =`; }) + + '\n' + registrations.map(n => `__exports.${n} = ${n};`).join('\n'); + + const __exports = {}; + // eslint-disable-next-line no-new-func + const fn = new Function('__exports', 'globalThis', body); + + fn(__exports, globalStub); + + return __exports; +} + // ── resolveId ───────────────────────────────────────────────────────────────── describe('viteBrowserCompatPlugin', () => { @@ -127,6 +156,44 @@ describe('viteBrowserCompatPlugin', () => { expect(code).toContain('export var Buffer'); }); + // safe-buffer feature-detects from/alloc/allocUnsafe/allocUnsafeSlow and only + // re-exports the module (instead of subclassing Buffer) when all four exist. + // Missing any one drops it into the Object.create(Buffer.prototype) branch. + it('buffer stub exposes the full allocation API safe-buffer feature-detects', () => { + const { code } = runPlugin('buffer'); + + expect(code).toContain('from'); + expect(code).toContain('alloc'); + expect(code).toContain('allocUnsafe'); + expect(code).toContain('allocUnsafeSlow'); + }); + + // Regression guard for the kiln-edit crash: with no global Buffer polyfill the + // fallback Buffer must still be a function with a real prototype, so a library + // doing `SafeBuffer.prototype = Object.create(Buffer.prototype)` does not throw + // "Object prototype may only be an Object or null: undefined" at module eval. + it('buffer fallback Buffer is subclass-safe when no global Buffer exists', () => { + const { code } = runPlugin('buffer'); + const exports = evalEsmStub(code, {}); // browser path: globalThis has no Buffer + const Buffer = exports.default.Buffer; + + expect(typeof Buffer).toBe('function'); + expect(typeof Buffer.prototype).toBe('object'); + expect(() => Object.create(Buffer.prototype)).not.toThrow(); + // Mirror safe-buffer's line-12 check — all four must be truthy. + expect(Boolean(Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow)).toBe(true); + }); + + it('buffer stub prefers a real global Buffer polyfill when present', () => { + const { code } = runPlugin('buffer'); + const realish = function Buffer() {}; + + realish.from = realish.alloc = realish.allocUnsafe = realish.allocUnsafeSlow = () => {}; + const exports = evalEsmStub(code, { Buffer: realish }); + + expect(exports.default.Buffer).toBe(realish); + }); + it('http stub exports request and get', () => { const { code } = runPlugin('http');