diff --git a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
index 4af71458cb6a7..2805b629d4b48 100644
--- a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+++ b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
@@ -275,7 +275,7 @@ type PreinitOptions = {
crossOrigin?: string,
integrity?: string,
};
-function preinit(href: string, options: PreinitOptions) {
+function preinit(href: string, options: PreinitOptions): void {
if (!currentResources) {
// While we expect that preinit calls are primarily going to be observed
// during render because effects and events don't run on the server it is
@@ -285,7 +285,17 @@ function preinit(href: string, options: PreinitOptions) {
// simply return and do not warn.
return;
}
- const resources = currentResources;
+ preinitImpl(currentResources, href, options);
+}
+
+// On the server, preinit may be called outside of render when sending an
+// external SSR runtime as part of the initial resources payload. Since this
+// is an internal React call, we do not need to use the resources stack.
+export function preinitImpl(
+ resources: Resources,
+ href: string,
+ options: PreinitOptions,
+): void {
if (__DEV__) {
validatePreinitArguments(href, options);
}
diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
index 01f53697e2e5f..63f5b64d93c8c 100644
--- a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
+++ b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
@@ -3,6 +3,7 @@
* clients. Therefore, it should be fast and not have many external dependencies.
* @flow
*/
+/* eslint-disable dot-notation */
// Imports are resolved statically by the closure compiler in release bundles
// and by rollup in jest unit tests
@@ -13,13 +14,94 @@ import {
completeSegment,
} from './fizz-instruction-set/ReactDOMFizzInstructionSet';
-// Intentionally does nothing. Implementation will be added in future PR.
-// eslint-disable-next-line no-unused-vars
-const observer = new MutationObserver(mutations => {
- // These are only called so I can check what the module output looks like. The
- // code is unreachable.
- clientRenderBoundary();
- completeBoundaryWithStyles();
- completeBoundary();
- completeSegment();
-});
+if (!window.$RC) {
+ // TODO: Eventually remove, we currently need to set these globals for
+ // compatibility with ReactDOMFizzInstructionSet
+ window.$RC = completeBoundary;
+ window.$RM = new Map();
+}
+
+if (document.readyState === 'loading') {
+ if (document.body != null) {
+ installFizzInstrObserver(document.body);
+ } else {
+ // body may not exist yet if the fizz runtime is sent in
+ // (e.g. as a preinit resource)
+ const domBodyObserver = new MutationObserver(() => {
+ // We expect the body node to be stable once parsed / created
+ if (document.body) {
+ if (document.readyState === 'loading') {
+ installFizzInstrObserver(document.body);
+ }
+ handleExistingNodes();
+ domBodyObserver.disconnect();
+ }
+ });
+ // documentElement must already exist at this point
+ // $FlowFixMe[incompatible-call]
+ domBodyObserver.observe(document.documentElement, {childList: true});
+ }
+}
+
+handleExistingNodes();
+
+function handleExistingNodes() {
+ const existingNodes = document.getElementsByTagName('template');
+ for (let i = 0; i < existingNodes.length; i++) {
+ handleNode(existingNodes[i]);
+ }
+}
+
+function installFizzInstrObserver(target /*: Node */) {
+ const fizzInstrObserver = new MutationObserver(mutations => {
+ for (let i = 0; i < mutations.length; i++) {
+ const addedNodes = mutations[i].addedNodes;
+ for (let j = 0; j < addedNodes.length; j++) {
+ if (addedNodes.item(j).parentNode) {
+ handleNode(addedNodes.item(j));
+ }
+ }
+ }
+ });
+ // We assume that instruction data nodes are eventually appended to the
+ // body, even if Fizz is streaming to a shell / subtree.
+ fizzInstrObserver.observe(target, {
+ childList: true,
+ });
+ window.addEventListener('DOMContentLoaded', () => {
+ fizzInstrObserver.disconnect();
+ });
+}
+
+function handleNode(node_ /*: Node */) {
+ // $FlowFixMe[incompatible-cast]
+ if (node_.nodeType !== 1 || !(node_ /*: HTMLElement*/).dataset) {
+ return;
+ }
+ // $FlowFixMe[incompatible-cast]
+ const node = (node_ /*: HTMLElement*/);
+ const dataset = node.dataset;
+ if (dataset['rxi'] != null) {
+ clientRenderBoundary(
+ dataset['bid'],
+ dataset['dgst'],
+ dataset['msg'],
+ dataset['stck'],
+ );
+ node.remove();
+ } else if (dataset['rri'] != null) {
+ // Convert styles here, since its type is Array>
+ completeBoundaryWithStyles(
+ dataset['bid'],
+ dataset['sid'],
+ JSON.parse(dataset['sty']),
+ );
+ node.remove();
+ } else if (dataset['rci'] != null) {
+ completeBoundary(dataset['bid'], dataset['sid']);
+ node.remove();
+ } else if (dataset['rsi'] != null) {
+ completeSegment(dataset['sid'], dataset['pid']);
+ node.remove();
+ }
+}
diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
index 91d241e4e7929..c66292d801835 100644
--- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
@@ -63,6 +63,7 @@ import sanitizeURL from '../shared/sanitizeURL';
import isArray from 'shared/isArray';
import {
+ preinitImpl,
prepareToRenderResources,
finishRenderingResources,
resourcesFromElement,
@@ -105,22 +106,33 @@ export function cleanupAfterRender(previousDispatcher: mixed) {
// E.g. this can be used to distinguish legacy renderers from this modern one.
export const isPrimaryRenderer = true;
+export type StreamingFormat = 0 | 1;
+const ScriptStreamingFormat: StreamingFormat = 0;
+const DataStreamingFormat: StreamingFormat = 1;
+
// Per response, global state that is not contextual to the rendering subtree.
export type ResponseState = {
bootstrapChunks: Array,
- startInlineScript: PrecomputedChunk,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
idPrefix: string,
nextSuspenseID: number,
+ streamingFormat: StreamingFormat,
+ // state for script streaming format, unused if using external runtime / data
+ startInlineScript: PrecomputedChunk,
sentCompleteSegmentFunction: boolean,
sentCompleteBoundaryFunction: boolean,
sentClientRenderFunction: boolean,
- sentStyleInsertionFunction: boolean, // We allow the legacy renderer to extend this object.
+ sentStyleInsertionFunction: boolean,
+ // state for data streaming format
+ externalRuntimeConfig: BootstrapScriptDescriptor | null,
+ // We allow the legacy renderer to extend this object.
...
};
+const dataElementQuotedEnd = stringToPrecomputedChunk('">');
+
const startInlineScript = stringToPrecomputedChunk('');
@@ -154,6 +166,8 @@ export type BootstrapScriptDescriptor = {
integrity?: string,
};
// Allows us to keep track of what we've already written so we can refer back to it.
+// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
+// is set, the server will send instructions via data attributes (instead of inline scripts)
export function createResponseState(
identifierPrefix: string | void,
nonce: string | void,
@@ -170,6 +184,8 @@ export function createResponseState(
'');
+const completeSegmentScriptEnd = stringToPrecomputedChunk('")');
+
+const completeSegmentData1 = stringToPrecomputedChunk(
+ '');
+const completeBoundaryScript3a = stringToPrecomputedChunk('",');
+const completeBoundaryScript3b = stringToPrecomputedChunk('"');
+const completeBoundaryScriptEnd = stringToPrecomputedChunk(')');
+
+const completeBoundaryData1 = stringToPrecomputedChunk(
+ '');
const clientRenderErrorScriptArgInterstitial = stringToPrecomputedChunk(',');
+const clientRenderScriptEnd = stringToPrecomputedChunk(')');
+
+const clientRenderData1 = stringToPrecomputedChunk(
+ '
+ return writeChunkAndReturn(destination, clientRenderDataEnd);
}
- return writeChunkAndReturn(destination, clientRenderScript2);
}
const regexForJSStringsInInstructionScripts = /[<\u2028\u2029]/g;
@@ -2598,7 +2738,22 @@ export function writeInitialResources(
destination: Destination,
resources: Resources,
responseState: ResponseState,
+ willFlushAllSegments: boolean,
): boolean {
+ // Write initially discovered resources after the shell completes
+ if (
+ enableFizzExternalRuntime &&
+ !willFlushAllSegments &&
+ responseState.externalRuntimeConfig
+ ) {
+ // If the root segment is incomplete due to suspended tasks
+ // (e.g. willFlushAllSegments = false) and we are using data
+ // streaming format, ensure the external runtime is sent.
+ // (User code could choose to send this even earlier by calling
+ // preinit(...), if they know they will suspend).
+ const {src, integrity} = responseState.externalRuntimeConfig;
+ preinitImpl(resources, src, {as: 'script', integrity});
+ }
function flushLinkResource(resource) {
if (!resource.flushed) {
pushLinkImpl(target, resource.props, responseState);
@@ -2839,7 +2994,10 @@ const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
const arrayInterstitial = stringToPrecomputedChunk(',');
const arrayCloseBracket = stringToPrecomputedChunk(']');
-function writeStyleResourceDependencies(
+// This function writes a 2D array of strings to be embedded in javascript.
+// E.g.
+// [["JS_escaped_string1", "JS_escaped_string2"]]
+function writeStyleResourceDependenciesInJS(
destination: Destination,
boundaryResources: BoundaryResources,
): void {
@@ -2852,12 +3010,12 @@ function writeStyleResourceDependencies(
// should be ready before content is shown on the client
} else if (resource.flushed) {
writeChunk(destination, nextArrayOpenBrackChunk);
- writeStyleResourceDependencyHrefOnly(destination, resource.href);
+ writeStyleResourceDependencyHrefOnlyInJS(destination, resource.href);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
} else {
writeChunk(destination, nextArrayOpenBrackChunk);
- writeStyleResourceDependency(
+ writeStyleResourceDependencyInJS(
destination,
resource.href,
resource.precedence,
@@ -2873,7 +3031,8 @@ function writeStyleResourceDependencies(
writeChunk(destination, arrayCloseBracket);
}
-function writeStyleResourceDependencyHrefOnly(
+/* Helper functions */
+function writeStyleResourceDependencyHrefOnlyInJS(
destination: Destination,
href: string,
) {
@@ -2889,7 +3048,7 @@ function writeStyleResourceDependencyHrefOnly(
);
}
-function writeStyleResourceDependency(
+function writeStyleResourceDependencyInJS(
destination: Destination,
href: string,
precedence: string,
@@ -2936,7 +3095,7 @@ function writeStyleResourceDependency(
);
// eslint-disable-next-line-no-fallthrough
default:
- writeStyleResourceAttribute(destination, propKey, propValue);
+ writeStyleResourceAttributeInJS(destination, propKey, propValue);
break;
}
}
@@ -2944,7 +3103,7 @@ function writeStyleResourceDependency(
return null;
}
-function writeStyleResourceAttribute(
+function writeStyleResourceAttributeInJS(
destination: Destination,
name: string,
value: string | boolean | number | Function | Object, // not null or undefined
@@ -3022,3 +3181,192 @@ function writeStyleResourceAttribute(
stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)),
);
}
+
+// This function writes a 2D array of strings to be embedded in an attribute
+// value and read with JSON.parse in ReactDOMServerExternalRuntime.js
+// E.g.
+// [["JSON_escaped_string1", "JSON_escaped_string2"]]
+function writeStyleResourceDependenciesInAttr(
+ destination: Destination,
+ boundaryResources: BoundaryResources,
+): void {
+ writeChunk(destination, arrayFirstOpenBracket);
+
+ let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
+ boundaryResources.forEach(resource => {
+ if (resource.inShell) {
+ // We can elide this dependency because it was flushed in the shell and
+ // should be ready before content is shown on the client
+ } else if (resource.flushed) {
+ writeChunk(destination, nextArrayOpenBrackChunk);
+ writeStyleResourceDependencyHrefOnlyInAttr(destination, resource.href);
+ writeChunk(destination, arrayCloseBracket);
+ nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
+ } else {
+ writeChunk(destination, nextArrayOpenBrackChunk);
+ writeStyleResourceDependencyInAttr(
+ destination,
+ resource.href,
+ resource.precedence,
+ resource.props,
+ );
+ writeChunk(destination, arrayCloseBracket);
+ nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
+
+ resource.flushed = true;
+ resource.hint.flushed = true;
+ }
+ });
+ writeChunk(destination, arrayCloseBracket);
+}
+
+/* Helper functions */
+function writeStyleResourceDependencyHrefOnlyInAttr(
+ destination: Destination,
+ href: string,
+) {
+ // We should actually enforce this earlier when the resource is created but for
+ // now we make sure we are actually dealing with a string here.
+ if (__DEV__) {
+ checkAttributeStringCoercion(href, 'href');
+ }
+ const coercedHref = '' + (href: any);
+ writeChunk(
+ destination,
+ stringToChunk(escapeTextForBrowser(JSON.stringify(coercedHref))),
+ );
+}
+
+function writeStyleResourceDependencyInAttr(
+ destination: Destination,
+ href: string,
+ precedence: string,
+ props: Object,
+) {
+ if (__DEV__) {
+ checkAttributeStringCoercion(href, 'href');
+ }
+ const coercedHref = '' + (href: any);
+ sanitizeURL(coercedHref);
+ writeChunk(
+ destination,
+ stringToChunk(escapeTextForBrowser(JSON.stringify(coercedHref))),
+ );
+
+ if (__DEV__) {
+ checkAttributeStringCoercion(precedence, 'precedence');
+ }
+ const coercedPrecedence = '' + (precedence: any);
+ writeChunk(destination, arrayInterstitial);
+ writeChunk(
+ destination,
+ stringToChunk(escapeTextForBrowser(JSON.stringify(coercedPrecedence))),
+ );
+
+ for (const propKey in props) {
+ if (hasOwnProperty.call(props, propKey)) {
+ const propValue = props[propKey];
+ if (propValue == null) {
+ continue;
+ }
+ switch (propKey) {
+ case 'href':
+ case 'rel':
+ case 'precedence':
+ case 'data-precedence': {
+ break;
+ }
+ case 'children':
+ case 'dangerouslySetInnerHTML':
+ throw new Error(
+ `${'link'} is a self-closing tag and must neither have \`children\` nor ` +
+ 'use `dangerouslySetInnerHTML`.',
+ );
+ // eslint-disable-next-line-no-fallthrough
+ default:
+ writeStyleResourceAttributeInAttr(destination, propKey, propValue);
+ break;
+ }
+ }
+ }
+ return null;
+}
+
+function writeStyleResourceAttributeInAttr(
+ destination: Destination,
+ name: string,
+ value: string | boolean | number | Function | Object, // not null or undefined
+): void {
+ let attributeName = name.toLowerCase();
+ let attributeValue;
+ switch (typeof value) {
+ case 'function':
+ case 'symbol':
+ return;
+ }
+
+ switch (name) {
+ // Reserved names
+ case 'innerHTML':
+ case 'dangerouslySetInnerHTML':
+ case 'suppressContentEditableWarning':
+ case 'suppressHydrationWarning':
+ case 'style':
+ // Ignored
+ return;
+
+ // Attribute renames
+ case 'className':
+ attributeName = 'class';
+ break;
+
+ // Booleans
+ case 'hidden':
+ if (value === false) {
+ return;
+ }
+ attributeValue = '';
+ break;
+
+ // Santized URLs
+ case 'src':
+ case 'href': {
+ if (__DEV__) {
+ checkAttributeStringCoercion(value, attributeName);
+ }
+ attributeValue = '' + (value: any);
+ sanitizeURL(attributeValue);
+ break;
+ }
+ default: {
+ if (!isAttributeNameSafe(name)) {
+ return;
+ }
+ }
+ }
+
+ if (
+ // shouldIgnoreAttribute
+ // We have already filtered out null/undefined and reserved words.
+ name.length > 2 &&
+ (name[0] === 'o' || name[0] === 'O') &&
+ (name[1] === 'n' || name[1] === 'N')
+ ) {
+ return;
+ }
+
+ if (__DEV__) {
+ checkAttributeStringCoercion(value, attributeName);
+ }
+ attributeValue = '' + (value: any);
+ writeChunk(destination, arrayInterstitial);
+ writeChunk(
+ destination,
+ stringToChunk(escapeTextForBrowser(JSON.stringify(attributeName))),
+ );
+ writeChunk(destination, arrayInterstitial);
+ writeChunk(
+ destination,
+ stringToChunk(escapeTextForBrowser(JSON.stringify(attributeValue))),
+ );
+}
diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js
index 01e70273a656f..aa11451b1eeea 100644
--- a/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js
+++ b/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js
@@ -7,7 +7,11 @@
* @flow
*/
-import type {FormatContext} from './ReactDOMServerFormatConfig';
+import type {
+ BootstrapScriptDescriptor,
+ FormatContext,
+ StreamingFormat,
+} from './ReactDOMServerFormatConfig';
import {
createResponseState as createResponseStateImpl,
@@ -31,16 +35,18 @@ export const isPrimaryRenderer = false;
export type ResponseState = {
// Keep this in sync with ReactDOMServerFormatConfig
bootstrapChunks: Array,
- startInlineScript: PrecomputedChunk,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
idPrefix: string,
nextSuspenseID: number,
+ streamingFormat: StreamingFormat,
+ startInlineScript: PrecomputedChunk,
sentCompleteSegmentFunction: boolean,
sentCompleteBoundaryFunction: boolean,
sentClientRenderFunction: boolean,
sentStyleInsertionFunction: boolean,
+ externalRuntimeConfig: BootstrapScriptDescriptor | null,
// This is an extra field for the legacy renderer
generateStaticMarkup: boolean,
};
@@ -48,21 +54,31 @@ export type ResponseState = {
export function createResponseState(
generateStaticMarkup: boolean,
identifierPrefix: string | void,
+ externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
): ResponseState {
- const responseState = createResponseStateImpl(identifierPrefix, undefined);
+ const responseState = createResponseStateImpl(
+ identifierPrefix,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ externalRuntimeConfig,
+ );
return {
// Keep this in sync with ReactDOMServerFormatConfig
bootstrapChunks: responseState.bootstrapChunks,
- startInlineScript: responseState.startInlineScript,
placeholderPrefix: responseState.placeholderPrefix,
segmentPrefix: responseState.segmentPrefix,
boundaryPrefix: responseState.boundaryPrefix,
idPrefix: responseState.idPrefix,
nextSuspenseID: responseState.nextSuspenseID,
+ streamingFormat: responseState.streamingFormat,
+ startInlineScript: responseState.startInlineScript,
sentCompleteSegmentFunction: responseState.sentCompleteSegmentFunction,
sentCompleteBoundaryFunction: responseState.sentCompleteBoundaryFunction,
sentClientRenderFunction: responseState.sentClientRenderFunction,
sentStyleInsertionFunction: responseState.sentStyleInsertionFunction,
+ externalRuntimeConfig: responseState.externalRuntimeConfig,
// This is an extra field for the legacy renderer
generateStaticMarkup,
};
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 030e5093e52fe..b900f3452b4ee 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -8,7 +8,12 @@
*/
'use strict';
-import {replaceScriptsAndMove, mergeOptions} from '../test-utils/FizzTestUtils';
+import {
+ replaceScriptsAndMove,
+ mergeOptions,
+ stripExternalRuntimeInNodes,
+ withLoadingReadyState,
+} from '../test-utils/FizzTestUtils';
let JSDOM;
let Stream;
@@ -31,7 +36,7 @@ let container;
let buffer = '';
let hasErrored = false;
let fatalError = undefined;
-const renderOptions = {};
+let renderOptions;
describe('ReactDOMFizzServer', () => {
beforeEach(() => {
@@ -89,6 +94,12 @@ describe('ReactDOMFizzServer', () => {
hasErrored = true;
fatalError = error;
});
+
+ renderOptions = {};
+ if (gate(flags => flags.enableFizzExternalRuntime)) {
+ renderOptions.unstable_externalRuntimeSrc =
+ 'react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js';
+ }
});
function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {
@@ -134,10 +145,13 @@ describe('ReactDOMFizzServer', () => {
fakeBody.innerHTML = bufferedContent;
const parent =
container.nodeName === '#document' ? container.body : container;
- while (fakeBody.firstChild) {
- const node = fakeBody.firstChild;
- await replaceScriptsAndMove(window, CSPnonce, node, parent);
- }
+
+ await withLoadingReadyState(async () => {
+ while (fakeBody.firstChild) {
+ const node = fakeBody.firstChild;
+ await replaceScriptsAndMove(window, CSPnonce, node, parent);
+ }
+ }, document);
}
async function actIntoEmptyDocument(callback) {
@@ -162,7 +176,9 @@ describe('ReactDOMFizzServer', () => {
document = jsdom.window.document;
container = document;
buffer = '';
- await replaceScriptsAndMove(window, CSPnonce, document.documentElement);
+ await withLoadingReadyState(async () => {
+ await replaceScriptsAndMove(window, CSPnonce, document.documentElement);
+ }, document);
}
function getVisibleChildren(element) {
@@ -595,7 +611,12 @@ describe('ReactDOMFizzServer', () => {
// Because there is no content inside the Suspense boundary that could've
// been written, we expect to not see any additional partial data flushed
// yet.
- expect(container.childNodes.length).toBe(1);
+ expect(
+ stripExternalRuntimeInNodes(
+ container.childNodes,
+ renderOptions.unstable_externalRuntimeSrc,
+ ).length,
+ ).toBe(1);
await act(async () => {
resolveElement({default: });
});
@@ -3490,7 +3511,10 @@ describe('ReactDOMFizzServer', () => {
,
);
expect(
- Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML),
+ stripExternalRuntimeInNodes(
+ document.getElementsByTagName('script'),
+ renderOptions.unstable_externalRuntimeSrc,
+ ).map(n => n.outerHTML),
).toEqual([
'',
'',
@@ -3566,7 +3590,9 @@ describe('ReactDOMFizzServer', () => {
-
hello world
+
+
+
,
{
@@ -3576,17 +3602,68 @@ describe('ReactDOMFizzServer', () => {
pipe(writable);
});
+ // We want the external runtime to be sent in so the script can be
+ // fetched and executed as early as possible. For SSR pages using Suspense,
+ // this script execution would be render blocking.
+ expect(
+ Array.from(document.head.getElementsByTagName('script')).map(
+ n => n.outerHTML,
+ ),
+ ).toEqual(['']);
+
expect(getVisibleChildren(document)).toEqual(
-
-
hello world
-
+ loading...
,
);
- expect(
- Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML),
- ).toEqual(['']);
+ });
+
+ // @gate enableFizzExternalRuntime
+ it('does not send script tags for SSR instructions when using the external runtime', async () => {
+ function App() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ await actIntoEmptyDocument(() => {
+ const {pipe} = renderToPipeableStream();
+ pipe(writable);
+ });
+ await act(async () => {
+ resolveText('Hello');
+ });
+
+ // The only script elements sent should be from unstable_externalRuntimeSrc
+ expect(document.getElementsByTagName('script').length).toEqual(1);
+ });
+
+ it('does not send the external runtime for static pages', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = renderToPipeableStream(
+
+
+
+
hello world!
+
+ ,
+ );
+ pipe(writable);
+ });
+
+ // no scripts should be sent
+ expect(document.getElementsByTagName('script').length).toEqual(0);
+
+ // the html should be as-is
+ expect(document.documentElement.innerHTML).toEqual(
+ '
hello world!
',
+ );
});
it('#24384: Suspending should halt hydration warnings and not emit any if hydration completes successfully after unsuspending', async () => {
@@ -4527,12 +4604,21 @@ describe('ReactDOMFizzServer', () => {
await act(() => resolveText('Foo'));
- expect(container.firstElementChild.outerHTML).toEqual(
+ const div = stripExternalRuntimeInNodes(
+ container.children,
+ renderOptions.unstable_externalRuntimeSrc,
+ )[0];
+ expect(div.outerHTML).toEqual(
'
helloworld, Foo!
',
);
- // there are extra script nodes at the end of container
- expect(container.childNodes.length).toBe(5);
- const div = container.childNodes[1];
+ // there may be either:
+ // - an external runtime script and deleted nodes with data attributes
+ // - extra script nodes containing fizz instructions at the end of container
+ expect(
+ Array.from(container.childNodes).filter(e => e.tagName !== 'SCRIPT')
+ .length,
+ ).toBe(3);
+
expect(div.childNodes.length).toBe(3);
const b = div.childNodes[1];
expect(b.childNodes.length).toBe(2);
@@ -4582,9 +4668,12 @@ describe('ReactDOMFizzServer', () => {
);
await act(() => resolveText('ello'));
- expect(container.firstElementChild.outerHTML).toEqual(
- '