Skip to content

Commit

Permalink
Add browser entrypoint (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Mar 15, 2024
1 parent 1febc95 commit f489cb3
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 23 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"type": "module",
"exports": {
"types": "./source/index.d.ts",
"browser": "./source/exports.js",
"default": "./source/index.js"
},
"engines": {
Expand Down
12 changes: 11 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
## Features

- Works in any JavaScript environment ([Node.js](#nodejs-streams), [browsers](#web-streams), etc.).
- Works in any JavaScript environment ([Node.js](#nodejs-streams), [browsers](#browser-support), etc.).
- Supports [text streams](#getstreamstream-options), [binary streams](#getstreamasbufferstream-options) and [object streams](#getstreamasarraystream-options).
- Supports [async iterables](#async-iterables).
- Can set a [maximum stream size](#maxbuffer).
Expand Down Expand Up @@ -144,6 +144,16 @@ try {
}
```

## Browser support

For this module to work in browsers, a bundler must be used that either:
- Supports the [`exports.browser`](https://nodejs.org/api/packages.html#community-conditions-definitions) field in `package.json`
- Strips or ignores `node:*` imports

Most bundlers (such as [Webpack](https://webpack.js.org/guides/package-exports/#target-environment)) support either of these.

Additionally, browsers support [web streams](#web-streams) and [async iterables](#async-iterables), but not [Node.js streams](#nodejs-streams).

## Tips

### Alternatives
Expand Down
5 changes: 5 additions & 0 deletions source/exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {getStreamAsArray} from './array.js';
export {getStreamAsArrayBuffer} from './array-buffer.js';
export {getStreamAsBuffer} from './buffer.js';
export {getStreamAsString as default} from './string.js';
export {MaxBufferError} from './contents.js';
18 changes: 13 additions & 5 deletions source/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export {getStreamAsArray} from './array.js';
export {getStreamAsArrayBuffer} from './array-buffer.js';
export {getStreamAsBuffer} from './buffer.js';
export {getStreamAsString as default} from './string.js';
export {MaxBufferError} from './contents.js';
import {on} from 'node:events';
import {finished} from 'node:stream/promises';
import {nodeImports} from './stream.js';

Object.assign(nodeImports, {on, finished});

export {
default,
getStreamAsArray,
getStreamAsArrayBuffer,
getStreamAsBuffer,
MaxBufferError,
} from './exports.js';
23 changes: 6 additions & 17 deletions source/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {isReadableStream} from 'is-stream';
import {ponyfill} from './web-stream.js';

export const getAsyncIterable = stream => {
if (isReadableStream(stream, {checkOpen: false})) {
if (isReadableStream(stream, {checkOpen: false}) && nodeImports.on !== undefined) {
return getStreamIterable(stream);
}

Expand All @@ -22,16 +22,12 @@ const {toString} = Object.prototype;

// The default iterable for Node.js streams does not allow for multiple readers at once, so we re-implement it
const getStreamIterable = async function * (stream) {
if (nodeImports === undefined) {
await loadNodeImports();
}

const controller = new AbortController();
const state = {};
handleStreamEnd(stream, controller, state);

try {
for await (const [chunk] of nodeImports.events.on(stream, 'data', {signal: controller.signal})) {
for await (const [chunk] of nodeImports.on(stream, 'data', {signal: controller.signal})) {
yield chunk;
}
} catch (error) {
Expand All @@ -51,21 +47,14 @@ const getStreamIterable = async function * (stream) {

const handleStreamEnd = async (stream, controller, state) => {
try {
await nodeImports.streamPromises.finished(stream, {cleanup: true, readable: true, writable: false, error: false});
await nodeImports.finished(stream, {cleanup: true, readable: true, writable: false, error: false});
} catch (error) {
state.error = error;
} finally {
controller.abort();
}
};

// Use dynamic imports to support browsers
const loadNodeImports = async () => {
const [events, streamPromises] = await Promise.all([
import('node:events'),
import('node:stream/promises'),
]);
nodeImports = {events, streamPromises};
};

let nodeImports;
// Loaded by the Node entrypoint, but not by the browser one.
// This prevents using dynamic imports.
export const nodeImports = {};
27 changes: 27 additions & 0 deletions test/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {execFile} from 'node:child_process';
import {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import {promisify} from 'node:util';
import test from 'ava';
import {fixtureString} from './fixtures/index.js';

const pExecFile = promisify(execFile);
const cwd = dirname(fileURLToPath(import.meta.url));
const nodeStreamFixture = './fixtures/node-stream.js';
const webStreamFixture = './fixtures/web-stream.js';
const iterableFixture = './fixtures/iterable.js';
const nodeConditions = [];
const browserConditions = ['--conditions=browser'];

const testEntrypoint = async (t, fixture, conditions, expectedOutput = fixtureString) => {
const {stdout, stderr} = await pExecFile('node', [...conditions, fixture], {cwd});
t.is(stderr, '');
t.is(stdout, expectedOutput);
};

test('Node entrypoint works with Node streams', testEntrypoint, nodeStreamFixture, nodeConditions, `${fixtureString}${fixtureString}`);
test('Browser entrypoint works with Node streams', testEntrypoint, nodeStreamFixture, browserConditions);
test('Node entrypoint works with web streams', testEntrypoint, webStreamFixture, nodeConditions);
test('Browser entrypoint works with web streams', testEntrypoint, webStreamFixture, browserConditions);
test('Node entrypoint works with async iterables', testEntrypoint, iterableFixture, nodeConditions);
test('Browser entrypoint works with async iterables', testEntrypoint, iterableFixture, browserConditions);
11 changes: 11 additions & 0 deletions test/fixtures/iterable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import process from 'node:process';
import getStream from 'get-stream';
import {createStream} from '../helpers/index.js';
import {fixtureString} from './index.js';

const generator = async function * () {
yield fixtureString;
};

const stream = createStream(generator);
process.stdout.write(await getStream(stream));
8 changes: 8 additions & 0 deletions test/fixtures/node-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import process from 'node:process';
import getStream from 'get-stream';
import {createStream} from '../helpers/index.js';
import {fixtureString} from './index.js';

const stream = createStream([fixtureString]);
const [output, secondOutput] = await Promise.all([getStream(stream), getStream(stream)]);
process.stdout.write(`${output}${secondOutput}`);
7 changes: 7 additions & 0 deletions test/fixtures/web-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import process from 'node:process';
import getStream from 'get-stream';
import {readableStreamFrom} from '../helpers/index.js';
import {fixtureString} from './index.js';

const stream = readableStreamFrom([fixtureString]);
process.stdout.write(await getStream(stream));
11 changes: 11 additions & 0 deletions test/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ const assertReadFail = assertStream.bind(undefined, {writableEnded: true});
const assertWriteFail = assertStream.bind(undefined, {readableEnded: true});
const assertBothFail = assertStream.bind(undefined, {});

test('Can emit "error" event right after getStream()', async t => {
const stream = Readable.from([fixtureString]);
t.is(stream.listenerCount('error'), 0);
const promise = getStream(stream);
t.is(stream.listenerCount('error'), 1);

const error = new Error('test');
stream.emit('error', error);
t.is(await t.throwsAsync(promise), error);
});

const testSuccess = async (t, StreamClass) => {
const stream = StreamClass.from(fixtureMultiString);
t.true(stream instanceof StreamClass);
Expand Down

0 comments on commit f489cb3

Please sign in to comment.