diff --git a/lib/internal/shims/process.js b/lib/internal/shims/process.js
new file mode 100644
index 000000000..48ae9b2cf
--- /dev/null
+++ b/lib/internal/shims/process.js
@@ -0,0 +1,27 @@
+/* wraps the internal process module, circumventing issues with some polyfills (see #539) */
+
+/** @type {import('node:process')} */
+const process = ((base, esmKey, keys, isValid) => {
+ // check if top-level es module, in which case it may have a default export
+ if (esmKey in base && base[esmKey] === true) {
+ let candidate
+ for (const key of keys) {
+ if (!(key in base)) {
+ continue
+ }
+ candidate = base[key]
+ // sanity check
+ if (isValid(candidate)) {
+ return candidate
+ }
+ }
+ }
+ return base
+})(
+ require('process/'),
+ '__esModule',
+ ['default', 'process'],
+ (candidate) => 'nextTick' in candidate
+)
+
+module.exports = process
diff --git a/lib/internal/streams/destroy.js b/lib/internal/streams/destroy.js
index 38292315e..fe9651841 100644
--- a/lib/internal/streams/destroy.js
+++ b/lib/internal/streams/destroy.js
@@ -2,7 +2,7 @@
/* replacement start */
-const process = require('process/')
+const process = require('../shims/process')
/* replacement end */
diff --git a/lib/internal/streams/duplexify.js b/lib/internal/streams/duplexify.js
index 05740d70f..bb1e251a0 100644
--- a/lib/internal/streams/duplexify.js
+++ b/lib/internal/streams/duplexify.js
@@ -1,6 +1,6 @@
/* replacement start */
-const process = require('process/')
+const process = require('../shims/process')
/* replacement end */
diff --git a/lib/internal/streams/end-of-stream.js b/lib/internal/streams/end-of-stream.js
index 29feb936b..b713c2ef1 100644
--- a/lib/internal/streams/end-of-stream.js
+++ b/lib/internal/streams/end-of-stream.js
@@ -1,6 +1,6 @@
/* replacement start */
-const process = require('process/')
+const process = require('../shims/process')
/* replacement end */
// Ported from https://github.com/mafintosh/end-of-stream with
diff --git a/lib/internal/streams/from.js b/lib/internal/streams/from.js
index c7e753140..5cd814260 100644
--- a/lib/internal/streams/from.js
+++ b/lib/internal/streams/from.js
@@ -2,7 +2,7 @@
/* replacement start */
-const process = require('process/')
+const process = require('../shims/process')
/* replacement end */
diff --git a/lib/internal/streams/pipeline.js b/lib/internal/streams/pipeline.js
index a2bab8800..cafe9aad3 100644
--- a/lib/internal/streams/pipeline.js
+++ b/lib/internal/streams/pipeline.js
@@ -1,6 +1,6 @@
/* replacement start */
-const process = require('process/')
+const process = require('../shims/process')
/* replacement end */
// Ported from https://github.com/mafintosh/pump with
diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js
index 5b494ba3d..b4e391a12 100644
--- a/lib/internal/streams/readable.js
+++ b/lib/internal/streams/readable.js
@@ -1,6 +1,6 @@
/* replacement start */
-const process = require('process/')
+const process = require('../shims/process')
/* replacement end */
// Copyright Joyent, Inc. and other Node contributors.
diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js
index 8a2800346..1a730b40e 100644
--- a/lib/internal/streams/writable.js
+++ b/lib/internal/streams/writable.js
@@ -1,6 +1,6 @@
/* replacement start */
-const process = require('process/')
+const process = require('../shims/process')
/* replacement end */
// Copyright Joyent, Inc. and other Node contributors.
diff --git a/package.json b/package.json
index c3d6f8a40..9d16dadbf 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"test:prepare": "node test/browser/runner-prepare.mjs",
"test:browsers": "node test/browser/runner-browser.mjs",
"test:bundlers": "node test/browser/runner-node.mjs",
+ "test:vite": "node test/browser/runner-vite.mjs",
"test:readable-stream-only": "node readable-stream-test/runner-prepare.mjs",
"coverage": "c8 -c ./c8.json tap --rcfile=./tap.yml test/parallel/test-*.js test/ours/test-*.js",
"format": "prettier -w src lib test",
@@ -77,6 +78,9 @@
"tape": "^5.5.3",
"tar": "^6.1.11",
"undici": "^5.1.1",
+ "vite": "^5.4.9",
+ "vite-plugin-commonjs": "^0.10.3",
+ "vite-plugin-node-polyfills": "^0.22.0",
"webpack": "^5.72.1",
"webpack-cli": "^4.9.2"
},
diff --git a/test/browser/fixtures/vite/index.html b/test/browser/fixtures/vite/index.html
new file mode 100644
index 000000000..2cb9b0b1d
--- /dev/null
+++ b/test/browser/fixtures/vite/index.html
@@ -0,0 +1,10 @@
+
+
+
+ readable-stream test:vite
+
+
+ Testing...
+
+
+
diff --git a/test/browser/fixtures/vite/index.mjs b/test/browser/fixtures/vite/index.mjs
new file mode 100644
index 000000000..328a6945a
--- /dev/null
+++ b/test/browser/fixtures/vite/index.mjs
@@ -0,0 +1,56 @@
+import { reportSuccess, reportError, reportLog } from './lib/reporting.mjs'
+import { createBankStream } from './lib/bank.mjs'
+import { createCollectorStream } from './lib/collector.mjs'
+
+const DATA_SIZE = 512
+
+async function test() {
+ await reportLog('Generating test data...')
+
+ const expected = new Uint8Array(DATA_SIZE)
+ window.crypto.getRandomValues(expected)
+
+ await reportLog('Creating input stream...')
+ const readable = createBankStream(expected)
+
+ await reportLog('Creating output stream...')
+ const writable = createCollectorStream()
+
+ await reportLog('Piping...')
+ await new Promise((resolve, reject) => {
+ readable.pipe(writable)
+ .on('finish', resolve)
+ .on('error', reject)
+ })
+
+ await reportLog('Comparing...')
+ const retrieved = writable.collect()
+ if (retrieved.length !== DATA_SIZE) {
+ throw new Error(`Expected output data of length ${DATA_SIZE}, got ${retrieved.length}`)
+ }
+
+ let nMatch = 0
+ let firstNonMatch = -1
+ for (let i = 0; i < DATA_SIZE; i++) {
+ if (expected[i] === retrieved[i]) {
+ nMatch++
+ } else if (firstNonMatch === -1) {
+ firstNonMatch = i
+ }
+ }
+
+ if (firstNonMatch === -1) {
+ await reportLog('100% match!')
+ } else {
+ await reportLog(`expected: ${expected}`)
+ await reportLog(`actual: ${retrieved}`)
+
+ const percent = (nMatch / DATA_SIZE) * 100
+ throw new Error(`${percent.toFixed(2)}% match (first mismatch at position ${firstNonMatch})`)
+ }
+}
+
+(async () => {
+ await test()
+ await reportSuccess()
+})().catch(reportError)
diff --git a/test/browser/fixtures/vite/lib/bank.mjs b/test/browser/fixtures/vite/lib/bank.mjs
new file mode 100644
index 000000000..539004caf
--- /dev/null
+++ b/test/browser/fixtures/vite/lib/bank.mjs
@@ -0,0 +1,24 @@
+import { Readable } from '@me'
+
+/**
+ * Returns a Readable stream that reports the content of a buffer
+ * @param buffer {Uint8Array}
+ * @return {import('stream').Readable}
+ */
+export function createBankStream(buffer) {
+ let bytesRead = 0
+ const readable = new Readable()
+ readable._read = function (count) {
+ let end = bytesRead + count
+ let done = false
+ if (end >= buffer.byteLength) {
+ end = buffer.byteLength
+ done = true
+ }
+ readable.push(buffer.subarray(bytesRead, bytesRead = end))
+ if (done) {
+ readable.push(null)
+ }
+ }
+ return readable
+}
diff --git a/test/browser/fixtures/vite/lib/collector.mjs b/test/browser/fixtures/vite/lib/collector.mjs
new file mode 100644
index 000000000..dbd3e60e7
--- /dev/null
+++ b/test/browser/fixtures/vite/lib/collector.mjs
@@ -0,0 +1,51 @@
+import { Writable } from '@me'
+
+const LOAD_FACTOR = 0.75
+const INITIAL_CAPACITY = 16
+
+/**
+ * Returns a Writable stream that stores chunks to an internal buffer, retrievable with #collect().
+ * @returns {import("stream").Writable & { collect(): Uint8Array }}
+ */
+export function createCollectorStream() {
+ let capacity = INITIAL_CAPACITY
+ let buffer = new Uint8Array(capacity)
+ let size = 0
+
+ // ensures that "buffer" can hold n additional bytes
+ function provision(n) {
+ const requiredSize = size + n
+ if (requiredSize <= capacity) {
+ return
+ }
+ const newCapacity = Math.ceil((requiredSize + 1) / LOAD_FACTOR)
+ let arrayBuffer = buffer.buffer
+ if ('transfer' in arrayBuffer) {
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer
+ arrayBuffer = arrayBuffer.transfer(newCapacity)
+ buffer = new Uint8Array(arrayBuffer)
+ } else {
+ const copy = new Uint8Array(newCapacity)
+ copy.set(buffer, 0)
+ buffer = copy
+ }
+ capacity = newCapacity
+ }
+
+ const writable = new Writable()
+ writable._write = function (chunk, _, cb) {
+ if (!(chunk instanceof Uint8Array)) {
+ throw new Error('Unexpected chunk')
+ }
+ provision(chunk.byteLength)
+ buffer.set(chunk, size)
+ size += chunk.byteLength
+ cb(null)
+ }
+
+ return Object.assign(writable, {
+ collect: function () {
+ return buffer.subarray(0, size)
+ }
+ })
+}
diff --git a/test/browser/fixtures/vite/lib/reporting.mjs b/test/browser/fixtures/vite/lib/reporting.mjs
new file mode 100644
index 000000000..bcbdd5ff6
--- /dev/null
+++ b/test/browser/fixtures/vite/lib/reporting.mjs
@@ -0,0 +1,52 @@
+const ENDPOINT_STATUS = '/status'
+const ENDPOINT_LOG = '/log'
+
+function closeCurrentWindow() {
+ window.close()
+}
+
+async function reportRaw(endpoint, data, tryBeacon) {
+ data = JSON.stringify(data)
+ if (tryBeacon && typeof navigator === 'object' && 'sendBeacon' in navigator) {
+ data = (new TextEncoder()).encode(data)
+ data = data.buffer
+ navigator.sendBeacon(endpoint, data)
+ } else {
+ await fetch(endpoint, {
+ method: 'POST',
+ body: data,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ }
+}
+
+export async function reportSuccess() {
+ await reportRaw(ENDPOINT_STATUS, { success: true }, true)
+ closeCurrentWindow()
+}
+
+export async function reportError(error) {
+ let msg = 'Unknown error'
+ if (typeof error === 'string') {
+ msg = error
+ } else if (typeof error === 'object') {
+ if (error instanceof Error) {
+ msg = error.message
+ } else if (error !== null) {
+ msg = `${error}`
+ }
+ } else {
+ msg = `${error}`
+ }
+ await reportRaw(ENDPOINT_STATUS, { success: false, error: msg }, true)
+ closeCurrentWindow()
+}
+
+/**
+ * @param message {string}
+ */
+export async function reportLog(message) {
+ await reportRaw(ENDPOINT_LOG, { message }, false)
+}
diff --git a/test/browser/fixtures/vite/vite.config.mjs b/test/browser/fixtures/vite/vite.config.mjs
new file mode 100644
index 000000000..41b3f44e0
--- /dev/null
+++ b/test/browser/fixtures/vite/vite.config.mjs
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import { nodePolyfills } from 'vite-plugin-node-polyfills'
+import commonjs from 'vite-plugin-commonjs'
+
+// noinspection JSUnusedGlobalSymbols
+export default defineConfig({
+ mode: 'development',
+ build: {
+ sourcemap: true
+ },
+ plugins: [
+ commonjs(),
+ nodePolyfills()
+ ]
+})
diff --git a/test/browser/runner-vite.mjs b/test/browser/runner-vite.mjs
new file mode 100644
index 000000000..acfa1607e
--- /dev/null
+++ b/test/browser/runner-vite.mjs
@@ -0,0 +1,328 @@
+import { resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { createServer } from 'vite'
+import * as stream from 'node:stream'
+import * as fs from 'node:fs/promises'
+
+const TIMEOUT_MS = 10 * 1e3
+
+// Enum for success state
+/** @type {{ [p: number]: string, IDLE: 0, SUCCESS: 1, ERROR: 2 }} */
+const State = (() => {
+ const _s = {}
+ _s[_s[0] = 'IDLE'] = 0
+ _s[_s[1] = 'SUCCESS'] = 1
+ _s[_s[2] = 'ERROR'] = 2
+ return _s
+})()
+
+// Utility to read a http IncomingMessage into a buffer
+/**
+ * @param msg {import("http").IncomingMessage}
+ * @return {Promise}
+ */
+async function readMessage(msg) {
+ const chunks = []
+ const writable = new stream.Writable({
+ write: function(chunk, encoding, next) {
+ if (chunk === null) {
+ return
+ }
+ if (!(chunk instanceof Uint8Array)) {
+ throw new Error('Chunk is not Uint8Array')
+ }
+ chunks.push(chunk)
+ next()
+ }
+ })
+ const promise = new Promise((resolve, reject) => {
+ writable.on('finish', resolve)
+ writable.on('error', reject)
+ })
+ msg.pipe(writable)
+ await promise
+ return Buffer.concat(chunks)
+}
+
+// Create a middleware that registers an endpoint on /status, so that the runner can receive the status.
+// Also registers /log to log debug messages.
+function createMiddleware() {
+ const stateChangeCallbacks = []
+ let error = null
+
+ /**
+ * @param state {0 | 1 | 2}
+ */
+ function setState(state) {
+ for (const cb of stateChangeCallbacks.splice(0)) {
+ cb(state)
+ }
+ }
+
+ /**
+ * @param timeout {number}
+ * @return {Promise<0 | 1 | 2>}
+ */
+ function awaitStateChange(timeout) {
+ let timeoutHandle = -1
+ return Promise.race([
+ new Promise((resolve, reject) => {
+ timeoutHandle = setTimeout(() => {
+ reject(new Error('Timed out (' + timeout + 'ms)'))
+ }, timeout)
+ }),
+ new Promise((resolve) => {
+ clearTimeout(timeoutHandle)
+ stateChangeCallbacks.push(resolve)
+ })
+ ])
+ }
+
+ /**
+ * @return {Error | null}
+ */
+ function getError() {
+ return error
+ }
+
+ /**
+ * @param req {import("http").IncomingMessage}
+ */
+ async function handleAsync(req) {
+ let action = -1
+ switch (req.url) {
+ case '/status':
+ action = 0
+ break
+ case '/log':
+ action = 1
+ break
+ default:
+ return
+ }
+ if (action === -1 || req.method !== 'POST') {
+ return
+ }
+
+ // Read the JSON body
+ /** @type { { success?: boolean, error?: string, message?: string } } */
+ const body = JSON.parse((new TextDecoder()).decode(await readMessage(req)))
+
+ if (action) {
+ // log
+ if (!('message' in body)) {
+ throw new Error('Log data missing required property: message')
+ }
+ console.log(`[browser] ${body.message}`)
+ } else {
+ // status
+ if (!('success' in body)) {
+ throw new Error('Status data missing required property: success')
+ }
+ if (body.success) {
+ setState(State.SUCCESS)
+ } else {
+ if ('error' in body) {
+ error = new Error(`${body.error}`)
+ }
+ setState(State.ERROR)
+ }
+ }
+ }
+
+ /**
+ * @param req {import("http").IncomingMessage}
+ * @param res {import("http").ServerResponse}
+ * @param next {() => void}
+ */
+ function handle(req, res, next) {
+ handleAsync(req).then(() => {
+ next()
+ }).catch((err) => {
+ console.error(err)
+ process.exit(1)
+ })
+ }
+
+ return Object.assign(handle, {
+ awaitStateChange,
+ getError
+ })
+}
+
+/**
+ * If the named directory exists, clears out that directory.
+ * If the path exists as a non-directory, throws an error.
+ * If the path does not exist, creates a new directory.
+ * @param dir {string}
+ * @returns {Promise}
+ */
+async function ensureEmptyDir(dir) {
+ let conflict = false
+ let clear = false
+ try {
+ const stats = await fs.stat(dir)
+ conflict = !stats.isDirectory()
+ clear = true
+ } catch (ignored) { }
+ if (conflict) {
+ throw new Error(`Creating directory at ${dir} would conflict with existing filesystem entity`)
+ }
+ if (clear) {
+ for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
+ if (ent.isDirectory()) {
+ await fs.rm(resolve(dir, ent.name), { recursive: true, force: true })
+ } else {
+ await fs.rm(resolve(dir, ent.name), { force: true })
+ }
+ }
+ } else {
+ await fs.mkdir(dir, { recursive: true })
+ }
+}
+
+/**
+ * Copies the content of one directory to another, recursively. The destination directory
+ * is created with semantics defined by {@link ensureEmptyDir}.
+ * @param from {string}
+ * @param to {string}
+ * @returns {Promise}
+ */
+async function recursiveCopyDir(from, to) {
+ await ensureEmptyDir(to)
+
+ for (const ent of await fs.readdir(from, { withFileTypes: true })) {
+ if (ent.isDirectory()) {
+ await recursiveCopyDir(
+ resolve(from, ent.name),
+ resolve(to, ent.name)
+ )
+ } else if (ent.isFile()) {
+ await fs.cp(
+ resolve(from, ent.name),
+ resolve(to, ent.name)
+ )
+ }
+ }
+}
+
+/**
+ * Creates a copy of the project directory within node_modules, with an arbitrary name returned by this method.
+ * The goal is to force Vite into treating this project as external to the Vite test source.
+ * TL;DR, makes "@me" work in the Vite test source.
+ * @param moduleRoot {string}
+ * @returns {Promise}
+ */
+async function createMirror(moduleRoot) {
+ const name = 'vitest-self-hack'
+ const link = resolve(moduleRoot, `node_modules/${name}`)
+
+ await ensureEmptyDir(link)
+
+ // Symlink everything except package.json & lib
+ for (const ent of await fs.readdir(moduleRoot, { withFileTypes: true })) {
+ let type
+ if (ent.isDirectory()) {
+ if (ent.name === 'lib') {
+ continue
+ }
+ type = 'dir'
+ } else if (ent.isFile()) {
+ if (ent.name === 'package.json') {
+ continue
+ }
+ type = 'file'
+ } else {
+ continue
+ }
+ await fs.symlink(
+ resolve(moduleRoot, ent.name),
+ resolve(link, ent.name),
+ type
+ )
+ }
+
+ // Copy lib
+ await recursiveCopyDir(
+ resolve(moduleRoot, 'lib'),
+ resolve(link, 'lib')
+ )
+
+ // Copy package.json (with our fake name)
+ const pkgJson = JSON.parse(await fs.readFile(resolve(moduleRoot, 'package.json'), { encoding: 'utf-8' }))
+ pkgJson.name = name
+ await fs.writeFile(
+ resolve(link, 'package.json'),
+ (new TextEncoder()).encode(JSON.stringify(pkgJson, null, 4)),
+ { flag: 'w' }
+ )
+ return name
+}
+
+async function main() {
+ // Execute the test
+ const __dirname = fileURLToPath(new URL('.', import.meta.url))
+ const moduleRoot = resolve(__dirname, '../..')
+ const viteRoot = resolve(__dirname, './fixtures/vite')
+ const start = performance.now()
+
+ console.log('Creating module mirror...')
+ const moduleName = await createMirror(moduleRoot)
+
+ const middleware = createMiddleware()
+ const server = await createServer({
+ configFile: resolve(viteRoot, 'vite.config.mjs'),
+ root: viteRoot,
+ plugins: [
+ {
+ name: 'Status Broker',
+ configureServer: async (server) => {
+ console.log('Binding status middleware')
+ server.middlewares.use(middleware)
+ }
+ }
+ ],
+ server: {
+ port: 1337
+ },
+ resolve: {
+ alias: {
+ '@me': moduleName
+ }
+ }
+ })
+
+ await server.listen()
+ console.log('Vite server started')
+ server.printUrls()
+
+ try {
+ /** @type {Promise<0 | 1 | 2>} */
+ const statePromise = middleware.awaitStateChange(TIMEOUT_MS)
+
+ console.log('Opening in browser and awaiting test results...')
+ server.openBrowser()
+
+ const state = await statePromise
+ const elapsed = performance.now() - start
+ const elapsedStr = elapsed.toFixed(2) + ' ms'
+ switch (state) {
+ case State.SUCCESS:
+ console.log('Test succeeded in ' + elapsedStr)
+ break
+ case State.ERROR:
+ console.log('Test failed in ' + elapsedStr)
+ throw middleware.getError()
+ default:
+ throw new Error('Assertion failed: state is out of bounds (' + state + ' / ' + State[state] + ')')
+ }
+ } finally {
+ await server.close()
+ console.log('Vite server stopped')
+ }
+}
+
+main().catch((e) => {
+ console.error(e)
+ process.exit(1)
+})