From 68a25c16ca0986d138fbd0f8d779a41b1e4805d9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 17 Oct 2022 16:52:15 -0400 Subject: [PATCH] Add option to load Fizz runtime from external file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When unstable_externalRuntimeSrc is provided, React will inject a script tag that points to the provided URL. Then, instead of emitting inline scripts, the Fizz stream will emit HTML nodes with data attributes that encode the instructions. The external runtime will detect these with a mutation observer and translate them into runtime commands. This part isn't implemented in this PR, though — all this does is set up the option to use an external runtime, and inject the script tag. The external runtime is injected at the same time as bootstrap scripts. --- .../src/server/ReactDOMServerFormatConfig.js | 25 ++++++++++++++++ .../src/__tests__/ReactDOMFizzServer-test.js | 30 +++++++++++++++++++ .../src/server/ReactDOMFizzServerBrowser.js | 2 +- .../src/server/ReactDOMFizzServerNode.js | 2 ++ .../src/server/ReactDOMFizzStaticBrowser.js | 2 ++ .../src/server/ReactDOMFizzStaticNode.js | 2 ++ 6 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index f5c95eaa37c69..f6c01a2dd4333 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -23,6 +23,7 @@ import { enableFilterEmptyStringAttributesDOM, enableCustomElementPropertySupport, enableFloat, + enableFizzExternalRuntime, } from 'shared/ReactFeatureFlags'; import type { @@ -156,6 +157,7 @@ export function createResponseState( bootstrapScriptContent: string | void, bootstrapScripts: $ReadOnlyArray | void, bootstrapModules: $ReadOnlyArray | void, + externalRuntimeConfig: string | BootstrapScriptDescriptor | void, ): ResponseState { const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix; const inlineScriptWithNonce = @@ -172,6 +174,29 @@ export function createResponseState( endInlineScript, ); } + if (enableFizzExternalRuntime) { + if (externalRuntimeConfig !== undefined) { + const src = + typeof externalRuntimeConfig === 'string' + ? externalRuntimeConfig + : externalRuntimeConfig.src; + const integrity = + typeof externalRuntimeConfig === 'string' + ? undefined + : externalRuntimeConfig.integrity; + bootstrapChunks.push( + startScriptSrc, + stringToChunk(escapeTextForBrowser(src)), + ); + if (integrity) { + bootstrapChunks.push( + scriptIntegirty, + stringToChunk(escapeTextForBrowser(integrity)), + ); + } + bootstrapChunks.push(endAsyncScript); + } + } if (bootstrapScripts !== undefined) { for (let i = 0; i < bootstrapScripts.length; i++) { const scriptConfig = bootstrapScripts[i]; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 82d103cfecf89..07a6684b1b809 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3546,6 +3546,36 @@ describe('ReactDOMFizzServer', () => { }); }); + // @gate enableFizzExternalRuntime + it('supports option to load runtime as an external script', async () => { + await actIntoEmptyDocument(() => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + + + +
hello world
+ + , + { + unstable_externalRuntimeSrc: 'src-of-external-runtime', + }, + ); + pipe(writable); + }); + + expect(getVisibleChildren(document)).toEqual( + + + +
hello world
+ + , + ); + expect( + Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML), + ).toEqual(['']); + }); + it('#24384: Suspending should halt hydration warnings and not emit any if hydration completes successfully after unsuspending', async () => { const makeApp = () => { let resolve, resolved; diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 4285b75a804fa..ffe7e07d10732 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -34,7 +34,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed) => ?string, - unstable_externalRuntimeSrc?: string, + unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; // TODO: Move to sub-classing ReadableStream. diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 5280dd50a13a5..ccc930cf8890d 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -47,6 +47,7 @@ type Options = { onShellError?: (error: mixed) => void, onAllReady?: () => void, onError?: (error: mixed) => ?string, + unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; type PipeableStream = { @@ -65,6 +66,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) { options ? options.bootstrapScriptContent : undefined, options ? options.bootstrapScripts : undefined, options ? options.bootstrapModules : undefined, + options ? options.unstable_externalRuntimeSrc : undefined, ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index 743e807a0594f..7607579db56cf 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -33,6 +33,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed) => ?string, + unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; type StaticResult = { @@ -71,6 +72,7 @@ function prerender( options ? options.bootstrapScriptContent : undefined, options ? options.bootstrapScripts : undefined, options ? options.bootstrapModules : undefined, + options ? options.unstable_externalRuntimeSrc : undefined, ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index 74631c3e19f8f..0a8ff59a3f4a5 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -35,6 +35,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed) => ?string, + unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; type StaticResult = { @@ -86,6 +87,7 @@ function prerenderToNodeStreams( options ? options.bootstrapScriptContent : undefined, options ? options.bootstrapScripts : undefined, options ? options.bootstrapModules : undefined, + options ? options.unstable_externalRuntimeSrc : undefined, ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined,