Skip to content

Commit dbe3363

Browse files
authored
[Fizz] Implement Legacy renderToString and renderToNodeStream on top of Fizz (#21276)
* Wire up DOM legacy build * Hack to filter extra comments for testing purposes * Use string concat in renderToString I think this might be faster. We could probably use a combination of this technique in the stream too to lower the overhead. * Error if we can't complete the root synchronously Maybe this should always error but in the async forms we can just delay the stream until it resolves so it does have some useful semantics. In the synchronous form it's never useful though. I'm mostly adding the error because we're testing this behavior for renderToString specifically. * Gate memory leak tests of internals These tests don't translate as is to the new implementation and have been ported to the Fizz tests separately. * Enable Fizz legacy mode in stable * Add wrapper around the ServerFormatConfig for legacy mode This ensures that we can inject custom overrides without negatively affecting the new implementation. This adds another field for static mark up for example. * Wrap pushTextInstance to avoid emitting comments for text in static markup * Don't emit static mark up for completed suspense boundaries Completed and client rendered boundaries are only marked for the client to take over. Pending boundaries are still supported in case you stream non-hydratable mark up. * Wire up generateStaticMarkup to static API entry points * Mark as renderer for stable This shouldn't affect the FB one ideally but it's done with the same build so let's hope this works.
1 parent 101ea9f commit dbe3363

25 files changed

+680
-40
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
11+
export * from 'react-client/src/ReactFlightClientHostConfigStream';
12+
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export {
11+
renderToString,
12+
renderToStaticMarkup,
13+
renderToNodeStream,
14+
renderToStaticNodeStream,
15+
version,
16+
} from './src/server/ReactDOMServerBrowser';

packages/react-dom/server.browser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ export {
1313
renderToNodeStream,
1414
renderToStaticNodeStream,
1515
version,
16-
} from './src/server/ReactDOMServerBrowser';
16+
} from './src/server/ReactDOMLegacyServerBrowser';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export {
11+
renderToString,
12+
renderToStaticMarkup,
13+
renderToNodeStream,
14+
renderToStaticNodeStream,
15+
version,
16+
} from './src/server/ReactDOMServerBrowser';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// For some reason Flow doesn't like export * in this file. I don't know why.
11+
export {
12+
renderToString,
13+
renderToStaticMarkup,
14+
renderToNodeStream,
15+
renderToStaticNodeStream,
16+
version,
17+
} from './src/server/ReactDOMServerNode';

packages/react-dom/server.node.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ export {
1414
renderToNodeStream,
1515
renderToStaticNodeStream,
1616
version,
17-
} from './src/server/ReactDOMServerNode';
17+
} from './src/server/ReactDOMLegacyServerNode';

packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ describe('ReactDOMServerIntegration', () => {
487487
});
488488

