Skip to content

Commit db7e963

Browse files
committed
Add Web Streams APIs to Node entry points in Server Flight Webpack
1 parent 280ff6f commit db7e963

File tree

8 files changed

+258
-12
lines changed

8 files changed

+258
-12
lines changed

packages/react-server-dom-webpack/npm/server.node.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ if (process.env.NODE_ENV === 'production') {
77
s = require('./cjs/react-server-dom-webpack-server.node.development.js');
88
}
99

10+
exports.renderToReadableStream = s.renderToReadableStream;
1011
exports.renderToPipeableStream = s.renderToPipeableStream;
11-
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
1212
exports.decodeReply = s.decodeReply;
13+
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
14+
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
1315
exports.decodeAction = s.decodeAction;
1416
exports.decodeFormState = s.decodeFormState;
1517
exports.registerServerReference = s.registerServerReference;

packages/react-server-dom-webpack/npm/static.node.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ if (process.env.NODE_ENV === 'production') {
77
s = require('./cjs/react-server-dom-webpack-server.node.development.js');
88
}
99

10+
if (s.unstable_prerender) {
11+
exports.unstable_prerender = s.unstable_prerender;
12+
}
1013
if (s.unstable_prerenderToNodeStream) {
1114
exports.unstable_prerenderToNodeStream = s.unstable_prerenderToNodeStream;
1215
}

packages/react-server-dom-webpack/server.node.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
export {
1111
renderToPipeableStream,
12-
decodeReplyFromBusboy,
12+
renderToReadableStream,
1313
decodeReply,
14+
decodeReplyFromBusboy,
15+
decodeReplyFromAsyncIterable,
1416
decodeAction,
1517
decodeFormState,
1618
registerServerReference,

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @emails react-core
8+
* @jest-environment node
89
*/
910

1011
'use strict';
@@ -92,6 +93,46 @@ describe('ReactFlightDOMNode', () => {
9293
});
9394
}
9495

