Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions lib/cmd/vite/plugins/browser-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions lib/cmd/vite/plugins/browser-compat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');

Expand Down
Loading