Skip to content

Commit f489cb3

Browse files
authored
Add browser entrypoint (#124)
1 parent 1febc95 commit f489cb3

File tree

10 files changed

+100
-23
lines changed

10 files changed

+100
-23
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"type": "module",
1414
"exports": {
1515
"types": "./source/index.d.ts",
16+
"browser": "./source/exports.js",
1617
"default": "./source/index.js"
1718
},
1819
"engines": {

readme.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
## Features
66

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

147+
## Browser support
148+
149+
For this module to work in browsers, a bundler must be used that either:
150+
- Supports the [`exports.browser`](https://nodejs.org/api/packages.html#community-conditions-definitions) field in `package.json`
151+
- Strips or ignores `node:*` imports
152+
153+
Most bundlers (such as [Webpack](https://webpack.js.org/guides/package-exports/#target-environment)) support either of these.
154+
155+
Additionally, browsers support [web streams](#web-streams) and [async iterables](#async-iterables), but not [Node.js streams](#nodejs-streams).
156+
147157
## Tips
148158

149159
### Alternatives

source/exports.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {getStreamAsArray} from './array.js';
2+
export {getStreamAsArrayBuffer} from './array-buffer.js';
3+
export {getStreamAsBuffer} from './buffer.js';
4+
export {getStreamAsString as default} from './string.js';
5+
export {MaxBufferError} from './contents.js';

source/index.js

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
export {getStreamAsArray} from './array.js';
2-
export {getStreamAsArrayBuffer} from './array-buffer.js';
3-
export {getStreamAsBuffer} from './buffer.js';
4-
export {getStreamAsString as default} from './string.js';
5-
export {MaxBufferError} from './contents.js';
1+
import {on} from 'node:events';
2+
import {finished} from 'node:stream/promises';
3+
import {nodeImports} from './stream.js';
4+
5+
Object.assign(nodeImports, {on, finished});
6+
7+
export {
8+
default,
9+
getStreamAsArray,
10+
getStreamAsArrayBuffer,
11+
getStreamAsBuffer,
12+
MaxBufferError,
13+
} from './exports.js';

source/stream.js

+6-17
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {isReadableStream} from 'is-stream';
22
import {ponyfill} from './web-stream.js';
33

44
export const getAsyncIterable = stream => {
5-
if (isReadableStream(stream, {checkOpen: false})) {
5+
if (isReadableStream(stream, {checkOpen: false}) && nodeImports.on !== undefined) {
66
return getStreamIterable(stream);
77
}
88

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

2323
// The default iterable for Node.js streams does not allow for multiple readers at once, so we re-implement it
2424
const getStreamIterable = async function * (stream) {
25-
if (nodeImports === undefined) {
26-
await loadNodeImports();
27-
}
28-
2925
const controller = new AbortController();
3026
const state = {};
3127
handleStreamEnd(stream, controller, state);
3228

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

5248
const handleStreamEnd = async (stream, controller, state) => {
5349
try {
54-
await nodeImports.streamPromises.finished(stream, {cleanup: true, readable: true, writable: false, error: false});
50+
await nodeImports.finished(stream, {cleanup: true, readable: true, writable: false, error: false});
5551
} catch (error) {
5652
state.error = error;
5753
} finally {
5854
controller.abort();
5955
}
6056
};
6157

62-
// Use dynamic imports to support browsers
63-
const loadNodeImports = async () => {
64-
const [events, streamPromises] = await Promise.all([
65-
import('node:events'),
66-
import('node:stream/promises'),
67-
]);
68-
nodeImports = {events, streamPromises};
69-
};
70-
71-
let nodeImports;
58+
// Loaded by the Node entrypoint, but not by the browser one.
59+
// This prevents using dynamic imports.
60+
export const nodeImports = {};

test/browser.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {execFile} from 'node:child_process';
2+
import {dirname} from 'node:path';
3+
import {fileURLToPath} from 'node:url';
4+
import {promisify} from 'node:util';
5+
import test from 'ava';
6+
import {fixtureString} from './fixtures/index.js';
7+
8+
const pExecFile = promisify(execFile);
9+
const cwd = dirname(fileURLToPath(import.meta.url));
10+
const nodeStreamFixture = './fixtures/node-stream.js';
11+
const webStreamFixture = './fixtures/web-stream.js';
12+
const iterableFixture = './fixtures/iterable.js';
13+
const nodeConditions = [];
14+
const browserConditions = ['--conditions=browser'];
15+
16+
const testEntrypoint = async (t, fixture, conditions, expectedOutput = fixtureString) => {
17+
const {stdout, stderr} = await pExecFile('node', [...conditions, fixture], {cwd});
18+
t.is(stderr, '');
19+
t.is(stdout, expectedOutput);
20+
};
21+
22+
test('Node entrypoint works with Node streams', testEntrypoint, nodeStreamFixture, nodeConditions, `${fixtureString}${fixtureString}`);
23+
test('Browser entrypoint works with Node streams', testEntrypoint, nodeStreamFixture, browserConditions);
24+
test('Node entrypoint works with web streams', testEntrypoint, webStreamFixture, nodeConditions);
25+
test('Browser entrypoint works with web streams', testEntrypoint, webStreamFixture, browserConditions);
26+
test('Node entrypoint works with async iterables', testEntrypoint, iterableFixture, nodeConditions);
27+
test('Browser entrypoint works with async iterables', testEntrypoint, iterableFixture, browserConditions);

test/fixtures/iterable.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import process from 'node:process';
2+
import getStream from 'get-stream';
3+
import {createStream} from '../helpers/index.js';
4+
import {fixtureString} from './index.js';
5+
6+
const generator = async function * () {
7+
yield fixtureString;
8+
};
9+
10+
const stream = createStream(generator);
11+
process.stdout.write(await getStream(stream));

test/fixtures/node-stream.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import process from 'node:process';
2+
import getStream from 'get-stream';
3+
import {createStream} from '../helpers/index.js';
4+
import {fixtureString} from './index.js';
5+
6+
const stream = createStream([fixtureString]);
7+
const [output, secondOutput] = await Promise.all([getStream(stream), getStream(stream)]);
8+
process.stdout.write(`${output}${secondOutput}`);

test/fixtures/web-stream.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import process from 'node:process';
2+
import getStream from 'get-stream';
3+
import {readableStreamFrom} from '../helpers/index.js';
4+
import {fixtureString} from './index.js';
5+
6+
const stream = readableStreamFrom([fixtureString]);
7+
process.stdout.write(await getStream(stream));

test/stream.js

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ const assertReadFail = assertStream.bind(undefined, {writableEnded: true});
2727
const assertWriteFail = assertStream.bind(undefined, {readableEnded: true});
2828
const assertBothFail = assertStream.bind(undefined, {});
2929

30+
test('Can emit "error" event right after getStream()', async t => {
31+
const stream = Readable.from([fixtureString]);
32+
t.is(stream.listenerCount('error'), 0);
33+
const promise = getStream(stream);
34+
t.is(stream.listenerCount('error'), 1);
35+
36+
const error = new Error('test');
37+
stream.emit('error', error);
38+
t.is(await t.throwsAsync(promise), error);
39+
});
40+
3041
const testSuccess = async (t, StreamClass) => {
3142
const stream = StreamClass.from(fixtureMultiString);
3243
t.true(stream instanceof StreamClass);

0 commit comments

Comments
 (0)