96+
it('should support web streams in node', async () => {
97+
function Text({children}) {
98+
return <span>{children}</span>;
99+
}
100+
function HTML() {
101+
return (
102+
<div>
103+
<Text>hello</Text>
104+
<Text>world</Text>
105+
</div>
106+
);
107+
}
108+
109+
function App() {
110+
const model = {
111+
html: <HTML />,
112+
};
113+
return model;
114+
}
115+
116+
const readable = await serverAct(() =>
117+
ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
118+
);
119+
const response = ReactServerDOMClient.createFromReadableStream(readable, {
120+
serverConsumerManifest: {
121+
moduleMap: null,
122+
moduleLoading: null,
123+
},
124+
});
125+
const model = await response;
126+
expect(model).toEqual({
127+
html: (
128+
<div>
129+
<span>hello</span>
130+
<span>world</span>
131+
</div>
132+
),
133+
});
134+
});
135+
95136
it('should allow an alternative module mapping to be used for SSR', async () => {
96137
function ClientComponent() {
97138
return <span>Client Component</span>;
@@ -498,8 +539,6 @@ describe('ReactFlightDOMNode', () => {
498539
expect(errors).toEqual([new Error('Connection closed.')]);
499540
// Should still match the result when parsed
500541
const result = await readResult(ssrStream);
501-
const div = document.createElement('div');
502-
div.innerHTML = result;
503-
expect(div.textContent).toBe('loading...');
542+
expect(result).toContain('loading...');
504543
});
505544
});

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js

Lines changed: 195 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import type {Thenable} from 'shared/ReactTypes';
2020

2121
import {Readable} from 'stream';
2222

23+
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
24+
2325
import {
2426
createRequest,
2527
createPrerenderRequest,
@@ -34,6 +36,7 @@ import {
3436
reportGlobalError,
3537
close,
3638
resolveField,
39+
resolveFile,
3740
resolveFileInfo,
3841
resolveFileChunk,
3942
resolveFileComplete,
@@ -128,11 +131,88 @@ function renderToPipeableStream(
128131
};
129132
}
130133

131-
function createFakeWritable(readable: any): Writable {
134+
function createFakeWritableFromReadableStreamController(
135+
controller: ReadableStreamController,
136+
): Writable {
137+
// The current host config expects a Writable so we create
138+
// a fake writable for now to push into the Readable.
139+
return ({
140+
write(chunk: string | Uint8Array) {
141+
controller.enqueue(chunk);
142+
// in web streams there is no backpressure so we can alwas write more
143+
return true;
144+
},
145+
end() {
146+
controller.close();
147+
},
148+
destroy(error) {
149+
// $FlowFixMe[method-unbinding]
150+
if (typeof controller.error === 'function') {
151+
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
152+
controller.error(error);
153+
} else {
154+
controller.close();
155+
}
156+
},
157+
}: any);
158+
}
159+
160+
function renderToReadableStream(
161+
model: ReactClientValue,
162+
webpackMap: ClientManifest,
163+
options?: Options & {
164+
signal?: AbortSignal,
165+
},
166+
): ReadableStream {
167+
const request = createRequest(
168+
model,
169+
webpackMap,
170+
options ? options.onError : undefined,
171+
options ? options.identifierPrefix : undefined,
172+
options ? options.onPostpone : undefined,
173+
options ? options.temporaryReferences : undefined,
174+
__DEV__ && options ? options.environmentName : undefined,
175+
__DEV__ && options ? options.filterStackFrame : undefined,
176+
);
177+
if (options && options.signal) {
178+
const signal = options.signal;
179+
if (signal.aborted) {
180+
abort(request, (signal: any).reason);
181+
} else {
182+
const listener = () => {
183+
abort(request, (signal: any).reason);
184+
signal.removeEventListener('abort', listener);
185+
};
186+
signal.addEventListener('abort', listener);
187+
}
188+
}
189+
let writable: Writable;
190+
const stream = new ReadableStream(
191+
{
192+
type: 'bytes',
193+
start: (controller): ?Promise<void> => {
194+
writable = createFakeWritableFromReadableStreamController(controller);
195+
startWork(request);
196+
},
197+
pull: (controller): ?Promise<void> => {
198+
startFlowing(request, writable);
199+
},
200+
cancel: (reason): ?Promise<void> => {
201+
stopFlowing(request);
202+
abort(request, reason);
203+
},
204+
},
205+
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
206+
{highWaterMark: 0},
207+
);
208+
return stream;
209+
}
210+
211+
function createFakeWritableFromNodeReadable(readable: any): Writable {
132212
// The current host config expects a Writable so we create
133213
// a fake writable for now to push into the Readable.
134214
return ({
135-
write(chunk) {
215+
write(chunk: string | Uint8Array) {
136216
return readable.push(chunk);
137217
},
138218
end() {
@@ -171,7 +251,7 @@ function prerenderToNodeStream(
171251
startFlowing(request, writable);
172252
},
173253
});
174-
const writable = createFakeWritable(readable);
254+
const writable = createFakeWritableFromNodeReadable(readable);
175255
resolve({prelude: readable});
176256
}
177257

@@ -205,6 +285,69 @@ function prerenderToNodeStream(
205285
});
206286
}
207287

288+
function prerender(
289+
model: ReactClientValue,
290+
webpackMap: ClientManifest,
291+
options?: Options & {
292+
signal?: AbortSignal,
293+
},
294+
): Promise<{
295+
prelude: ReadableStream,
296+
}> {
297+
return new Promise((resolve, reject) => {
298+
const onFatalError = reject;
299+
function onAllReady() {
300+
let writable: Writable;
301+
const stream = new ReadableStream(
302+
{
303+
type: 'bytes',
304+
start: (controller): ?Promise<void> => {
305+
writable =
306+
createFakeWritableFromReadableStreamController(controller);
307+
},
308+
pull: (controller): ?Promise<void> => {
309+
startFlowing(request, writable);
310+
},
311+
cancel: (reason): ?Promise<void> => {
312+
stopFlowing(request);
313+
abort(request, reason);
314+
},
315+
},
316+
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
317+
{highWaterMark: 0},
318+
);
319+
resolve({prelude: stream});
320+
}
321+
const request = createPrerenderRequest(
322+
model,
323+
webpackMap,
324+
onAllReady,
325+
onFatalError,
326+
options ? options.onError : undefined,
327+
options ? options.identifierPrefix : undefined,
328+
options ? options.onPostpone : undefined,
329+
options ? options.temporaryReferences : undefined,
330+
__DEV__ && options ? options.environmentName : undefined,
331+
__DEV__ && options ? options.filterStackFrame : undefined,
332+
);
333+
if (options && options.signal) {
334+
const signal = options.signal;
335+
if (signal.aborted) {
336+
const reason = (signal: any).reason;
337+
abort(request, reason);
338+
} else {
339+
const listener = () => {
340+
const reason = (signal: any).reason;
341+
abort(request, reason);
342+
signal.removeEventListener('abort', listener);
343+
};
344+
signal.addEventListener('abort', listener);
345+
}
346+
}
347+
startWork(request);
348+
});
349+
}
350+
208351
function decodeReplyFromBusboy<T>(
209352
busboyStream: Busboy,
210353
webpackMap: ServerManifest,
@@ -286,11 +429,59 @@ function decodeReply<T>(
286429
return root;
287430
}
288431

432+
function decodeReplyFromAsyncIterable<T>(
433+
iterable: AsyncIterable<[string, string | File]>,
434+
webpackMap: ServerManifest,
435+
options?: {temporaryReferences?: TemporaryReferenceSet},
436+
): Thenable<T> {
437+
const iterator: AsyncIterator<[string, string | File]> =
438+
iterable[ASYNC_ITERATOR]();
439+
440+
const response = createResponse(
441+
webpackMap,
442+
'',
443+
options ? options.temporaryReferences : undefined,
444+
);
445+
446+
function progress(
447+
entry:
448+
| {done: false, +value: [string, string | File], ...}
449+
| {done: true, +value: void, ...},
450+
) {
451+
if (entry.done) {
452+
close(response);
453+
} else {
454+
const [name, value] = entry.value;
455+
if (typeof value === 'string') {
456+
resolveField(response, name, value);
457+
} else {
458+
resolveFile(response, name, value);
459+
}
460+
iterator.next().then(progress, error);
461+
}
462+
}
463+
function error(reason: Error) {
464+
reportGlobalError(response, reason);
465+
if (typeof (iterator: any).throw === 'function') {
466+
// The iterator protocol doesn't necessarily include this but a generator do.
467+
// $FlowFixMe should be able to pass mixed
468+
iterator.throw(reason).then(error, error);
469+
}
470+
}
471+
472+
iterator.next().then(progress, error);
473+
474+
return getRoot(response);
475+
}
476+
289477
export {
478+
renderToReadableStream,
290479
renderToPipeableStream,
480+
prerender,
291481
prerenderToNodeStream,
292-
decodeReplyFromBusboy,
293482
decodeReply,
483+
decodeReplyFromBusboy,
484+
decodeReplyFromAsyncIterable,
294485
decodeAction,
295486
decodeFormState,
296487
};

packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
*/
99

1010
export {
11+
renderToReadableStream,
1112
renderToPipeableStream,
13+
prerender as unstable_prerender,
1214
prerenderToNodeStream as unstable_prerenderToNodeStream,
13-
decodeReplyFromBusboy,
1415
decodeReply,
16+
decodeReplyFromBusboy,
17+
decodeReplyFromAsyncIterable,
1518
decodeAction,
1619
decodeFormState,
1720
registerServerReference,

packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
*/
99

1010
export {
11+
renderToReadableStream,
1112
renderToPipeableStream,
13+
prerender as unstable_prerender,
1214
prerenderToNodeStream as unstable_prerenderToNodeStream,
13-
decodeReplyFromBusboy,
1415
decodeReply,
16+
decodeReplyFromBusboy,
17+
decodeReplyFromAsyncIterable,
1518
decodeAction,
1619
decodeFormState,
1720
registerServerReference,

packages/react-server-dom-webpack/static.node.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@
77
* @flow
88
*/
99

10-
export {unstable_prerenderToNodeStream} from './src/server/react-flight-dom-server.node';
10+
export {
11+
unstable_prerender,
12+
unstable_prerenderToNodeStream,
13+
} from './src/server/react-flight-dom-server.node';

0 commit comments

Comments
 (0)