Add option to load Fizz runtime from external file (#25499)
* Add feature flag for external Fizz runtime Only enabled for www for now * Add option to load Fizz runtime from external file 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.
Andrew Clark committed
Oct 17, 2022 at 17:57 UTC
500bea532d1b349fd5681b6477c14cedf4ed54ac
14 files changed
+73
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+25
@@ -23,6 +23,7 @@ import {
23
enableFilterEmptyStringAttributesDOM,
24
enableCustomElementPropertySupport,
25
enableFloat,
26
+ enableFizzExternalRuntime,
27
} from 'shared/ReactFeatureFlags';
28
29
import type {
@@ -157,6 +158,7 @@ export function createResponseState(
158
bootstrapScriptContent: string | void,
159
bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
160
bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
161
+ externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
162
): ResponseState {
163
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
164
const inlineScriptWithNonce =
@@ -173,6 +175,29 @@ export function createResponseState(
175
endInlineScript,
176
);
177
}
178
+ if (enableFizzExternalRuntime) {
179
+ if (externalRuntimeConfig !== undefined) {
180
+ const src =
181
+ typeof externalRuntimeConfig === 'string'
182
+ ? externalRuntimeConfig
183
+ : externalRuntimeConfig.src;
184
+ const integrity =
185
+ typeof externalRuntimeConfig === 'string'
186
+ ? undefined
187
+ : externalRuntimeConfig.integrity;
188
+ bootstrapChunks.push(
189
+ startScriptSrc,
190
+ stringToChunk(escapeTextForBrowser(src)),
191
+ );
192
+ if (integrity) {
193
+ bootstrapChunks.push(
194
+ scriptIntegirty,
195
+ stringToChunk(escapeTextForBrowser(integrity)),
196
+ );
197
+ }
198
+ bootstrapChunks.push(endAsyncScript);
199
+ }
200
+ }
201
if (bootstrapScripts !== undefined) {
202
for (let i = 0; i < bootstrapScripts.length; i++) {
203
const scriptConfig = bootstrapScripts[i];
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+30
@@ -3546,6 +3546,36 @@ describe('ReactDOMFizzServer', () => {
3546
});
3547
});
3548
3549
+ // @gate enableFizzExternalRuntime
3550
+ it('supports option to load runtime as an external script', async () => {
3551
+ await actIntoEmptyDocument(() => {
3552
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3553
+ <html>
3554
+ <head />
3555
+ <body>
3556
+ <div>hello world</div>
3557
+ </body>
3558
+ </html>,
3559
+ {
3560
+ unstable_externalRuntimeSrc: 'src-of-external-runtime',
3561
+ },
3562
+ );
3563
+ pipe(writable);
3564
+ });
3565
+
3566
+ expect(getVisibleChildren(document)).toEqual(
3567
+ <html>
3568
+ <head />
3569
+ <body>
3570
+ <div>hello world</div>
3571
+ </body>
3572
+ </html>,
3573
+ );
3574
+ expect(
3575
+ Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML),
3576
+ ).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
3577
+ });
3578
+
3579
it('#24384: Suspending should halt hydration warnings and not emit any if hydration completes successfully after unsuspending', async () => {
3580
const makeApp = () => {
3581
let resolve, resolved;
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+2
@@ -34,6 +34,7 @@ type Options = {
34
progressiveChunkSize?: number,
35
signal?: AbortSignal,
36
onError?: (error: mixed) => ?string,
37
+ unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
38
};
39
40
// TODO: Move to sub-classing ReadableStream.
@@ -86,6 +87,7 @@ function renderToReadableStream(
87
options ? options.bootstrapScriptContent : undefined,
88
options ? options.bootstrapScripts : undefined,
89
options ? options.bootstrapModules : undefined,
90
+ options ? options.unstable_externalRuntimeSrc : undefined,
91
),
92
createRootFormatContext(options ? options.namespaceURI : undefined),
93
options ? options.progressiveChunkSize : undefined,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+2
@@ -47,6 +47,7 @@ type Options = {
47
onShellError?: (error: mixed) => void,
48
onAllReady?: () => void,
49
onError?: (error: mixed) => ?string,
50
+ unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
51
};
52
53
type PipeableStream = {
@@ -65,6 +66,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
66
options ? options.bootstrapScriptContent : undefined,
67
options ? options.bootstrapScripts : undefined,
68
options ? options.bootstrapModules : undefined,
69
+ options ? options.unstable_externalRuntimeSrc : undefined,
70
),
71
createRootFormatContext(options ? options.namespaceURI : undefined),
72
options ? options.progressiveChunkSize : undefined,
packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+2
@@ -33,6 +33,7 @@ type Options = {
33
progressiveChunkSize?: number,
34
signal?: AbortSignal,
35
onError?: (error: mixed) => ?string,
36
+ unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
37
};
38
39
type StaticResult = {
@@ -71,6 +72,7 @@ function prerender(
72
options ? options.bootstrapScriptContent : undefined,
73
options ? options.bootstrapScripts : undefined,
74
options ? options.bootstrapModules : undefined,
75
+ options ? options.unstable_externalRuntimeSrc : undefined,
76
),
77
createRootFormatContext(options ? options.namespaceURI : undefined),
78
options ? options.progressiveChunkSize : undefined,
packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+2
@@ -35,6 +35,7 @@ type Options = {
35
progressiveChunkSize?: number,
36
signal?: AbortSignal,
37
onError?: (error: mixed) => ?string,
38
+ unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
39
};
40
41
type StaticResult = {
@@ -86,6 +87,7 @@ function prerenderToNodeStreams(
87
options ? options.bootstrapScriptContent : undefined,
88
options ? options.bootstrapScripts : undefined,
89
options ? options.bootstrapModules : undefined,
90
+ options ? options.unstable_externalRuntimeSrc : undefined,
91
),
92
createRootFormatContext(options ? options.namespaceURI : undefined),
93
options ? options.progressiveChunkSize : undefined,
packages/shared/ReactFeatureFlags.js
+3
@@ -125,6 +125,9 @@ export const enableUseMemoCacheHook = __EXPERIMENTAL__;
125
126
export const enableUseEventHook = __EXPERIMENTAL__;
127
128
+// Test in www before enabling in open source.
129
+export const enableFizzExternalRuntime = false;
130
+
131
// -----------------------------------------------------------------------------
132
// Chopping Block
133
//
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -86,6 +86,7 @@ export const enableFloat = false;
86
export const enableHostSingletons = false;
87
88
export const useModernStrictMode = false;
89
+export const enableFizzExternalRuntime = false;
90
91
// Flow magic to verify the exports of this file match the original version.
92
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -76,6 +76,7 @@ export const enableFloat = false;
76
export const enableHostSingletons = false;
77
78
export const useModernStrictMode = false;
79
+export const enableFizzExternalRuntime = false;
80
81
// Flow magic to verify the exports of this file match the original version.
82
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -76,6 +76,7 @@ export const enableFloat = false;
76
export const enableHostSingletons = false;
77
78
export const useModernStrictMode = false;
79
+export const enableFizzExternalRuntime = false;
80
81
// Flow magic to verify the exports of this file match the original version.
82
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -78,6 +78,7 @@ export const enableFloat = false;
78
export const enableHostSingletons = false;
79
80
export const useModernStrictMode = false;
81
+export const enableFizzExternalRuntime = false;
82
83
// Flow magic to verify the exports of this file match the original version.
84
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -76,6 +76,7 @@ export const enableFloat = false;
76
export const enableHostSingletons = false;
77
78
export const useModernStrictMode = false;
79
+export const enableFizzExternalRuntime = false;
80
81
// Flow magic to verify the exports of this file match the original version.
82
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -77,6 +77,7 @@ export const enableFloat = false;
77
export const enableHostSingletons = false;
78
79
export const useModernStrictMode = false;
80
+export const enableFizzExternalRuntime = false;
81
82
// Flow magic to verify the exports of this file match the original version.
83
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -113,6 +113,7 @@ export const enableUseMutableSource = true;
113
export const enableCustomElementPropertySupport = __EXPERIMENTAL__;
114
115
export const useModernStrictMode = false;
116
+export const enableFizzExternalRuntime = true;
117
118
// Flow magic to verify the exports of this file match the original version.
119
((((null: any): ExportsType): FeatureFlagsType): ExportsType);