489489
// Regression test for https://github.com/facebook/react/issues/14705
490+
// @gate !experimental && www
490491
it('does not pollute later renders when stream destroyed', () => {
491492
const LoggedInUser = React.createContext('default');
492493

@@ -529,6 +530,7 @@ describe('ReactDOMServerIntegration', () => {
529530
});
530531

531532
// Regression test for https://github.com/facebook/react/issues/14705
533+
// @gate !experimental && www
532534
it('frees context value reference when stream destroyed', () => {
533535
const LoggedInUser = React.createContext('default');
534536

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import ReactVersion from 'shared/ReactVersion';
11+
import invariant from 'shared/invariant';
12+
13+
import type {ReactNodeList} from 'shared/ReactTypes';
14+
15+
import {
16+
createRequest,
17+
startWork,
18+
startFlowing,
19+
abort,
20+
} from 'react-server/src/ReactFizzServer';
21+
22+
import {
23+
createResponseState,
24+
createRootFormatContext,
25+
} from './ReactDOMServerLegacyFormatConfig';
26+
27+
type ServerOptions = {
28+
identifierPrefix?: string,
29+
};
30+
31+
function onError() {
32+
// Non-fatal errors are ignored.
33+
}
34+
35+
function renderToStringImpl(
36+
children: ReactNodeList,
37+
options: void | ServerOptions,
38+
generateStaticMarkup: boolean,
39+
): string {
40+
let didFatal = false;
41+
let fatalError = null;
42+
let result = '';
43+
const destination = {
44+
push(chunk) {
45+
if (chunk !== null) {
46+
result += chunk;
47+
}
48+
return true;
49+
},
50+
destroy(error) {
51+
didFatal = true;
52+
fatalError = error;
53+
},
54+
};
55+
56+
let readyToStream = false;
57+
function onReadyToStream() {
58+
readyToStream = true;
59+
}
60+
const request = createRequest(
61+
children,
62+
destination,
63+
createResponseState(
64+
generateStaticMarkup,
65+
options ? options.identifierPrefix : undefined,
66+
),
67+
createRootFormatContext(undefined),
68+
Infinity,
69+
onError,
70+
undefined,
71+
onReadyToStream,
72+
);
73+
startWork(request);
74+
// If anything suspended and is still pending, we'll abort it before writing.
75+
// That way we write only client-rendered boundaries from the start.
76+
abort(request);
77+
startFlowing(request);
78+
if (didFatal) {
79+
throw fatalError;
80+
}
81+
invariant(
82+
readyToStream,
83+
'A React component suspended while rendering, but no fallback UI was specified.\n' +
84+
'\n' +
85+
'Add a <Suspense fallback=...> component higher in the tree to ' +
86+
'provide a loading indicator or placeholder to display.',
87+
);
88+
return result;
89+
}
90+
91+
function renderToString(
92+
children: ReactNodeList,
93+
options?: ServerOptions,
94+
): string {
95+
return renderToStringImpl(children, options, false);
96+
}
97+
98+
function renderToStaticMarkup(
99+
children: ReactNodeList,
100+
options?: ServerOptions,
101+
): string {
102+
return renderToStringImpl(children, options, true);
103+
}
104+
105+
function renderToNodeStream() {
106+
invariant(
107+
false,
108+
'ReactDOMServer.renderToNodeStream(): The streaming API is not available ' +
109+
'in the browser. Use ReactDOMServer.renderToString() instead.',
110+
);
111+
}
112+
113+
function renderToStaticNodeStream() {
114+
invariant(
115+
false,
116+
'ReactDOMServer.renderToStaticNodeStream(): The streaming API is not available ' +
117+
'in the browser. Use ReactDOMServer.renderToStaticMarkup() instead.',
118+
);
119+
}
120+
121+
export {
122+
renderToString,
123+
renderToStaticMarkup,
124+
renderToNodeStream,
125+
renderToStaticNodeStream,
126+
ReactVersion as version,
127+
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ReactNodeList} from 'shared/ReactTypes';
11+
12+
import type {Request} from 'react-server/src/ReactFizzServer';
13+
14+
import {
15+
createRequest,
16+
startWork,
17+
startFlowing,
18+
abort,
19+
} from 'react-server/src/ReactFizzServer';
20+
21+
import {
22+
createResponseState,
23+
createRootFormatContext,
24+
} from './ReactDOMServerLegacyFormatConfig';
25+
26+
import {
27+
version,
28+
renderToString,
29+
renderToStaticMarkup,
30+
} from './ReactDOMLegacyServerBrowser';
31+
32+
import {Readable} from 'stream';
33+
34+
type ServerOptions = {
35+
identifierPrefix?: string,
36+
};
37+
38+
class ReactMarkupReadableStream extends Readable {
39+
request: Request;
40+
startedFlowing: boolean;
41+
constructor() {
42+
// Calls the stream.Readable(options) constructor. Consider exposing built-in
43+
// features like highWaterMark in the future.
44+
super({});
45+
this.request = (null: any);
46+
this.startedFlowing = false;
47+
}
48+
49+
_destroy(err, callback) {
50+
abort(this.request);
51+
// $FlowFixMe: The type definition for the callback should allow undefined and null.
52+
callback(err);
53+
}
54+
55+
_read(size) {
56+
if (this.startedFlowing) {
57+
startFlowing(this.request);
58+
}
59+
}
60+
}
61+
62+
function onError() {
63+
// Non-fatal errors are ignored.
64+
}
65+
66+
function renderToNodeStreamImpl(
67+
children: ReactNodeList,
68+
options: void | ServerOptions,
69+
generateStaticMarkup: boolean,
70+
): Readable {
71+
function onCompleteAll() {
72+
// We wait until everything has loaded before starting to write.
73+
// That way we only end up with fully resolved HTML even if we suspend.
74+
destination.startedFlowing = true;
75+
startFlowing(request);
76+
}
77+
const destination = new ReactMarkupReadableStream();
78+
const request = createRequest(
79+
children,
80+
destination,
81+
createResponseState(false, options ? options.identifierPrefix : undefined),
82+
createRootFormatContext(undefined),
83+
Infinity,
84+
onError,
85+
onCompleteAll,
86+
undefined,
87+
);
88+
destination.request = request;
89+
startWork(request);
90+
return destination;
91+
}
92+
93+
function renderToNodeStream(
94+
children: ReactNodeList,
95+
options?: ServerOptions,
96+
): Readable {
97+
return renderToNodeStreamImpl(children, options, false);
98+
}
99+
100+
function renderToStaticNodeStream(
101+
children: ReactNodeList,
102+
options?: ServerOptions,
103+
): Readable {
104+
return renderToNodeStreamImpl(children, options, true);
105+
}
106+
107+
export {
108+
renderToString,
109+
renderToStaticMarkup,
110+
renderToNodeStream,
111+
renderToStaticNodeStream,
112+
version,
113+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export type Destination = {
11+
push(chunk: string | null): boolean,
12+
destroy(error: Error): mixed,
13+
...
14+
};
15+
16+
export type PrecomputedChunk = string;
17+
export type Chunk = string;
18+
19+
export function scheduleWork(callback: () => void) {
20+
callback();
21+
}
22+
23+
export function flushBuffered(destination: Destination) {}
24+
25+
export function beginWriting(destination: Destination) {}
26+
27+
let prevWasCommentSegmenter = false;
28+
export function writeChunk(
29+
destination: Destination,
30+
chunk: Chunk | PrecomputedChunk,
31+
): boolean {
32+
if (prevWasCommentSegmenter) {
33+
prevWasCommentSegmenter = false;
34+
if (chunk[0] !== '<') {
35+
destination.push('<!-- -->');
36+
}
37+
}
38+
if (chunk === '<!-- -->') {
39+
prevWasCommentSegmenter = true;
40+
return true;
41+
}
42+
return destination.push(chunk);
43+
}
44+
45+
export function completeWriting(destination: Destination) {}
46+
47+
export function close(destination: Destination) {
48+
destination.push(null);
49+
}
50+
51+
export function stringToChunk(content: string): Chunk {
52+
return content;
53+
}
54+
55+
export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
56+
return content;
57+
}
58+
59+
export function closeWithError(destination: Destination, error: mixed): void {
60+
// $FlowFixMe: This is an Error object or the destination accepts other types.
61+
destination.destroy(error);
62+
}

0 commit comments

Comments
 (0)