[Fizz/Float] Float for stylesheet resources (#25243)
* [Fizz/Float] Float for stylesheet resources This commit implements Float in Fizz and on the Client. The initial set of supported APIs is roughly 1. Convert certain stylesheets into style Resources when opting in with precedence prop 2. Emit preloads for stylesheets and explicit preload tags 3. Dedupe all Resources by href 4. Implement ReactDOM.preload() to allow for imperative preloading 5. Implement ReactDOM.preinit() to allow for imperative preinitialization Currently supports 1. style Resources (link rel "stylesheet") 2. font Resources (preload as "font") later updates will include support for scripts and modules
Josh Story committed
Sep 30, 2022 at 16:14 UTC
7b25b961df878109a2b3810f33815249cae37ecc
57 files changed
+7070
-634
packages/react-art/src/ReactARTHostConfig.js
+9
@@ -243,6 +243,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
243
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
244
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
245
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
246
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
247
248
export function appendInitialChild(parentInstance, child) {
249
if (typeof child === 'string') {
@@ -455,3 +456,11 @@ export function detachDeletedInstance(node: Instance): void {
456
export function requestPostPaintCallback(callback: (time: number) => void) {
457
// noop
458
}
459
+
460
+export function prepareRendererToRender(container: Container): void {
461
+ // noop
462
+}
463
+
464
+export function resetRendererAfterRender(): void {
465
+ // noop
466
+}
packages/react-dom-bindings/src/client/ReactDOMComponent.js
-12
@@ -73,7 +73,6 @@ import {
73
enableTrustedTypesIntegration,
74
enableCustomElementPropertySupport,
75
enableClientRenderFallbackOnTextMismatch,
76
- enableFloat,
76
} from 'shared/ReactFeatureFlags';
77
import {
78
mediaEventTypes,
@@ -1019,17 +1018,6 @@ export function diffHydratedProperties(
1018
: getPropertyInfo(propKey);
1019
if (rawProps[SUPPRESS_HYDRATION_WARNING] === true) {
1020
// Don't bother comparing. We're ignoring all these warnings.
1022
- } else if (
1023
- enableFloat &&
1024
- tag === 'link' &&
1025
- rawProps.rel === 'stylesheet' &&
1026
- propKey === 'precedence'
1027
- ) {
1028
- // @TODO this is a temporary rule while we haven't implemented HostResources yet. This is used to allow
1029
- // for hydrating Resources (at the moment, stylesheets with a precedence prop) by using a data attribute.
1030
- // When we implement HostResources there will be no hydration directly so this code can be deleted
1031
- // $FlowFixMe - Should be inferred as not undefined.
1032
- extraAttributeNames.delete('data-rprec');
1021
} else if (
1022
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
1023
propKey === SUPPRESS_HYDRATION_WARNING ||
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+9
-3
@@ -23,6 +23,7 @@ import type {
23
24
import {
25
HostComponent,
26
+ HostResource,
27
HostText,
28
HostRoot,
29
SuspenseComponent,
@@ -30,7 +31,7 @@ import {
31
32
import {getParentSuspenseInstance} from './ReactDOMHostConfig';
33
33
-import {enableScopeAPI} from 'shared/ReactFeatureFlags';
34
+import {enableScopeAPI, enableFloat} from 'shared/ReactFeatureFlags';
35
36
const randomKey = Math.random()
37
.toString(36)
@@ -166,7 +167,8 @@ export function getInstanceFromNode(node: Node): Fiber | null {
167
inst.tag === HostComponent ||
168
inst.tag === HostText ||
169
inst.tag === SuspenseComponent ||
169
- inst.tag === HostRoot
170
+ inst.tag === HostRoot ||
171
+ (enableFloat ? inst.tag === HostResource : false)
172
) {
173
return inst;
174
} else {
@@ -181,7 +183,11 @@ export function getInstanceFromNode(node: Node): Fiber | null {
183
* DOM node.
184
*/
185
export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
184
- if (inst.tag === HostComponent || inst.tag === HostText) {
186
+ if (
187
+ inst.tag === HostComponent ||
188
+ inst.tag === HostText ||
189
+ (enableFloat ? inst.tag === HostResource : false)
190
+ ) {
191
// In Fiber this, is just the state node right now. We assume it will be
192
// a host component or host text.
193
return inst.stateNode;
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
new
+825
@@ -0,0 +1,825 @@
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 {Instance} from './ReactDOMHostConfig';
11
+import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
12
+const {Dispatcher} = ReactDOMSharedInternals;
13
+import {
14
+ validateUnmatchedLinkResourceProps,
15
+ validatePreloadResourceDifference,
16
+ validateHrefKeyedUpdatedProps,
17
+ validateStyleResourceDifference,
18
+ validateLinkPropsForStyleResource,
19
+ validateLinkPropsForPreloadResource,
20
+ validatePreloadArguments,
21
+ validatePreinitArguments,
22
+} from '../shared/ReactDOMResourceValidation';
23
+import {createElement, setInitialProperties} from './ReactDOMComponent';
24
+import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
25
+
26
+// The resource types we support. currently they match the form for the as argument.
27
+// In the future this may need to change, especially when modules / scripts are supported
28
+type ResourceType = 'style' | 'font';
29
+
30
+type PreloadProps = {
31
+ rel: 'preload',
32
+ as: ResourceType,
33
+ href: string,
34
+ [string]: mixed,
35
+};
36
+type PreloadResource = {
37
+ type: 'preload',
38
+ href: string,
39
+ ownerDocument: Document,
40
+ props: PreloadProps,
41
+ instance: Element,
42
+};
43
+
44
+type StyleProps = {
45
+ rel: 'stylesheet',
46
+ href: string,
47
+ 'data-rprec': string,
48
+ [string]: mixed,
49
+};
50
+type StyleResource = {
51
+ type: 'style',
52
+
53
+ // Ref count for resource
54
+ count: number,
55
+
56
+ // Resource Descriptors
57
+ href: string,
58
+ precedence: string,
59
+ props: StyleProps,
60
+
61
+ // Related Resources
62
+ hint: ?PreloadResource,
63
+
64
+ // Insertion
65
+ preloaded: boolean,
66
+ loaded: boolean,
67
+ error: mixed,
68
+ instance: ?Element,
69
+ ownerDocument: Document,
70
+};
71
+
72
+type Props = {[string]: mixed};
73
+
74
+type Resource = StyleResource | PreloadResource;
75
+
76
+// Brief on purpose due to insertion by script when streaming late boundaries
77
+// s = Status
78
+// l = loaded
79
+// e = errored
80
+type StyleResourceLoadingState = Promise<mixed> & {s?: 'l' | 'e'};
81
+
82
+// When rendering we set the currentDocument if one exists. we use this for Resources
83
+// we encounter during render. If this is null and we are dispatching preloads and
84
+// other calls on the ReactDOM module we look for the window global and get the document from there
85
+let currentDocument: ?Document = null;
86
+
87
+// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
88
+// called when there is no rendering we track the last used document. It is not safe to insert
89
+// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
90
+// that the resource is meant to apply too (for example stylesheets or scripts). This is only
91
+// appropriate for resources that don't really have a strict tie to the document itself for example
92
+// preloads
93
+let lastCurrentDocument: ?Document = null;
94
+
95
+let previousDispatcher = null;
96
+export function prepareToRenderResources(ownerDocument: Document) {
97
+ currentDocument = lastCurrentDocument = ownerDocument;
98
+ previousDispatcher = Dispatcher.current;
99
+ Dispatcher.current = ReactDOMClientDispatcher;
100
+}
101
+
102
+export function cleanupAfterRenderResources() {
103
+ currentDocument = null;
104
+ Dispatcher.current = previousDispatcher;
105
+ previousDispatcher = null;
106
+}
107
+
108
+// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
109
+// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
110
+// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
111
+export const ReactDOMClientDispatcher = {preload, preinit};
112
+
113
+// global maps of Resources
114
+const preloadResources: Map<string, PreloadResource> = new Map();
115
+const styleResources: Map<string, StyleResource> = new Map();
116
+
117
+// Preloads are somewhat special. Even if we don't have the Document
118
+// used by the root that is rendering a component trying to insert a preload
119
+// we can still seed the file cache by doing the preload on any document we have
120
+// access to. We prefer the currentDocument if it exists, we also prefer the
121
+// lastCurrentDocument if that exists. As a fallback we will use the window.document
122
+// if available.
123
+function getDocumentForPreloads(): ?Document {
124
+ try {
125
+ return currentDocument || lastCurrentDocument || window.document;
126
+ } catch (error) {
127
+ return null;
128
+ }
129
+}
130
+
131
+// --------------------------------------
132
+// ReactDOM.Preload
133
+// --------------------------------------
134
+type PreloadAs = ResourceType;
135
+type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
136
+function preload(href: string, options: PreloadOptions) {
137
+ if (__DEV__) {
138
+ validatePreloadArguments(href, options);
139
+ }
140
+ const ownerDocument = getDocumentForPreloads();
141
+ if (
142
+ typeof href === 'string' &&
143
+ href &&
144
+ typeof options === 'object' &&
145
+ options !== null &&
146
+ ownerDocument
147
+ ) {
148
+ const as = options.as;
149
+ const resource = preloadResources.get(href);
150
+ if (resource) {
151
+ if (__DEV__) {
152
+ const originallyImplicit =
153
+ (resource: any)._dev_implicit_construction === true;
154
+ const latestProps = preloadPropsFromPreloadOptions(href, as, options);
155
+ validatePreloadResourceDifference(
156
+ resource.props,
157
+ originallyImplicit,
158
+ latestProps,
159
+ false,
160
+ );
161
+ }
162
+ } else {
163
+ const resourceProps = preloadPropsFromPreloadOptions(href, as, options);
164
+ createPreloadResource(ownerDocument, href, resourceProps);
165
+ }
166
+ }
167
+}
168
+
169
+function preloadPropsFromPreloadOptions(
170
+ href: string,
171
+ as: ResourceType,
172
+ options: PreloadOptions,
173
+): PreloadProps {
174
+ return {
175
+ href,
176
+ rel: 'preload',
177
+ as,
178
+ crossOrigin: as === 'font' ? '' : options.crossOrigin,
179
+ };
180
+}
181
+
182
+// --------------------------------------
183
+// ReactDOM.preinit
184
+// --------------------------------------
185
+
186
+type PreinitAs = 'style';
187
+type PreinitOptions = {
188
+ as: PreinitAs,
189
+ crossOrigin?: string,
190
+ precedence?: string,
191
+};
192
+function preinit(href: string, options: PreinitOptions) {
193
+ if (__DEV__) {
194
+ validatePreinitArguments(href, options);
195
+ }
196
+
197
+ if (
198
+ typeof href === 'string' &&
199
+ href &&
200
+ typeof options === 'object' &&
201
+ options !== null
202
+ ) {
203
+ const as = options.as;
204
+ if (!currentDocument) {
205
+ // We are going to emit a preload as a best effort fallback since this preinit
206
+ // was called outside of a render. Given the passive nature of this fallback
207
+ // we do not warn in dev when props disagree if there happens to already be a
208
+ // matching preload with this href
209
+ const preloadDocument = getDocumentForPreloads();
210
+ if (preloadDocument) {
211
+ const preloadResource = preloadResources.get(href);
212
+ if (!preloadResource) {
213
+ const preloadProps = preloadPropsFromPreinitOptions(
214
+ href,
215
+ as,
216
+ options,
217
+ );
218
+ createPreloadResource(preloadDocument, href, preloadProps);
219
+ }
220
+ }
221
+ return;
222
+ }
223
+
224
+ switch (as) {
225
+ case 'style': {
226
+ const precedence = options.precedence || 'default';
227
+ let resource = styleResources.get(href);
228
+ if (resource) {
229
+ if (__DEV__) {
230
+ const latestProps = stylePropsFromPreinitOptions(
231
+ href,
232
+ precedence,
233
+ options,
234
+ );
235
+ validateStyleResourceDifference(resource.props, latestProps);
236
+ }
237
+ } else {
238
+ const resourceProps = stylePropsFromPreinitOptions(
239
+ href,
240
+ precedence,
241
+ options,
242
+ );
243
+ resource = createStyleResource(
244
+ currentDocument,
245
+ href,
246
+ precedence,
247
+ resourceProps,
248
+ );
249
+ }
250
+ acquireResource(resource);
251
+ }
252
+ }
253
+ }
254
+}
255
+
256
+function preloadPropsFromPreinitOptions(
257
+ href: string,
258
+ as: ResourceType,
259
+ options: PreinitOptions,
260
+): PreloadProps {
261
+ return {
262
+ href,
263
+ rel: 'preload',
264
+ as,
265
+ crossOrigin: as === 'font' ? '' : options.crossOrigin,
266
+ };
267
+}
268
+
269
+function stylePropsFromPreinitOptions(
270
+ href: string,
271
+ precedence: string,
272
+ options: PreinitOptions,
273
+): StyleProps {
274
+ return {
275
+ rel: 'stylesheet',
276
+ href,
277
+ 'data-rprec': precedence,
278
+ crossOrigin: options.crossOrigin,
279
+ };
280
+}
281
+
282
+// --------------------------------------
283
+// Resources from render
284
+// --------------------------------------
285
+
286
+type StyleQualifyingProps = {
287
+ rel: 'stylesheet',
288
+ href: string,
289
+ precedence: string,
290
+ [string]: mixed,
291
+};
292
+type PreloadQualifyingProps = {
293
+ rel: 'preload',
294
+ href: string,
295
+ as: ResourceType,
296
+ [string]: mixed,
297
+};
298
+
299
+// This function is called in begin work and we should always have a currentDocument set
300
+export function getResource(
301
+ type: string,
302
+ pendingProps: Props,
303
+ currentProps: null | Props,
304
+): null | Resource {
305
+ if (!currentDocument) {
306
+ throw new Error(
307
+ '"currentDocument" was expected to exist. This is a bug in React.',
308
+ );
309
+ }
310
+ switch (type) {
311
+ case 'link': {
312
+ const {rel} = pendingProps;
313
+ switch (rel) {
314
+ case 'stylesheet': {
315
+ let didWarn;
316
+ if (__DEV__) {
317
+ if (currentProps) {
318
+ didWarn = validateHrefKeyedUpdatedProps(
319
+ pendingProps,
320
+ currentProps,
321
+ );
322
+ }
323
+ if (!didWarn) {
324
+ didWarn = validateLinkPropsForStyleResource(pendingProps);
325
+ }
326
+ }
327
+ const {precedence, href} = pendingProps;
328
+ if (typeof href === 'string' && typeof precedence === 'string') {
329
+ // We've asserted all the specific types for StyleQualifyingProps
330
+ const styleRawProps: StyleQualifyingProps = (pendingProps: any);
331
+
332
+ // We construct or get an existing resource for the style itself and return it
333
+ let resource = styleResources.get(href);
334
+ if (resource) {
335
+ if (__DEV__) {
336
+ if (!didWarn) {
337
+ const latestProps = stylePropsFromRawProps(styleRawProps);
338
+ if ((resource: any)._dev_preload_props) {
339
+ adoptPreloadProps(
340
+ latestProps,
341
+ (resource: any)._dev_preload_props,
342
+ );
343
+ }
344
+ validateStyleResourceDifference(resource.props, latestProps);
345
+ }
346
+ }
347
+ } else {
348
+ const resourceProps = stylePropsFromRawProps(styleRawProps);
349
+ resource = createStyleResource(
350
+ currentDocument,
351
+ href,
352
+ precedence,
353
+ resourceProps,
354
+ );
355
+ immediatelyPreloadStyleResource(resource);
356
+ }
357
+ return resource;
358
+ }
359
+ return null;
360
+ }
361
+ case 'preload': {
362
+ if (__DEV__) {
363
+ validateLinkPropsForPreloadResource(pendingProps);
364
+ }
365
+ const {href, as} = pendingProps;
366
+ if (typeof href === 'string' && isResourceAsType(as)) {
367
+ // We've asserted all the specific types for PreloadQualifyingProps
368
+ const preloadRawProps: PreloadQualifyingProps = (pendingProps: any);
369
+ let resource = preloadResources.get(href);
370
+ if (resource) {
371
+ if (__DEV__) {
372
+ const originallyImplicit =
373
+ (resource: any)._dev_implicit_construction === true;
374
+ const latestProps = preloadPropsFromRawProps(preloadRawProps);
375
+ validatePreloadResourceDifference(
376
+ resource.props,
377
+ originallyImplicit,
378
+ latestProps,
379
+ false,
380
+ );
381
+ }
382
+ } else {
383
+ const resourceProps = preloadPropsFromRawProps(preloadRawProps);
384
+ resource = createPreloadResource(
385
+ currentDocument,
386
+ href,
387
+ resourceProps,
388
+ );
389
+ }
390
+ return resource;
391
+ }
392
+ return null;
393
+ }
394
+ default: {
395
+ if (__DEV__) {
396
+ validateUnmatchedLinkResourceProps(pendingProps, currentProps);
397
+ }
398
+ return null;
399
+ }
400
+ }
401
+ }
402
+ default: {
403
+ throw new Error(
404
+ `getResource encountered a resource type it did not expect: "${type}". this is a bug in React.`,
405
+ );
406
+ }
407
+ }
408
+}
409
+
410
+function preloadPropsFromRawProps(
411
+ rawBorrowedProps: PreloadQualifyingProps,
412
+): PreloadProps {
413
+ return Object.assign({}, rawBorrowedProps);
414
+}
415
+
416
+function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
417
+ const props: StyleProps = Object.assign({}, rawProps);
418
+ props['data-rprec'] = rawProps.precedence;
419
+ props.precedence = null;
420
+
421
+ return props;
422
+}
423
+
424
+// --------------------------------------
425
+// Resource Reconciliation
426
+// --------------------------------------
427
+
428
+export function acquireResource(resource: Resource): Instance {
429
+ switch (resource.type) {
430
+ case 'style': {
431
+ return acquireStyleResource(resource);
432
+ }
433
+ case 'preload': {
434
+ return resource.instance;
435
+ }
436
+ default: {
437
+ throw new Error(
438
+ `acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
439
+ );
440
+ }
441
+ }
442
+}
443
+
444
+export function releaseResource(resource: Resource) {
445
+ switch (resource.type) {
446
+ case 'style': {
447
+ resource.count--;
448
+ }
449
+ }
450
+}
451
+
452
+function createResourceInstance(
453
+ type: string,
454
+ props: Object,
455
+ ownerDocument: Document,
456
+): Instance {
457
+ const element = createElement(type, props, ownerDocument, HTML_NAMESPACE);
458
+ setInitialProperties(element, type, props);
459
+ return element;
460
+}
461
+
462
+function createStyleResource(
463
+ ownerDocument: Document,
464
+ href: string,
465
+ precedence: string,
466
+ props: StyleProps,
467
+): StyleResource {
468
+ if (__DEV__) {
469
+ if (styleResources.has(href)) {
470
+ console.error(
471
+ 'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
472
+ );
473
+ }
474
+ }
475
+
476
+ const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
477
+ href,
478
+ );
479
+ const existingEl = ownerDocument.querySelector(
480
+ `link[rel="stylesheet"][href="${limitedEscapedHref}"]`,
481
+ );
482
+ const resource = {
483
+ type: 'style',
484
+ count: 0,
485
+ href,
486
+ precedence,
487
+ props,
488
+ hint: null,
489
+ preloaded: false,
490
+ loaded: false,
491
+ error: false,
492
+ ownerDocument,
493
+ instance: null,
494
+ };
495
+ styleResources.set(href, resource);
496
+
497
+ if (existingEl) {
498
+ // If we have an existing element in the DOM we don't need to preload this resource nor can we
499
+ // adopt props from any preload that might exist already for this resource. We do need to try
500
+ // to reify the Resource loading state the best we can.
501
+ const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
502
+ if (loadingState) {
503
+ switch (loadingState.s) {
504
+ case 'l': {
505
+ resource.loaded = true;
506
+ break;
507
+ }
508
+ case 'e': {
509
+ resource.error = true;
510
+ break;
511
+ }
512
+ default: {
513
+ attachLoadListeners(existingEl, resource);
514
+ }
515
+ }
516
+ } else {
517
+ // This is unfortunately just an assumption. The rationale here is that stylesheets without
518
+ // a loading state must have been flushed in the shell and would have blocked until loading
519
+ // or error. we can't know afterwards which happened for all types of stylesheets (cross origin)
520
+ // for instance) and the techniques for determining if a sheet has loaded that we do have still
521
+ // fail if the sheet loaded zero rules. At the moment we are going to just opt to assume the
522
+ // sheet is loaded if it was flushed in the shell
523
+ resource.loaded = true;
524
+ }
525
+ } else {
526
+ const hint = preloadResources.get(href);
527
+ if (hint) {
528
+ resource.hint = hint;
529
+ // If a preload for this style Resource already exists there are certain props we want to adopt
530
+ // on the style Resource, primarily focussed on making sure the style network pathways utilize
531
+ // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
532
+ // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
533
+ const preloadProps = hint.props;
534
+ adoptPreloadProps(resource.props, hint.props);
535
+ if (__DEV__) {
536
+ (resource: any)._dev_preload_props = preloadProps;
537
+ }
538
+ }
539
+ }
540
+
541
+ return resource;
542
+}
543
+
544
+function adoptPreloadProps(
545
+ styleProps: StyleProps,
546
+ preloadProps: PreloadProps,
547
+): void {
548
+ if (styleProps.crossOrigin == null)
549
+ styleProps.crossOrigin = preloadProps.crossOrigin;
550
+ if (styleProps.referrerPolicy == null)
551
+ styleProps.referrerPolicy = preloadProps.referrerPolicy;
552
+ if (styleProps.media == null) styleProps.media = preloadProps.media;
553
+ if (styleProps.title == null) styleProps.title = preloadProps.title;
554
+}
555
+
556
+function immediatelyPreloadStyleResource(resource: StyleResource) {
557
+ // This function must be called synchronously after creating a styleResource otherwise it may
558
+ // violate assumptions around the existence of a preload. The reason it is extracted out is we
559
+ // don't always want to preload a style, in particular when we are going to synchronously insert
560
+ // that style. We confirm the style resource has no preload already and then construct it. If
561
+ // we wait and call this later it is possible a preload will already exist for this href
562
+ if (resource.loaded === false && resource.hint === null) {
563
+ const {href, props} = resource;
564
+ const preloadProps = preloadPropsFromStyleProps(props);
565
+ resource.hint = createPreloadResource(
566
+ resource.ownerDocument,
567
+ href,
568
+ preloadProps,
569
+ );
570
+ }
571
+}
572
+
573
+function preloadPropsFromStyleProps(props: StyleProps): PreloadProps {
574
+ return {
575
+ rel: 'preload',
576
+ as: 'style',
577
+ href: props.href,
578
+ crossOrigin: props.crossOrigin,
579
+ integrity: props.integrity,
580
+ media: props.media,
581
+ hrefLang: props.hrefLang,
582
+ referrerPolicy: props.referrerPolicy,
583
+ };
584
+}
585
+
586
+function createPreloadResource(
587
+ ownerDocument: Document,
588
+ href: string,
589
+ props: PreloadProps,
590
+): PreloadResource {
591
+ const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
592
+ href,
593
+ );
594
+ let element = ownerDocument.querySelector(
595
+ `link[rel="preload"][href="${limitedEscapedHref}"]`,
596
+ );
597
+ if (!element) {
598
+ element = createResourceInstance('link', props, ownerDocument);
599
+ insertPreloadInstance(element, ownerDocument);
600
+ }
601
+ return {
602
+ type: 'preload',
603
+ href: href,
604
+ ownerDocument,
605
+ props,
606
+ instance: element,
607
+ };
608
+}
609
+
610
+function acquireStyleResource(resource: StyleResource): Instance {
611
+ if (!resource.instance) {
612
+ const {props, ownerDocument, precedence} = resource;
613
+ const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
614
+ props.href,
615
+ );
616
+ const existingEl = ownerDocument.querySelector(
617
+ `link[rel="stylesheet"][data-rprec][href="${limitedEscapedHref}"]`,
618
+ );
619
+ if (existingEl) {
620
+ resource.instance = existingEl;
621
+ resource.preloaded = true;
622
+ const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
623
+ if (loadingState) {
624
+ // if an existingEl is found there should always be a loadingState because if
625
+ // the resource was flushed in the head it should have already been found when
626
+ // the resource was first created. Still defensively we gate this
627
+ switch (loadingState.s) {
628
+ case 'l': {
629
+ resource.loaded = true;
630
+ resource.error = false;
631
+ break;
632
+ }
633
+ case 'e': {
634
+ resource.error = true;
635
+ break;
636
+ }
637
+ default: {
638
+ attachLoadListeners(existingEl, resource);
639
+ }
640
+ }
641
+ } else {
642
+ resource.loaded = true;
643
+ }
644
+ } else {
645
+ const instance = createResourceInstance(
646
+ 'link',
647
+ resource.props,
648
+ ownerDocument,
649
+ );
650
+
651
+ attachLoadListeners(instance, resource);
652
+ insertStyleInstance(instance, precedence, ownerDocument);
653
+ resource.instance = instance;
654
+ }
655
+ }
656
+ resource.count++;
657
+ return resource.instance;
658
+}
659
+
660
+function attachLoadListeners(instance: Instance, resource: StyleResource) {
661
+ const listeners = {};
662
+ listeners.load = onResourceLoad.bind(
663
+ null,
664
+ instance,
665
+ resource,
666
+ listeners,
667
+ loadAndErrorEventListenerOptions,
668
+ );
669
+ listeners.error = onResourceError.bind(
670
+ null,
671
+ instance,
672
+ resource,
673
+ listeners,
674
+ loadAndErrorEventListenerOptions,
675
+ );
676
+
677
+ instance.addEventListener(
678
+ 'load',
679
+ listeners.load,
680
+ loadAndErrorEventListenerOptions,
681
+ );
682
+ instance.addEventListener(
683
+ 'error',
684
+ listeners.error,
685
+ loadAndErrorEventListenerOptions,
686
+ );
687
+}
688
+
689
+const loadAndErrorEventListenerOptions = {
690
+ passive: true,
691
+};
692
+
693
+function onResourceLoad(
694
+ instance: Instance,
695
+ resource: StyleResource,
696
+ listeners: {[string]: () => mixed},
697
+ listenerOptions: typeof loadAndErrorEventListenerOptions,
698
+) {
699
+ resource.loaded = true;
700
+ resource.error = false;
701
+ for (const event in listeners) {
702
+ instance.removeEventListener(event, listeners[event], listenerOptions);
703
+ }
704
+}
705
+
706
+function onResourceError(
707
+ instance: Instance,
708
+ resource: StyleResource,
709
+ listeners: {[string]: () => mixed},
710
+ listenerOptions: typeof loadAndErrorEventListenerOptions,
711
+) {
712
+ resource.loaded = false;
713
+ resource.error = true;
714
+ for (const event in listeners) {
715
+ instance.removeEventListener(event, listeners[event], listenerOptions);
716
+ }
717
+}
718
+
719
+function insertStyleInstance(
720
+ instance: Instance,
721
+ precedence: string,
722
+ ownerDocument: Document,
723
+): void {
724
+ const nodes = ownerDocument.querySelectorAll(
725
+ 'link[rel="stylesheet"][data-rprec]',
726
+ );
727
+ const last = nodes.length ? nodes[nodes.length - 1] : null;
728
+ let prior = last;
729
+ for (let i = 0; i < nodes.length; i++) {
730
+ const node = nodes[i];
731
+ const nodePrecedence = node.dataset.rprec;
732
+ if (nodePrecedence === precedence) {
733
+ prior = node;
734
+ } else if (prior !== last) {
735
+ break;
736
+ }
737
+ }
738
+ if (prior) {
739
+ // We get the prior from the document so we know it is in the tree.
740
+ // We also know that links can't be the topmost Node so the parentNode
741
+ // must exist.
742
+ ((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
743
+ } else {
744
+ // @TODO call getRootNode on root.container. if it is a Document, insert into head
745
+ // if it is a ShadowRoot insert it into the root node.
746
+ const parent = ownerDocument.head;
747
+ if (parent) {
748
+ parent.insertBefore(instance, parent.firstChild);
749
+ } else {
750
+ throw new Error(
751
+ 'While attempting to insert a Resource, React expected the Document to contain' +
752
+ ' a head element but it was not found.',
753
+ );
754
+ }
755
+ }
756
+}
757
+
758
+function insertPreloadInstance(
759
+ instance: Instance,
760
+ ownerDocument: Document,
761
+): void {
762
+ if (!ownerDocument.contains(instance)) {
763
+ const parent = ownerDocument.head;
764
+ if (parent) {
765
+ parent.appendChild(instance);
766
+ } else {
767
+ throw new Error(
768
+ 'While attempting to insert a Resource, React expected the Document to contain' +
769
+ ' a head element but it was not found.',
770
+ );
771
+ }
772
+ }
773
+}
774
+
775
+export function isHostResourceType(type: string, props: Props): boolean {
776
+ switch (type) {
777
+ case 'link': {
778
+ switch (props.rel) {
779
+ case 'stylesheet': {
780
+ if (__DEV__) {
781
+ validateLinkPropsForStyleResource(props);
782
+ }
783
+ const {href, precedence, onLoad, onError, disabled} = props;
784
+ return (
785
+ typeof href === 'string' &&
786
+ typeof precedence === 'string' &&
787
+ !onLoad &&
788
+ !onError &&
789
+ disabled == null
790
+ );
791
+ }
792
+ case 'preload': {
793
+ if (__DEV__) {
794
+ validateLinkPropsForStyleResource(props);
795
+ }
796
+ const {href, as, onLoad, onError} = props;
797
+ return (
798
+ !onLoad &&
799
+ !onError &&
800
+ typeof href === 'string' &&
801
+ isResourceAsType(as)
802
+ );
803
+ }
804
+ }
805
+ }
806
+ }
807
+ return false;
808
+}
809
+
810
+function isResourceAsType(as: mixed): boolean {
811
+ return as === 'style' || as === 'font';
812
+}
813
+
814
+// When passing user input into querySelector(All) the embedded string must not alter
815
+// the semantics of the query. This escape function is safe to use when we know the
816
+// provided value is going to be wrapped in double quotes as part of an attribute selector
817
+// Do not use it anywhere else
818
+// we escape double quotes and backslashes
819
+const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g;
820
+function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string {
821
+ return value.replace(
822
+ escapeSelectorAttributeValueInsideDoubleQuotesRegex,
823
+ ch => '\\' + ch.charCodeAt(0).toString(16),
824
+ );
825
+}
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+56
-64
@@ -67,7 +67,11 @@ import {
67
enableScopeAPI,
68
enableFloat,
69
} from 'shared/ReactFeatureFlags';
70
-import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
70
+import {
71
+ HostComponent,
72
+ HostResource,
73
+ HostText,
74
+} from 'react-reconciler/src/ReactWorkTags';
75
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
76
77
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
@@ -75,6 +79,12 @@ import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
79
// TODO: Remove this deep import when we delete the legacy root API
80
import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
81
82
+import {
83
+ prepareToRenderResources,
84
+ cleanupAfterRenderResources,
85
+ isHostResourceType,
86
+} from './ReactDOMFloatClient';
87
+
88
export type Type = string;
89
export type Props = {
90
autoFocus?: boolean,
@@ -680,14 +690,6 @@ export function clearContainer(container: Container): void {
690
691
export const supportsHydration = true;
692
683
-export function isHydratableResource(type: string, props: Props): boolean {
684
- return (
685
- type === 'link' &&
686
- typeof (props: any).precedence === 'string' &&
687
- (props: any).rel === 'stylesheet'
688
- );
689
-}
690
-
693
export function canHydrateInstance(
694
instance: HydratableInstance,
695
type: string,
@@ -761,18 +763,6 @@ export function getSuspenseInstanceFallbackErrorDetails(
763
digest,
764
};
765
}
764
-
765
- // let value = {message: undefined, hash: undefined};
766
- // const nextSibling = instance.nextSibling;
767
- // if (nextSibling) {
768
- // const dataset = ((nextSibling: any): HTMLTemplateElement).dataset;
769
- // value.message = dataset.msg;
770
- // value.hash = dataset.hash;
771
- // if (__DEV__) {
772
- // value.stack = dataset.stack;
773
- // }
774
- // }
775
- // return value;
766
}
767
768
export function registerSuspenseInstanceRetry(
@@ -788,15 +778,11 @@ function getNextHydratable(node) {
778
const nodeType = node.nodeType;
779
if (enableFloat) {
780
if (nodeType === ELEMENT_NODE) {
791
- if (
792
- ((node: any): Element).tagName === 'LINK' &&
793
- ((node: any): Element).hasAttribute('data-rprec')
794
- ) {
781
+ if (isHostResourceInstance(((node: any): Element))) {
782
continue;
783
}
784
break;
798
- }
799
- if (nodeType === TEXT_NODE) {
785
+ } else if (nodeType === TEXT_NODE) {
786
break;
787
}
788
} else {
@@ -903,43 +889,6 @@ export function hydrateSuspenseInstance(
889
precacheFiberNode(internalInstanceHandle, suspenseInstance);
890
}
891
906
-export function getMatchingResourceInstance(
907
- type: string,
908
- props: Props,
909
- rootHostContainer: Container,
910
-): ?Instance {
911
- if (enableFloat) {
912
- switch (type) {
913
- case 'link': {
914
- if (typeof (props: any).href !== 'string') {
915
- return null;
916
- }
917
- const selector = `link[rel="stylesheet"][data-rprec][href="${
918
- (props: any).href
919
- }"]`;
920
- const link = getOwnerDocumentFromRootContainer(
921
- rootHostContainer,
922
- ).querySelector(selector);
923
- if (__DEV__) {
924
- const allLinks = getOwnerDocumentFromRootContainer(
925
- rootHostContainer,
926
- ).querySelectorAll(selector);
927
- if (allLinks.length > 1) {
928
- console.error(
929
- 'Stylesheet resources need a unique representation in the DOM while hydrating' +
930
- ' and more than one matching DOM Node was found. To fix, ensure you are only' +
931
- ' rendering one stylesheet link with an href attribute of "%s".',
932
- (props: any).href,
933
- );
934
- }
935
- }
936
- return link;
937
- }
938
- }
939
- }
940
- return null;
941
-}
942
-
892
export function getNextHydratableInstanceAfterSuspenseInstance(
893
suspenseInstance: SuspenseInstance,
894
): null | HydratableInstance {
@@ -1281,6 +1230,7 @@ export function matchAccessibilityRole(node: Instance, role: string): boolean {
1230
1231
export function getTextContent(fiber: Fiber): string | null {
1232
switch (fiber.tag) {
1233
+ case HostResource:
1234
case HostComponent:
1235
let textContent = '';
1236
const childNodes = fiber.stateNode.childNodes;
@@ -1390,3 +1340,45 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
1340
localRequestAnimationFrame(time => callback(time));
1341
});
1342
}
1343
+// -------------------
1344
+// Resources
1345
+// -------------------
1346
+
1347
+export const supportsResources = true;
1348
+
1349
+export {isHostResourceType};
1350
+function isHostResourceInstance(instance: Instance | Container): boolean {
1351
+ if (instance.nodeType === ELEMENT_NODE) {
1352
+ switch (instance.tagName.toLowerCase()) {
1353
+ case 'link': {
1354
+ const rel = ((instance: any): HTMLLinkElement).rel;
1355
+ return (
1356
+ rel === 'preload' ||
1357
+ (rel === 'stylesheet' && instance.hasAttribute('data-rprec'))
1358
+ );
1359
+ }
1360
+ default: {
1361
+ return false;
1362
+ }
1363
+ }
1364
+ }
1365
+ return false;
1366
+}
1367
+
1368
+export function prepareRendererToRender(rootContainer: Container) {
1369
+ if (enableFloat) {
1370
+ prepareToRenderResources(getOwnerDocumentFromRootContainer(rootContainer));
1371
+ }
1372
+}
1373
+
1374
+export function resetRendererAfterRender() {
1375
+ if (enableFloat) {
1376
+ cleanupAfterRenderResources();
1377
+ }
1378
+}
1379
+
1380
+export {
1381
+ getResource,
1382
+ acquireResource,
1383
+ releaseResource,
1384
+} from './ReactDOMFloatClient';
packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
+19
-4
@@ -33,6 +33,7 @@ import {
33
HostRoot,
34
HostPortal,
35
HostComponent,
36
+ HostResource,
37
HostText,
38
ScopeComponent,
39
} from 'react-reconciler/src/ReactWorkTags';
@@ -52,6 +53,7 @@ import {
53
enableLegacyFBSupport,
54
enableCreateEventHandleAPI,
55
enableScopeAPI,
56
+ enableFloat,
57
} from 'shared/ReactFeatureFlags';
58
import {
59
invokeGuardedCallbackAndCatchFirstError,
@@ -621,7 +623,11 @@ export function dispatchEventForPluginEventSystem(
623
return;
624
}
625
const parentTag = parentNode.tag;
624
- if (parentTag === HostComponent || parentTag === HostText) {
626
+ if (
627
+ parentTag === HostComponent ||
628
+ parentTag === HostText ||
629
+ (enableFloat ? parentTag === HostResource : false)
630
+ ) {
631
node = ancestorInst = parentNode;
632
continue mainLoop;
633
}
@@ -675,7 +681,10 @@ export function accumulateSinglePhaseListeners(
681
while (instance !== null) {
682
const {stateNode, tag} = instance;
683
// Handle listeners that are on HostComponents (i.e. <div>)
678
- if (tag === HostComponent && stateNode !== null) {
684
+ if (
685
+ (tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
686
+ stateNode !== null
687
+ ) {
688
lastHostComponent = stateNode;
689
690
// createEventHandle listeners
@@ -786,7 +795,10 @@ export function accumulateTwoPhaseListeners(
795
while (instance !== null) {
796
const {stateNode, tag} = instance;
797
// Handle listeners that are on HostComponents (i.e. <div>)
789
- if (tag === HostComponent && stateNode !== null) {
798
+ if (
799
+ (tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
800
+ stateNode !== null
801
+ ) {
802
const currentTarget = stateNode;
803
const captureListener = getListener(instance, captureName);
804
if (captureListener != null) {
@@ -883,7 +895,10 @@ function accumulateEnterLeaveListenersForEvent(
895
if (alternate !== null && alternate === common) {
896
break;
897
}
886
- if (tag === HostComponent && stateNode !== null) {
898
+ if (
899
+ (tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
900
+ stateNode !== null
901
+ ) {
902
const currentTarget = stateNode;
903
if (inCapturePhase) {
904
const captureListener = getListener(instance, registrationName);
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
new
+575
@@ -0,0 +1,575 @@
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 {
11
+ validatePreloadResourceDifference,
12
+ validateStyleResourceDifference,
13
+ validateStyleAndHintProps,
14
+ validateLinkPropsForStyleResource,
15
+ validateLinkPropsForPreloadResource,
16
+ validatePreloadArguments,
17
+ validatePreinitArguments,
18
+} from '../shared/ReactDOMResourceValidation';
19
+
20
+type Props = {[string]: mixed};
21
+
22
+type ResourceType = 'style' | 'font';
23
+
24
+type PreloadProps = {
25
+ rel: 'preload',
26
+ as: ResourceType,
27
+ href: string,
28
+ [string]: mixed,
29
+};
30
+type PreloadResource = {
31
+ type: 'preload',
32
+ as: ResourceType,
33
+ href: string,
34
+ props: PreloadProps,
35
+ flushed: boolean,
36
+};
37
+
38
+type StyleProps = {
39
+ rel: 'stylesheet',
40
+ href: string,
41
+ 'data-rprec': string,
42
+ [string]: mixed,
43
+};
44
+type StyleResource = {
45
+ type: 'style',
46
+ href: string,
47
+ precedence: string,
48
+ props: StyleProps,
49
+
50
+ flushed: boolean,
51
+ inShell: boolean, // flushedInShell
52
+ hint: PreloadResource,
53
+};
54
+
55
+export type Resource = PreloadResource | StyleResource;
56
+
57
+export type Resources = {
58
+ // Request local cache
59
+ preloadsMap: Map<string, PreloadResource>,
60
+ stylesMap: Map<string, StyleResource>,
61
+
62
+ // Flushing queues for Resource dependencies
63
+ explicitPreloads: Set<PreloadResource>,
64
+ implicitPreloads: Set<PreloadResource>,
65
+ precedences: Map<string, Set<StyleResource>>,
66
+
67
+ // Module-global-like reference for current boundary resources
68
+ boundaryResources: ?BoundaryResources,
69
+};
70
+
71
+// @TODO add bootstrap script to implicit preloads
72
+export function createResources(): Resources {
73
+ return {
74
+ // persistent
75
+ preloadsMap: new Map(),
76
+ stylesMap: new Map(),
77
+
78
+ // cleared on flush
79
+ explicitPreloads: new Set(),
80
+ implicitPreloads: new Set(),
81
+ precedences: new Map(),
82
+
83
+ // like a module global for currently rendering boundary
84
+ boundaryResources: null,
85
+ };
86
+}
87
+
88
+export type BoundaryResources = Set<StyleResource>;
89
+
90
+export function createBoundaryResources(): BoundaryResources {
91
+ return new Set();
92
+}
93
+
94
+export function mergeBoundaryResources(
95
+ target: BoundaryResources,
96
+ source: BoundaryResources,
97
+) {
98
+ source.forEach(resource => target.add(resource));
99
+}
100
+
101
+let currentResources: null | Resources = null;
102
+const currentResourcesStack = [];
103
+
104
+export function prepareToRenderResources(resources: Resources) {
105
+ currentResourcesStack.push(currentResources);
106
+ currentResources = resources;
107
+}
108
+
109
+export function finishRenderingResources() {
110
+ currentResources = currentResourcesStack.pop();
111
+}
112
+
113
+export function setCurrentlyRenderingBoundaryResourcesTarget(
114
+ resources: Resources,
115
+ boundaryResources: null | BoundaryResources,
116
+) {
117
+ resources.boundaryResources = boundaryResources;
118
+}
119
+
120
+export const ReactDOMServerDispatcher = {
121
+ preload,
122
+ preinit,
123
+};
124
+
125
+type PreloadAs = ResourceType;
126
+type PreloadOptions = {as: PreloadAs, crossOrigin?: string};
127
+function preload(href: string, options: PreloadOptions) {
128
+ if (!currentResources) {
129
+ // While we expect that preload calls are primarily going to be observed
130
+ // during render because effects and events don't run on the server it is
131
+ // still possible that these get called in module scope. This is valid on
132
+ // the client since there is still a document to interact with but on the
133
+ // server we need a request to associate the call to. Because of this we
134
+ // simply return and do not warn.
135
+ return;
136
+ }
137
+ if (__DEV__) {
138
+ validatePreloadArguments(href, options);
139
+ }
140
+ if (
141
+ typeof href === 'string' &&
142
+ href &&
143
+ typeof options === 'object' &&
144
+ options !== null
145
+ ) {
146
+ const as = options.as;
147
+ let resource = currentResources.preloadsMap.get(href);
148
+ if (resource) {
149
+ if (__DEV__) {
150
+ const originallyImplicit =
151
+ (resource: any)._dev_implicit_construction === true;
152
+ const latestProps = preloadPropsFromPreloadOptions(href, as, options);
153
+ validatePreloadResourceDifference(
154
+ resource.props,
155
+ originallyImplicit,
156
+ latestProps,
157
+ false,
158
+ );
159
+ }
160
+ } else {
161
+ resource = createPreloadResource(
162
+ currentResources,
163
+ href,
164
+ as,
165
+ preloadPropsFromPreloadOptions(href, as, options),
166
+ );
167
+ }
168
+ captureExplicitPreloadResourceDependency(currentResources, resource);
169
+ }
170
+}
171
+
172
+type PreinitAs = 'style';
173
+type PreinitOptions = {
174
+ as: PreinitAs,
175
+ precedence?: string,
176
+ crossOrigin?: string,
177
+};
178
+function preinit(href: string, options: PreinitOptions) {
179
+ if (!currentResources) {
180
+ // While we expect that preinit calls are primarily going to be observed
181
+ // during render because effects and events don't run on the server it is
182
+ // still possible that these get called in module scope. This is valid on
183
+ // the client since there is still a document to interact with but on the
184
+ // server we need a request to associate the call to. Because of this we
185
+ // simply return and do not warn.
186
+ return;
187
+ }
188
+ if (__DEV__) {
189
+ validatePreinitArguments(href, options);
190
+ }
191
+ if (
192
+ typeof href === 'string' &&
193
+ href &&
194
+ typeof options === 'object' &&
195
+ options !== null
196
+ ) {
197
+ const as = options.as;
198
+ switch (as) {
199
+ case 'style': {
200
+ const precedence = options.precedence || 'default';
201
+
202
+ let resource = currentResources.stylesMap.get(href);
203
+ if (resource) {
204
+ if (__DEV__) {
205
+ const latestProps = stylePropsFromPreinitOptions(
206
+ href,
207
+ precedence,
208
+ options,
209
+ );
210
+ validateStyleResourceDifference(resource.props, latestProps);
211
+ }
212
+ } else {
213
+ const resourceProps = stylePropsFromPreinitOptions(
214
+ href,
215
+ precedence,
216
+ options,
217
+ );
218
+ resource = createStyleResource(
219
+ currentResources,
220
+ href,
221
+ precedence,
222
+ resourceProps,
223
+ );
224
+ }
225
+
226
+ // Do not associate preinit style resources with any specific boundary regardless of where it is called
227
+ captureStyleResourceDependency(currentResources, null, resource);
228
+
229
+ return;
230
+ }
231
+ }
232
+ }
233
+}
234
+
235
+function preloadPropsFromPreloadOptions(
236
+ href: string,
237
+ as: ResourceType,
238
+ options: PreloadOptions,
239
+): PreloadProps {
240
+ return {
241
+ href,
242
+ rel: 'preload',
243
+ as,
244
+ crossOrigin: as === 'font' ? '' : options.crossOrigin,
245
+ };
246
+}
247
+
248
+function preloadPropsFromRawProps(
249
+ href: string,
250
+ as: ResourceType,
251
+ rawProps: Props,
252
+): PreloadProps {
253
+ const props: PreloadProps = Object.assign({}, rawProps);
254
+ props.href = href;
255
+ props.rel = 'preload';
256
+ props.as = as;
257
+ if (as === 'font') {
258
+ // Font preloads always need CORS anonymous mode so we set it here
259
+ // regardless of the props provided. This should warn elsewhere in
260
+ // dev
261
+ props.crossOrigin = '';
262
+ }
263
+ return props;
264
+}
265
+
266
+function preloadAsStylePropsFromProps(
267
+ href: string,
268
+ props: Props | StyleProps,
269
+): PreloadProps {
270
+ return {
271
+ rel: 'preload',
272
+ as: 'style',
273
+ href: href,
274
+ crossOrigin: props.crossOrigin,
275
+ integrity: props.integrity,
276
+ media: props.media,
277
+ hrefLang: props.hrefLang,
278
+ referrerPolicy: props.referrerPolicy,
279
+ };
280
+}
281
+
282
+function createPreloadResource(
283
+ resources: Resources,
284
+ href: string,
285
+ as: ResourceType,
286
+ props: PreloadProps,
287
+): PreloadResource {
288
+ const {preloadsMap} = resources;
289
+ if (__DEV__) {
290
+ if (preloadsMap.has(href)) {
291
+ console.error(
292
+ 'createPreloadResource was called when a preload Resource matching the same href already exists. This is a bug in React.',
293
+ );
294
+ }
295
+ }
296
+
297
+ const resource = {
298
+ type: 'preload',
299
+ as,
300
+ href,
301
+ flushed: false,
302
+ props,
303
+ };
304
+ preloadsMap.set(href, resource);
305
+ return resource;
306
+}
307
+
308
+function stylePropsFromRawProps(
309
+ href: string,
310
+ precedence: string,
311
+ rawProps: Props,
312
+): StyleProps {
313
+ const props: StyleProps = Object.assign({}, rawProps);
314
+ props.href = href;
315
+ props.rel = 'stylesheet';
316
+ props['data-rprec'] = precedence;
317
+ delete props.precedence;
318
+
319
+ return props;
320
+}
321
+
322
+function stylePropsFromPreinitOptions(
323
+ href: string,
324
+ precedence: string,
325
+ options: PreinitOptions,
326
+): StyleProps {
327
+ return {
328
+ rel: 'stylesheet',
329
+ href,
330
+ 'data-rprec': precedence,
331
+ crossOrigin: options.crossOrigin,
332
+ };
333
+}
334
+
335
+function createStyleResource(
336
+ resources: Resources,
337
+ href: string,
338
+ precedence: string,
339
+ props: StyleProps,
340
+): StyleResource {
341
+ if (__DEV__) {
342
+ if (resources.stylesMap.has(href)) {
343
+ console.error(
344
+ 'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
345
+ );
346
+ }
347
+ }
348
+ const {stylesMap, preloadsMap} = resources;
349
+
350
+ let hint = preloadsMap.get(href);
351
+ if (hint) {
352
+ // If a preload for this style Resource already exists there are certain props we want to adopt
353
+ // on the style Resource, primarily focussed on making sure the style network pathways utilize
354
+ // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
355
+ // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
356
+ const preloadProps = hint.props;
357
+ if (props.crossOrigin == null) props.crossOrigin = preloadProps.crossOrigin;
358
+ if (props.referrerPolicy == null)
359
+ props.referrerPolicy = preloadProps.referrerPolicy;
360
+ if (props.media == null) props.media = preloadProps.media;
361
+ if (props.title == null) props.title = preloadProps.title;
362
+
363
+ if (__DEV__) {
364
+ validateStyleAndHintProps(
365
+ preloadProps,
366
+ props,
367
+ (hint: any)._dev_implicit_construction,
368
+ );
369
+ }
370
+ } else {
371
+ const preloadResourceProps = preloadAsStylePropsFromProps(href, props);
372
+ hint = createPreloadResource(
373
+ resources,
374
+ href,
375
+ 'style',
376
+ preloadResourceProps,
377
+ );
378
+ if (__DEV__) {
379
+ (hint: any)._dev_implicit_construction = true;
380
+ }
381
+ captureImplicitPreloadResourceDependency(resources, hint);
382
+ }
383
+
384
+ const resource = {
385
+ type: 'style',
386
+ href,
387
+ precedence,
388
+ flushed: false,
389
+ inShell: false,
390
+ props,
391
+ hint,
392
+ };
393
+ stylesMap.set(href, resource);
394
+
395
+ return resource;
396
+}
397
+
398
+function captureStyleResourceDependency(
399
+ resources: Resources,
400
+ boundaryResources: ?BoundaryResources,
401
+ styleResource: StyleResource,
402
+): void {
403
+ const {precedences} = resources;
404
+ const {precedence} = styleResource;
405
+
406
+ if (boundaryResources) {
407
+ boundaryResources.add(styleResource);
408
+ if (!precedences.has(precedence)) {
409
+ precedences.set(precedence, new Set());
410
+ }
411
+ } else {
412
+ let set = precedences.get(precedence);
413
+ if (!set) {
414
+ set = new Set();
415
+ precedences.set(precedence, set);
416
+ }
417
+ set.add(styleResource);
418
+ }
419
+}
420
+
421
+function captureExplicitPreloadResourceDependency(
422
+ resources: Resources,
423
+ preloadResource: PreloadResource,
424
+): void {
425
+ resources.explicitPreloads.add(preloadResource);
426
+}
427
+
428
+function captureImplicitPreloadResourceDependency(
429
+ resources: Resources,
430
+ preloadResource: PreloadResource,
431
+): void {
432
+ resources.implicitPreloads.add(preloadResource);
433
+}
434
+
435
+// Construct a resource from link props.
436
+export function resourcesFromLink(props: Props): boolean {
437
+ if (!currentResources) {
438
+ throw new Error(
439
+ '"currentResources" was expected to exist. This is a bug in React.',
440
+ );
441
+ }
442
+ const {rel, href} = props;
443
+ if (!href || typeof href !== 'string') {
444
+ return false;
445
+ }
446
+
447
+ switch (rel) {
448
+ case 'stylesheet': {
449
+ const {onLoad, onError, precedence, disabled} = props;
450
+ if (
451
+ typeof precedence !== 'string' ||
452
+ onLoad ||
453
+ onError ||
454
+ disabled != null
455
+ ) {
456
+ // This stylesheet is either not opted into Resource semantics or has conflicting properties which
457
+ // disqualify it for such. We can still create a preload resource to help it load faster on the
458
+ // client
459
+ if (__DEV__) {
460
+ validateLinkPropsForStyleResource(props);
461
+ }
462
+ let preloadResource = currentResources.preloadsMap.get(href);
463
+ if (!preloadResource) {
464
+ preloadResource = createPreloadResource(
465
+ currentResources,
466
+ href,
467
+ 'style',
468
+ preloadAsStylePropsFromProps(href, props),
469
+ );
470
+ if (__DEV__) {
471
+ (preloadResource: any)._dev_implicit_construction = true;
472
+ }
473
+ }
474
+ captureImplicitPreloadResourceDependency(
475
+ currentResources,
476
+ preloadResource,
477
+ );
478
+ return false;
479
+ } else {
480
+ // We are able to convert this link element to a resource exclusively. We construct the relevant Resource
481
+ // and return true indicating that this link was fully consumed.
482
+ let resource = currentResources.stylesMap.get(href);
483
+ if (resource) {
484
+ if (__DEV__) {
485
+ const resourceProps = stylePropsFromRawProps(
486
+ href,
487
+ precedence,
488
+ props,
489
+ );
490
+ validateStyleResourceDifference(resource.props, resourceProps);
491
+ }
492
+ } else {
493
+ const resourceProps = stylePropsFromRawProps(href, precedence, props);
494
+ resource = createStyleResource(
495
+ currentResources,
496
+ href,
497
+ precedence,
498
+ resourceProps,
499
+ );
500
+ }
501
+ captureStyleResourceDependency(
502
+ currentResources,
503
+ currentResources.boundaryResources,
504
+ resource,
505
+ );
506
+ return true;
507
+ }
508
+ }
509
+ case 'preload': {
510
+ const {as, onLoad, onError} = props;
511
+ if (onLoad || onError) {
512
+ // these props signal an opt-out of Resource semantics. We don't warn because there is no
513
+ // conflicting opt-in like there is with Style Resources
514
+ return false;
515
+ }
516
+ switch (as) {
517
+ case 'style':
518
+ case 'font': {
519
+ if (__DEV__) {
520
+ validateLinkPropsForPreloadResource(props);
521
+ }
522
+ let resource = currentResources.preloadsMap.get(href);
523
+ if (resource) {
524
+ if (__DEV__) {
525
+ const originallyImplicit =
526
+ (resource: any)._dev_implicit_construction === true;
527
+ const latestProps = preloadPropsFromRawProps(href, as, props);
528
+ validatePreloadResourceDifference(
529
+ resource.props,
530
+ originallyImplicit,
531
+ latestProps,
532
+ false,
533
+ );
534
+ }
535
+ } else {
536
+ resource = createPreloadResource(
537
+ currentResources,
538
+ href,
539
+ as,
540
+ preloadPropsFromRawProps(href, as, props),
541
+ );
542
+ }
543
+ captureExplicitPreloadResourceDependency(currentResources, resource);
544
+ return true;
545
+ }
546
+ }
547
+ return false;
548
+ }
549
+ }
550
+ return false;
551
+}
552
+
553
+export function hoistResources(
554
+ resources: Resources,
555
+ source: BoundaryResources,
556
+): void {
557
+ if (resources.boundaryResources) {
558
+ mergeBoundaryResources(resources.boundaryResources, source);
559
+ source.clear();
560
+ }
561
+}
562
+
563
+export function hoistResourcesToRoot(
564
+ resources: Resources,
565
+ boundaryResources: BoundaryResources,
566
+): void {
567
+ boundaryResources.forEach(resource => {
568
+ // all precedences are set upon discovery. so we know we will have a set here
569
+ const set: Set<StyleResource> = (resources.precedences.get(
570
+ resource.precedence,
571
+ ): any);
572
+ set.add(resource);
573
+ });
574
+ boundaryResources.clear();
575
+}
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+587
-78
@@ -8,6 +8,8 @@
8
*/
9
10
import type {ReactNodeList} from 'shared/ReactTypes';
11
+import type {Resources, BoundaryResources} from './ReactDOMFloatServer';
12
+export type {Resources, BoundaryResources};
13
14
import {
15
checkHtmlStringCoercion,
@@ -58,6 +60,36 @@ import hasOwnProperty from 'shared/hasOwnProperty';
60
import sanitizeURL from '../shared/sanitizeURL';
61
import isArray from 'shared/isArray';
62
63
+import {
64
+ prepareToRenderResources,
65
+ finishRenderingResources,
66
+ resourcesFromLink,
67
+ ReactDOMServerDispatcher,
68
+} from './ReactDOMFloatServer';
69
+export {
70
+ createResources,
71
+ createBoundaryResources,
72
+ setCurrentlyRenderingBoundaryResourcesTarget,
73
+ hoistResources,
74
+ hoistResourcesToRoot,
75
+} from './ReactDOMFloatServer';
76
+
77
+import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
78
+const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
79
+
80
+export function prepareToRender(resources: Resources): mixed {
81
+ prepareToRenderResources(resources);
82
+
83
+ const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
84
+ ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
85
+ return previousHostDispatcher;
86
+}
87
+
88
+export function cleanupAfterRender(previousDispatcher: mixed) {
89
+ finishRenderingResources();
90
+ ReactDOMCurrentDispatcher.current = previousDispatcher;
91
+}
92
+
93
// Used to distinguish these contexts from ones used in other renderers.
94
// E.g. this can be used to distinguish legacy renderers from this modern one.
95
export const isPrimaryRenderer = true;
@@ -73,7 +105,8 @@ export type ResponseState = {
105
nextSuspenseID: number,
106
sentCompleteSegmentFunction: boolean,
107
sentCompleteBoundaryFunction: boolean,
76
- sentClientRenderFunction: boolean, // We allow the legacy renderer to extend this object.
108
+ sentClientRenderFunction: boolean,
109
+ sentStyleInsertionFunction: boolean, // We allow the legacy renderer to extend this object.
110
...
111
};
112
@@ -183,6 +216,7 @@ export function createResponseState(
216
sentCompleteSegmentFunction: false,
217
sentCompleteBoundaryFunction: false,
218
sentClientRenderFunction: false,
219
+ sentStyleInsertionFunction: false,
220
};
221
}
222
@@ -1088,6 +1122,26 @@ function pushLink(
1122
target: Array<Chunk | PrecomputedChunk>,
1123
props: Object,
1124
responseState: ResponseState,
1125
+ textEmbedded: boolean,
1126
+): ReactNodeList {
1127
+ if (enableFloat && resourcesFromLink(props)) {
1128
+ if (textEmbedded) {
1129
+ // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1130
+ // to be safe and assume text will follow by inserting a textSeparator
1131
+ target.push(textSeparator);
1132
+ }
1133
+ // We have converted this link exclusively to a resource and no longer
1134
+ // need to emit it
1135
+ return null;
1136
+ }
1137
+
1138
+ return pushLinkImpl(target, props, responseState);
1139
+}
1140
+
1141
+function pushLinkImpl(
1142
+ target: Array<Chunk | PrecomputedChunk>,
1143
+ props: Object,
1144
+ responseState: ResponseState,
1145
): ReactNodeList {
1146
const isStylesheet = props.rel === 'stylesheet';
1147
target.push(startChunkForTag('link'));
@@ -1106,15 +1160,9 @@ function pushLink(
1160
'use `dangerouslySetInnerHTML`.',
1161
);
1162
case 'precedence': {
1109
- if (isStylesheet) {
1110
- if (propValue === true || typeof propValue === 'string') {
1111
- pushAttribute(target, responseState, 'data-rprec', propValue);
1112
- } else if (__DEV__) {
1113
- throw new Error(
1114
- `the "precedence" prop for links to stylesheets expects to receive a string but received something of type "${typeof propValue}" instead.`,
1115
- );
1116
- }
1117
- break;
1163
+ if (enableFloat && isStylesheet) {
1164
+ // precedence is a reversed property for stylesheets to opt-into resource semantcs
1165
+ continue;
1166
}
1167
// intentionally fall through
1168
}
@@ -1270,10 +1318,12 @@ function pushStartHead(
1318
tag: string,
1319
responseState: ResponseState,
1320
): ReactNodeList {
1273
- // Preamble type is nullable for feature off cases but is guaranteed when feature is on
1274
- target = enableFloat ? preamble : target;
1275
-
1276
- return pushStartGenericElement(target, props, tag, responseState);
1321
+ return pushStartGenericElement(
1322
+ enableFloat ? preamble : target,
1323
+ props,
1324
+ tag,
1325
+ responseState,
1326
+ );
1327
}
1328
1329
function pushStartHtml(
@@ -1281,12 +1331,10 @@ function pushStartHtml(
1331
preamble: Array<Chunk | PrecomputedChunk>,
1332
props: Object,
1333
tag: string,
1284
- formatContext: FormatContext,
1334
responseState: ResponseState,
1335
+ formatContext: FormatContext,
1336
): ReactNodeList {
1287
- // Preamble type is nullable for feature off cases but is guaranteed when feature is on
1337
target = enableFloat ? preamble : target;
1289
-
1338
if (formatContext.insertionMode === ROOT_HTML_MODE) {
1339
// If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
1340
// then we also emit the DOCTYPE as part of the root content as a convenience for
@@ -1517,6 +1565,7 @@ export function pushStartInstance(
1565
props: Object,
1566
responseState: ResponseState,
1567
formatContext: FormatContext,
1568
+ textEmbedded: boolean,
1569
): ReactNodeList {
1570
if (__DEV__) {
1571
validateARIAProperties(type, props);
@@ -1570,7 +1619,7 @@ export function pushStartInstance(
1619
case 'title':
1620
return pushStartTitle(target, props, responseState);
1621
case 'link':
1573
- return pushLink(target, props, responseState);
1622
+ return pushLink(target, props, responseState, textEmbedded);
1623
// Newline eating tags
1624
case 'listing':
1625
case 'pre': {
@@ -1613,8 +1662,8 @@ export function pushStartInstance(
1662
preamble,
1663
props,
1664
type,
1616
- formatContext,
1665
responseState,
1666
+ formatContext,
1667
);
1668
}
1669
default: {
@@ -1658,17 +1707,24 @@ export function pushEndInstance(
1707
case 'track':
1708
case 'wbr': {
1709
// No close tag needed.
1661
- break;
1710
+ return;
1711
}
1712
// Postamble end tags
1664
- case 'body':
1665
- case 'html':
1666
- target = enableFloat ? postamble : target;
1667
- // Intentional fallthrough
1668
- default: {
1669
- target.push(endTag1, stringToChunk(type), endTag2);
1713
+ case 'body': {
1714
+ if (enableFloat) {
1715
+ postamble.unshift(endTag1, stringToChunk(type), endTag2);
1716
+ return;
1717
+ }
1718
+ break;
1719
}
1720
+ case 'html':
1721
+ if (enableFloat) {
1722
+ postamble.push(endTag1, stringToChunk(type), endTag2);
1723
+ return;
1724
+ }
1725
+ break;
1726
}
1727
+ target.push(endTag1, stringToChunk(type), endTag2);
1728
}
1729
1730
export function writeCompletedRoot(
@@ -1977,7 +2033,9 @@ export function writeEndSegment(
2033
// const SUSPENSE_END_DATA = '/$';
2034
// const SUSPENSE_PENDING_START_DATA = '$?';
2035
// const SUSPENSE_FALLBACK_START_DATA = '$!';
1980
-//
2036
+// const LOADED = 'l';
2037
+// const ERRORED = 'e';
2038
+
2039
// function clientRenderBoundary(suspenseBoundaryID, errorDigest, errorMsg, errorComponentStack) {
2040
// // Find the fallback's first element.
2041
// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
@@ -2000,14 +2058,100 @@ export function writeEndSegment(
2058
// suspenseNode._reactRetry();
2059
// }
2060
// }
2003
-//
2004
-// function completeBoundary(suspenseBoundaryID, contentID) {
2005
-// // Find the fallback's first element.
2006
-// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
2061
+
2062
+// resourceMap = new Map();
2063
+// function completeBoundaryWithStyles(suspenseBoundaryID, contentID, styles) {
2064
+// const precedences = new Map();
2065
+// const thisDocument = document;
2066
+// let lastResource, node;
2067
+
2068
+// // Seed the precedence list with existing resources
2069
+// let nodes = thisDocument.querySelectorAll('link[data-rprec]');
2070
+// for (let i = 0;node = nodes[i++];) {
2071
+// precedences.set(node.dataset.rprec, lastResource = node);
2072
+// }
2073
+
2074
+// let i = 0;
2075
+// let dependencies = [];
2076
+// let style, href, precedence, attr, loadingState, resourceEl;
2077
+
2078
+// function setStatus(s) {
2079
+// this.s = s;
2080
+// }
2081
+
2082
+// while (style = styles[i++]) {
2083
+// let j = 0;
2084
+// href = style[j++];
2085
+// // We check if this resource is already in our resourceMap and reuse it if so.
2086
+// // If it is already loaded we don't return it as a depenendency since there is nothing
2087
+// // to wait for
2088
+// loadingState = resourceMap.get(href);
2089
+// if (loadingState) {
2090
+// if (loadingState.s !== 'l') {
2091
+// dependencies.push(loadingState);
2092
+// }
2093
+// continue;
2094
+// }
2095
+
2096
+// // We construct our new resource element, looping over remaining attributes if any
2097
+// // setting them to the Element.
2098
+// resourceEl = thisDocument.createElement("link");
2099
+// resourceEl.href = href;
2100
+// resourceEl.rel = 'stylesheet';
2101
+// resourceEl.dataset.rprec = precedence = style[j++];
2102
+// while(attr = style[j++]) {
2103
+// resourceEl.setAttribute(attr, style[j++]);
2104
+// }
2105
+
2106
+// // We stash a pending promise in our map by href which will resolve or reject
2107
+// // when the underlying resource loads or errors. We add it to the dependencies
2108
+// // array to be returned.
2109
+// loadingState = resourceEl._p = new Promise((re, rj) => {
2110
+// resourceEl.onload = re;
2111
+// resourceEl.onerror = rj;
2112
+// })
2113
+// loadingState.then(
2114
+// setStatus.bind(loadingState, LOADED),
2115
+// setStatus.bind(loadingState, ERRORED)
2116
+// );
2117
+// resourceMap.set(href, loadingState);
2118
+// dependencies.push(loadingState);
2119
+
2120
+// // The prior style resource is the last one placed at a given
2121
+// // precedence or the last resource itself which may be null.
2122
+// // We grab this value and then update the last resource for this
2123
+// // precedence to be the inserted element, updating the lastResource
2124
+// // pointer if needed.
2125
+// let prior = precedences.get(precedence) || lastResource;
2126
+// if (prior === lastResource) {
2127
+// lastResource = resourceEl
2128
+// }
2129
+// precedences.set(precedence, resourceEl)
2130
+
2131
+// // Finally, we insert the newly constructed instance at an appropriate location
2132
+// // in the Document.
2133
+// if (prior) {
2134
+// prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
2135
+// } else {
2136
+// let head = thisDocument.head;
2137
+// head.insertBefore(resourceEl, head.firstChild);
2138
+// }
2139
+// }
2140
+
2141
+// Promise.all(dependencies).then(
2142
+// completeBoundary.bind(null, suspenseBoundaryID, contentID, ''),
2143
+// completeBoundary.bind(null, suspenseBoundaryID, contentID, "Resource failed to load")
2144
+// );
2145
+// }
2146
+
2147
+// function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
2148
// const contentNode = document.getElementById(contentID);
2149
// // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
2150
// // This might also help by not causing recalcing each time we move a child from here to the target.
2151
// contentNode.parentNode.removeChild(contentNode);
2152
+
2153
+// // Find the fallback's first element.
2154
+// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
2155
// if (!suspenseIdNode) {
2156
// // The user must have already navigated away from this tree.
2157
// // E.g. because the parent was hydrated. That's fine there's nothing to do
@@ -2016,51 +2160,57 @@ export function writeEndSegment(
2160
// }
2161
// // Find the boundary around the fallback. This is always the previous node.
2162
// const suspenseNode = suspenseIdNode.previousSibling;
2019
-//
2020
-// // Clear all the existing children. This is complicated because
2021
-// // there can be embedded Suspense boundaries in the fallback.
2022
-// // This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
2023
-// // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
2024
-// // They never hydrate anyway. However, currently we support incrementally loading the fallback.
2025
-// const parentInstance = suspenseNode.parentNode;
2026
-// let node = suspenseNode.nextSibling;
2027
-// let depth = 0;
2028
-// do {
2029
-// if (node && node.nodeType === COMMENT_NODE) {
2030
-// const data = node.data;
2031
-// if (data === SUSPENSE_END_DATA) {
2032
-// if (depth === 0) {
2033
-// break;
2034
-// } else {
2035
-// depth--;
2163
+
2164
+// if (!errorDigest) {
2165
+// // Clear all the existing children. This is complicated because
2166
+// // there can be embedded Suspense boundaries in the fallback.
2167
+// // This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
2168
+// // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
2169
+// // They never hydrate anyway. However, currently we support incrementally loading the fallback.
2170
+// const parentInstance = suspenseNode.parentNode;
2171
+// let node = suspenseNode.nextSibling;
2172
+// let depth = 0;
2173
+// do {
2174
+// if (node && node.nodeType === COMMENT_NODE) {
2175
+// const data = node.data;
2176
+// if (data === SUSPENSE_END_DATA) {
2177
+// if (depth === 0) {
2178
+// break;
2179
+// } else {
2180
+// depth--;
2181
+// }
2182
+// } else if (
2183
+// data === SUSPENSE_START_DATA ||
2184
+// data === SUSPENSE_PENDING_START_DATA ||
2185
+// data === SUSPENSE_FALLBACK_START_DATA
2186
+// ) {
2187
+// depth++;
2188
// }
2037
-// } else if (
2038
-// data === SUSPENSE_START_DATA ||
2039
-// data === SUSPENSE_PENDING_START_DATA ||
2040
-// data === SUSPENSE_FALLBACK_START_DATA
2041
-// ) {
2042
-// depth++;
2189
// }
2190
+
2191
+// const nextNode = node.nextSibling;
2192
+// parentInstance.removeChild(node);
2193
+// node = nextNode;
2194
+// } while (node);
2195
+
2196
+// const endOfBoundary = node;
2197
+
2198
+// // Insert all the children from the contentNode between the start and end of suspense boundary.
2199
+// while (contentNode.firstChild) {
2200
+// parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
2201
// }
2045
-//
2046
-// const nextNode = node.nextSibling;
2047
-// parentInstance.removeChild(node);
2048
-// node = nextNode;
2049
-// } while (node);
2050
-//
2051
-// const endOfBoundary = node;
2052
-//
2053
-// // Insert all the children from the contentNode between the start and end of suspense boundary.
2054
-// while (contentNode.firstChild) {
2055
-// parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
2202
+
2203
+// suspenseNode.data = SUSPENSE_START_DATA;
2204
+// } else {
2205
+// suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
2206
+// suspenseIdNode.setAttribute('data-dgst', errorDigest)
2207
// }
2208
2058
-// suspenseNode.data = SUSPENSE_START_DATA;
2209
// if (suspenseNode._reactRetry) {
2210
// suspenseNode._reactRetry();
2211
// }
2212
// }
2063
-//
2213
+
2214
// function completeSegment(containerID, placeholderID) {
2215
// const segmentContainer = document.getElementById(containerID);
2216
// const placeholderNode = document.getElementById(placeholderID);
@@ -2080,7 +2230,9 @@ export function writeEndSegment(
2230
const completeSegmentFunction =
2231
'function $RS(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)}';
2232
const completeBoundaryFunction =
2083
- 'function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}}';
2233
+ 'function $RC(b,c,d){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(d)b.data="$!",a.setAttribute("data-dgst",d);else{d=b.parentNode;a=b.nextSibling;var e=0;do{if(a&&a.nodeType===8){var h=a.data;if(h==="/$")if(0===e)break;else e--;else h!=="$"&&h!=="$?"&&h!=="$!"||e++}h=a.nextSibling;d.removeChild(a);a=h}while(a);for(;c.firstChild;)d.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}}';
2234
+const styleInsertionFunction =
2235
+ '$RM=new Map;function $RR(p,q,t){function r(l){this.s=l}for(var m=new Map,n=document,g,e,f=n.querySelectorAll("link[data-rprec]"),d=0;e=f[d++];)m.set(e.dataset.rprec,g=e);e=0;f=[];for(var c,h,b,a;c=t[e++];){var k=0;h=c[k++];if(b=$RM.get(h))"l"!==b.s&&f.push(b);else{a=n.createElement("link");a.href=h;a.rel="stylesheet";for(a.dataset.rprec=d=c[k++];b=c[k++];)a.setAttribute(b,c[k++]);b=a._p=new Promise(function(l,u){a.onload=l;a.onerror=u});b.then(r.bind(b,"l"),r.bind(b,"e"));$RM.set(h,b);f.push(b);c=m.get(d)||g;c===g&&(g=a);m.set(d,a);c?c.parentNode.insertBefore(a,c.nextSibling):(d=n.head,d.insertBefore(a,d.firstChild))}}Promise.all(f).then($RC.bind(null,p,q,""),$RC.bind(null,p,q,"Resource failed to load"))}';
2236
const clientRenderFunction =
2237
'function $RX(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),b._reactRetry&&b._reactRetry())}';
2238
@@ -2118,23 +2270,51 @@ const completeBoundaryScript1Full = stringToPrecomputedChunk(
2270
completeBoundaryFunction + ';$RC("',
2271
);
2272
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
2273
+
2274
+const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk(
2275
+ completeBoundaryFunction + ';' + styleInsertionFunction + ';$RR("',
2276
+);
2277
+const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
2278
+ styleInsertionFunction + ';$RR("',
2279
+);
2280
+const completeBoundaryWithStylesScript1Partial = stringToPrecomputedChunk(
2281
+ '$RR("',
2282
+);
2283
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
2122
-const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');
2284
+const completeBoundaryScript2a = stringToPrecomputedChunk('",');
2285
+const completeBoundaryScript3 = stringToPrecomputedChunk('"');
2286
+const completeBoundaryScript4 = stringToPrecomputedChunk(')</script>');
2287
2288
export function writeCompletedBoundaryInstruction(
2289
destination: Destination,
2290
responseState: ResponseState,
2291
boundaryID: SuspenseBoundaryID,
2292
contentSegmentID: number,
2293
+ boundaryResources: BoundaryResources,
2294
): boolean {
2295
+ let hasStyleDependencies;
2296
+ if (enableFloat) {
2297
+ hasStyleDependencies = hasStyleResourceDependencies(boundaryResources);
2298
+ }
2299
writeChunk(destination, responseState.startInlineScript);
2131
- if (!responseState.sentCompleteBoundaryFunction) {
2132
- // The first time we write this, we'll need to include the full implementation.
2133
- responseState.sentCompleteBoundaryFunction = true;
2134
- writeChunk(destination, completeBoundaryScript1Full);
2300
+ if (enableFloat && hasStyleDependencies) {
2301
+ if (!responseState.sentCompleteBoundaryFunction) {
2302
+ responseState.sentCompleteBoundaryFunction = true;
2303
+ responseState.sentStyleInsertionFunction = true;
2304
+ writeChunk(destination, completeBoundaryWithStylesScript1FullBoth);
2305
+ } else if (!responseState.sentStyleInsertionFunction) {
2306
+ responseState.sentStyleInsertionFunction = true;
2307
+ writeChunk(destination, completeBoundaryWithStylesScript1FullPartial);
2308
+ } else {
2309
+ writeChunk(destination, completeBoundaryWithStylesScript1Partial);
2310
+ }
2311
} else {
2136
- // Future calls can just reuse the same function.
2137
- writeChunk(destination, completeBoundaryScript1Partial);
2312
+ if (!responseState.sentCompleteBoundaryFunction) {
2313
+ responseState.sentCompleteBoundaryFunction = true;
2314
+ writeChunk(destination, completeBoundaryScript1Full);
2315
+ } else {
2316
+ writeChunk(destination, completeBoundaryScript1Partial);
2317
+ }
2318
}
2319
2320
if (boundaryID === null) {
@@ -2148,7 +2328,13 @@ export function writeCompletedBoundaryInstruction(
2328
writeChunk(destination, completeBoundaryScript2);
2329
writeChunk(destination, responseState.segmentPrefix);
2330
writeChunk(destination, formattedContentID);
2151
- return writeChunkAndReturn(destination, completeBoundaryScript3);
2331
+ if (enableFloat && hasStyleDependencies) {
2332
+ writeChunk(destination, completeBoundaryScript2a);
2333
+ writeStyleResourceDependencies(destination, boundaryResources);
2334
+ } else {
2335
+ writeChunk(destination, completeBoundaryScript3);
2336
+ }
2337
+ return writeChunkAndReturn(destination, completeBoundaryScript4);
2338
}
2339
2340
const clientRenderScript1Full = stringToPrecomputedChunk(
@@ -2209,10 +2395,10 @@ export function writeClientRenderBoundaryInstruction(
2395
return writeChunkAndReturn(destination, clientRenderScript2);
2396
}
2397
2212
-const regexForJSStringsInScripts = /[<\u2028\u2029]/g;
2398
+const regexForJSStringsInInstructionScripts = /[<\u2028\u2029]/g;
2399
function escapeJSStringsForInstructionScripts(input: string): string {
2400
const escaped = JSON.stringify(input);
2215
- return escaped.replace(regexForJSStringsInScripts, match => {
2401
+ return escaped.replace(regexForJSStringsInInstructionScripts, match => {
2402
switch (match) {
2403
// santizing breaking out of strings and script tags
2404
case '<':
@@ -2230,3 +2416,326 @@ function escapeJSStringsForInstructionScripts(input: string): string {
2416
}
2417
});
2418
}
2419
+
2420
+const regexForJSStringsInScripts = /[&><\u2028\u2029]/g;
2421
+function escapeJSObjectForInstructionScripts(input: Object): string {
2422
+ const escaped = JSON.stringify(input);
2423
+ return escaped.replace(regexForJSStringsInScripts, match => {
2424
+ switch (match) {
2425
+ // santizing breaking out of strings and script tags
2426
+ case '&':
2427
+ return '\\u0026';
2428
+ case '>':
2429
+ return '\\u003e';
2430
+ case '<':
2431
+ return '\\u003c';
2432
+ case '\u2028':
2433
+ return '\\u2028';
2434
+ case '\u2029':
2435
+ return '\\u2029';
2436
+ default: {
2437
+ // eslint-disable-next-line react-internal/prod-error-codes
2438
+ throw new Error(
2439
+ 'escapeJSObjectForInstructionScripts encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
2440
+ );
2441
+ }
2442
+ }
2443
+ });
2444
+}
2445
+
2446
+export function writeInitialResources(
2447
+ destination: Destination,
2448
+ resources: Resources,
2449
+ responseState: ResponseState,
2450
+): boolean {
2451
+ const explicitPreloadsTarget = [];
2452
+ const remainingTarget = [];
2453
+
2454
+ const {precedences, explicitPreloads, implicitPreloads} = resources;
2455
+
2456
+ // Flush stylesheets first by earliest precedence
2457
+ precedences.forEach(precedenceResources => {
2458
+ precedenceResources.forEach(resource => {
2459
+ // resources should not already be flushed so we elide this check
2460
+ pushLinkImpl(remainingTarget, resource.props, responseState);
2461
+ resource.flushed = true;
2462
+ resource.inShell = true;
2463
+ resource.hint.flushed = true;
2464
+ });
2465
+ });
2466
+
2467
+ explicitPreloads.forEach(resource => {
2468
+ if (!resource.flushed) {
2469
+ pushLinkImpl(explicitPreloadsTarget, resource.props, responseState);
2470
+ resource.flushed = true;
2471
+ }
2472
+ });
2473
+ explicitPreloads.clear();
2474
+
2475
+ implicitPreloads.forEach(resource => {
2476
+ if (!resource.flushed) {
2477
+ pushLinkImpl(remainingTarget, resource.props, responseState);
2478
+ resource.flushed = true;
2479
+ }
2480
+ });
2481
+ implicitPreloads.clear();
2482
+
2483
+ let i;
2484
+ let r = true;
2485
+ for (i = 0; i < explicitPreloadsTarget.length - 1; i++) {
2486
+ writeChunk(destination, explicitPreloadsTarget[i]);
2487
+ }
2488
+ if (i < explicitPreloadsTarget.length) {
2489
+ r = writeChunkAndReturn(destination, explicitPreloadsTarget[i]);
2490
+ }
2491
+
2492
+ for (i = 0; i < remainingTarget.length - 1; i++) {
2493
+ writeChunk(destination, remainingTarget[i]);
2494
+ }
2495
+ if (i < remainingTarget.length) {
2496
+ r = writeChunkAndReturn(destination, remainingTarget[i]);
2497
+ }
2498
+ return r;
2499
+}
2500
+
2501
+export function writeImmediateResources(
2502
+ destination: Destination,
2503
+ resources: Resources,
2504
+ responseState: ResponseState,
2505
+): boolean {
2506
+ const {explicitPreloads, implicitPreloads} = resources;
2507
+ const target = [];
2508
+
2509
+ explicitPreloads.forEach(resource => {
2510
+ if (!resource.flushed) {
2511
+ pushLinkImpl(target, resource.props, responseState);
2512
+ resource.flushed = true;
2513
+ }
2514
+ });
2515
+ explicitPreloads.clear();
2516
+
2517
+ implicitPreloads.forEach(resource => {
2518
+ if (!resource.flushed) {
2519
+ pushLinkImpl(target, resource.props, responseState);
2520
+ resource.flushed = true;
2521
+ }
2522
+ });
2523
+ implicitPreloads.clear();
2524
+
2525
+ let i = 0;
2526
+ for (; i < target.length - 1; i++) {
2527
+ writeChunk(destination, target[i]);
2528
+ }
2529
+ if (i < target.length) {
2530
+ return writeChunkAndReturn(destination, target[i]);
2531
+ }
2532
+ return false;
2533
+}
2534
+
2535
+function hasStyleResourceDependencies(
2536
+ boundaryResources: BoundaryResources,
2537
+): boolean {
2538
+ const iter = boundaryResources.values();
2539
+ // At the moment boundaries only accumulate style resources
2540
+ // so we assume the type is correct and don't check it
2541
+ while (true) {
2542
+ const {value: resource} = iter.next();
2543
+ if (!resource) break;
2544
+
2545
+ // If every style Resource flushed in the shell we do not need to send
2546
+ // any dependencies
2547
+ if (!resource.inShell) {
2548
+ return true;
2549
+ }
2550
+ }
2551
+ return false;
2552
+}
2553
+
2554
+const arrayFirstOpenBracket = stringToPrecomputedChunk('[');
2555
+const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
2556
+const arrayInterstitial = stringToPrecomputedChunk(',');
2557
+const arrayCloseBracket = stringToPrecomputedChunk(']');
2558
+
2559
+function writeStyleResourceDependencies(
2560
+ destination: Destination,
2561
+ boundaryResources: BoundaryResources,
2562
+): void {
2563
+ writeChunk(destination, arrayFirstOpenBracket);
2564
+
2565
+ let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
2566
+ boundaryResources.forEach(resource => {
2567
+ if (resource.inShell) {
2568
+ // We can elide this dependency because it was flushed in the shell and
2569
+ // should be ready before content is shown on the client
2570
+ } else if (resource.flushed) {
2571
+ writeChunk(destination, nextArrayOpenBrackChunk);
2572
+ writeStyleResourceDependencyHrefOnly(destination, resource.href);
2573
+ writeChunk(destination, arrayCloseBracket);
2574
+ nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
2575
+ } else {
2576
+ writeChunk(destination, nextArrayOpenBrackChunk);
2577
+ writeStyleResourceDependency(
2578
+ destination,
2579
+ resource.href,
2580
+ resource.precedence,
2581
+ resource.props,
2582
+ );
2583
+ writeChunk(destination, arrayCloseBracket);
2584
+ nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
2585
+
2586
+ resource.flushed = true;
2587
+ resource.hint.flushed = true;
2588
+ }
2589
+ });
2590
+ writeChunk(destination, arrayCloseBracket);
2591
+}
2592
+
2593
+function writeStyleResourceDependencyHrefOnly(
2594
+ destination: Destination,
2595
+ href: string,
2596
+) {
2597
+ // We should actually enforce this earlier when the resource is created but for
2598
+ // now we make sure we are actually dealing with a string here.
2599
+ if (__DEV__) {
2600
+ checkAttributeStringCoercion(href, 'href');
2601
+ }
2602
+ const coercedHref = '' + (href: any);
2603
+ writeChunk(
2604
+ destination,
2605
+ stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
2606
+ );
2607
+}
2608
+
2609
+function writeStyleResourceDependency(
2610
+ destination: Destination,
2611
+ href: string,
2612
+ precedence: string,
2613
+ props: Object,
2614
+) {
2615
+ if (__DEV__) {
2616
+ checkAttributeStringCoercion(href, 'href');
2617
+ }
2618
+ const coercedHref = '' + (href: any);
2619
+ sanitizeURL(coercedHref);
2620
+ writeChunk(
2621
+ destination,
2622
+ stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
2623
+ );
2624
+
2625
+ if (__DEV__) {
2626
+ checkAttributeStringCoercion(precedence, 'precedence');
2627
+ }
2628
+ const coercedPrecedence = '' + (precedence: any);
2629
+ writeChunk(destination, arrayInterstitial);
2630
+ writeChunk(
2631
+ destination,
2632
+ stringToChunk(escapeJSObjectForInstructionScripts(coercedPrecedence)),
2633
+ );
2634
+
2635
+ for (const propKey in props) {
2636
+ if (hasOwnProperty.call(props, propKey)) {
2637
+ const propValue = props[propKey];
2638
+ if (propValue == null) {
2639
+ continue;
2640
+ }
2641
+ switch (propKey) {
2642
+ case 'href':
2643
+ case 'rel':
2644
+ case 'precedence':
2645
+ case 'data-rprec': {
2646
+ break;
2647
+ }
2648
+ case 'children':
2649
+ case 'dangerouslySetInnerHTML':
2650
+ throw new Error(
2651
+ `${'link'} is a self-closing tag and must neither have \`children\` nor ` +
2652
+ 'use `dangerouslySetInnerHTML`.',
2653
+ );
2654
+ // eslint-disable-next-line-no-fallthrough
2655
+ default:
2656
+ writeStyleResourceAttribute(destination, propKey, propValue);
2657
+ break;
2658
+ }
2659
+ }
2660
+ }
2661
+ return null;
2662
+}
2663
+
2664
+function writeStyleResourceAttribute(
2665
+ destination: Destination,
2666
+ name: string,
2667
+ value: string | boolean | number | Function | Object, // not null or undefined
2668
+): void {
2669
+ let attributeName = name.toLowerCase();
2670
+ let attributeValue;
2671
+ switch (typeof value) {
2672
+ case 'function':
2673
+ case 'symbol':
2674
+ return;
2675
+ }
2676
+
2677
+ switch (name) {
2678
+ // Reserved names
2679
+ case 'innerHTML':
2680
+ case 'dangerouslySetInnerHTML':
2681
+ case 'suppressContentEditableWarning':
2682
+ case 'suppressHydrationWarning':
2683
+ case 'style':
2684
+ // Ignored
2685
+ return;
2686
+
2687
+ // Attribute renames
2688
+ case 'className':
2689
+ attributeName = 'class';
2690
+ break;
2691
+
2692
+ // Booleans
2693
+ case 'hidden':
2694
+ if (value === false) {
2695
+ return;
2696
+ }
2697
+ attributeValue = '';
2698
+ break;
2699
+
2700
+ // Santized URLs
2701
+ case 'src':
2702
+ case 'href': {
2703
+ if (__DEV__) {
2704
+ checkAttributeStringCoercion(value, attributeName);
2705
+ }
2706
+ attributeValue = '' + (value: any);
2707
+ sanitizeURL(attributeValue);
2708
+ break;
2709
+ }
2710
+ default: {
2711
+ if (!isAttributeNameSafe(name)) {
2712
+ return;
2713
+ }
2714
+ }
2715
+ }
2716
+
2717
+ if (
2718
+ // shouldIgnoreAttribute
2719
+ // We have already filtered out null/undefined and reserved words.
2720
+ name.length > 2 &&
2721
+ (name[0] === 'o' || name[0] === 'O') &&
2722
+ (name[1] === 'n' || name[1] === 'N')
2723
+ ) {
2724
+ return;
2725
+ }
2726
+
2727
+ if (__DEV__) {
2728
+ checkAttributeStringCoercion(value, attributeName);
2729
+ }
2730
+ attributeValue = '' + (value: any);
2731
+ writeChunk(destination, arrayInterstitial);
2732
+ writeChunk(
2733
+ destination,
2734
+ stringToChunk(escapeJSObjectForInstructionScripts(attributeName)),
2735
+ );
2736
+ writeChunk(destination, arrayInterstitial);
2737
+ writeChunk(
2738
+ destination,
2739
+ stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)),
2740
+ );
2741
+}
packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js
+13
@@ -40,6 +40,7 @@ export type ResponseState = {
40
sentCompleteSegmentFunction: boolean,
41
sentCompleteBoundaryFunction: boolean,
42
sentClientRenderFunction: boolean,
43
+ sentStyleInsertionFunction: boolean,
44
// This is an extra field for the legacy renderer
45
generateStaticMarkup: boolean,
46
};
@@ -61,6 +62,7 @@ export function createResponseState(
62
sentCompleteSegmentFunction: responseState.sentCompleteSegmentFunction,
63
sentCompleteBoundaryFunction: responseState.sentCompleteBoundaryFunction,
64
sentClientRenderFunction: responseState.sentClientRenderFunction,
65
+ sentStyleInsertionFunction: responseState.sentStyleInsertionFunction,
66
// This is an extra field for the legacy renderer
67
generateStaticMarkup,
68
};
@@ -74,6 +76,8 @@ export function createRootFormatContext(): FormatContext {
76
}
77
78
export type {
79
+ Resources,
80
+ BoundaryResources,
81
FormatContext,
82
SuspenseBoundaryID,
83
} from './ReactDOMServerFormatConfig';
@@ -96,6 +100,15 @@ export {
100
writeEndPendingSuspenseBoundary,
101
writePlaceholder,
102
writeCompletedRoot,
103
+ createResources,
104
+ createBoundaryResources,
105
+ writeInitialResources,
106
+ writeImmediateResources,
107
+ hoistResources,
108
+ hoistResourcesToRoot,
109
+ setCurrentlyRenderingBoundaryResourcesTarget,
110
+ prepareToRender,
111
+ cleanupAfterRender,
112
} from './ReactDOMServerFormatConfig';
113
114
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
packages/react-dom-bindings/src/shared/ReactDOMDispatcher.js
new
+18
@@ -0,0 +1,18 @@
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
+type DispatcherType = {
11
+ [string]: mixed,
12
+};
13
+
14
+const Dispatcher: {current: null | DispatcherType} = {
15
+ current: null,
16
+};
17
+
18
+export default Dispatcher;
packages/react-dom-bindings/src/shared/ReactDOMFloat.js
new
+21
@@ -0,0 +1,21 @@
1
+import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
2
+
3
+export function preinit() {
4
+ const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
5
+ if (dispatcher) {
6
+ dispatcher.preinit.apply(this, arguments);
7
+ }
8
+ // We don't error because preinit needs to be resilient to being called in a variety of scopes
9
+ // and the runtime may not be capable of responding. The function is optimistic and not critical
10
+ // so we favor silent bailout over warning or erroring.
11
+}
12
+
13
+export function preload() {
14
+ const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
15
+ if (dispatcher) {
16
+ dispatcher.preload.apply(this, arguments);
17
+ }
18
+ // We don't error because preload needs to be resilient to being called in a variety of scopes
19
+ // and the runtime may not be capable of responding. The function is optimistic and not critical
20
+ // so we favor silent bailout over warning or erroring.
21
+}
packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js
new
+601
@@ -0,0 +1,601 @@
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 hasOwnProperty from 'shared/hasOwnProperty';
11
+
12
+type Props = {[string]: mixed};
13
+
14
+export function validateUnmatchedLinkResourceProps(
15
+ pendingProps: Props,
16
+ currentProps: ?Props,
17
+) {
18
+ if (__DEV__) {
19
+ if (pendingProps.rel !== 'font' && pendingProps.rel !== 'style') {
20
+ if (currentProps != null) {
21
+ const originalResourceName =
22
+ typeof currentProps.href === 'string'
23
+ ? `Resource with href "${currentProps.href}"`
24
+ : 'Resource';
25
+ const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
26
+ currentProps.rel,
27
+ );
28
+ const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
29
+ pendingProps.rel,
30
+ );
31
+ const pendingHrefStatement =
32
+ typeof pendingProps.href === 'string'
33
+ ? ` and the updated href is "${pendingProps.href}"`
34
+ : '';
35
+ console.error(
36
+ 'A <link> previously rendered as a %s but was updated with a rel type that is not' +
37
+ ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
38
+ ' props however in some limited circumstances it can be valid when changing the href.' +
39
+ ' When React encounters props that invalidate the Resource it is the same as not rendering' +
40
+ ' a Resource at all. valid rel types for Resources are "font" and "style". The previous' +
41
+ ' rel for this instance was %s. The updated rel is %s%s.',
42
+ originalResourceName,
43
+ originalRelStatement,
44
+ pendingRelStatement,
45
+ pendingHrefStatement,
46
+ );
47
+ } else {
48
+ const pendingRelStatement = getValueDescriptorExpectingEnumForWarning(
49
+ pendingProps.rel,
50
+ );
51
+ console.error(
52
+ 'A <link> is rendering as a Resource but has an invalid rel property. The rel encountered is %s.' +
53
+ ' This is a bug in React.',
54
+ pendingRelStatement,
55
+ );
56
+ }
57
+ }
58
+ }
59
+}
60
+
61
+export function validatePreloadResourceDifference(
62
+ originalProps: any,
63
+ originalImplicit: boolean,
64
+ latestProps: any,
65
+ latestImplicit: boolean,
66
+) {
67
+ if (__DEV__) {
68
+ const {href} = originalProps;
69
+ const originalWarningName = getResourceNameForWarning(
70
+ 'preload',
71
+ originalProps,
72
+ originalImplicit,
73
+ );
74
+ const latestWarningName = getResourceNameForWarning(
75
+ 'preload',
76
+ latestProps,
77
+ latestImplicit,
78
+ );
79
+
80
+ if (latestProps.as !== originalProps.as) {
81
+ console.error(
82
+ 'A %s is using the same href "%s" as a %s. This is always an error and React will only keep the first preload' +
83
+ ' for any given href, discarding subsequent instances. To fix, find where you are using this href in link' +
84
+ ' tags or in calls to ReactDOM.preload() or ReactDOM.preinit() and either make the Resource types agree or' +
85
+ ' update the hrefs to be distinct for different Resource types.',
86
+ latestWarningName,
87
+ href,
88
+ originalWarningName,
89
+ );
90
+ } else {
91
+ let missingProps = null;
92
+ let extraProps = null;
93
+ let differentProps = null;
94
+ if (originalProps.media != null && latestProps.media == null) {
95
+ missingProps = missingProps || {};
96
+ missingProps.media = originalProps.media;
97
+ }
98
+
99
+ for (const propName in latestProps) {
100
+ const propValue = latestProps[propName];
101
+ const originalValue = originalProps[propName];
102
+
103
+ if (propValue != null && propValue !== originalValue) {
104
+ if (originalValue == null) {
105
+ extraProps = extraProps || {};
106
+ extraProps[propName] = propValue;
107
+ } else {
108
+ differentProps = differentProps || {};
109
+ differentProps[propName] = {
110
+ original: originalValue,
111
+ latest: propValue,
112
+ };
113
+ }
114
+ }
115
+ }
116
+
117
+ if (missingProps || extraProps || differentProps) {
118
+ warnDifferentProps(
119
+ href,
120
+ originalWarningName,
121
+ latestWarningName,
122
+ extraProps,
123
+ missingProps,
124
+ differentProps,
125
+ );
126
+ }
127
+ }
128
+ }
129
+}
130
+
131
+export function validateStyleResourceDifference(
132
+ originalProps: any,
133
+ latestProps: any,
134
+) {
135
+ if (__DEV__) {
136
+ const {href} = originalProps;
137
+ // eslint-disable-next-line no-labels
138
+ const originalWarningName = getResourceNameForWarning(
139
+ 'style',
140
+ originalProps,
141
+ false,
142
+ );
143
+ const latestWarningName = getResourceNameForWarning(
144
+ 'style',
145
+ latestProps,
146
+ false,
147
+ );
148
+ let missingProps = null;
149
+ let extraProps = null;
150
+ let differentProps = null;
151
+ if (originalProps.media != null && latestProps.media == null) {
152
+ missingProps = missingProps || {};
153
+ missingProps.media = originalProps.media;
154
+ }
155
+
156
+ for (let propName in latestProps) {
157
+ const propValue = latestProps[propName];
158
+ const originalValue = originalProps[propName];
159
+
160
+ if (propValue != null && propValue !== originalValue) {
161
+ propName = propName === 'data-rprec' ? 'precedence' : propName;
162
+ if (originalValue == null) {
163
+ extraProps = extraProps || {};
164
+ extraProps[propName] = propValue;
165
+ } else {
166
+ differentProps = differentProps || {};
167
+ differentProps[propName] = {
168
+ original: originalValue,
169
+ latest: propValue,
170
+ };
171
+ }
172
+ }
173
+ }
174
+
175
+ if (missingProps || extraProps || differentProps) {
176
+ warnDifferentProps(
177
+ href,
178
+ originalWarningName,
179
+ latestWarningName,
180
+ extraProps,
181
+ missingProps,
182
+ differentProps,
183
+ );
184
+ }
185
+ }
186
+}
187
+
188
+export function validateStyleAndHintProps(
189
+ preloadProps: any,
190
+ styleProps: any,
191
+ implicitPreload: boolean,
192
+) {
193
+ if (__DEV__) {
194
+ const {href} = preloadProps;
195
+
196
+ const originalWarningName = getResourceNameForWarning(
197
+ 'preload',
198
+ preloadProps,
199
+ implicitPreload,
200
+ );
201
+ const latestWarningName = getResourceNameForWarning(
202
+ 'style',
203
+ styleProps,
204
+ false,
205
+ );
206
+
207
+ if (preloadProps.as !== 'style') {
208
+ console.error(
209
+ 'While creating a %s for href "%s" a %s for this same href was found. When preloading a stylesheet the' +
210
+ ' "as" prop must be of type "style". This most likely ocurred by rending a preload link with an incorrect' +
211
+ ' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
212
+ latestWarningName,
213
+ href,
214
+ originalWarningName,
215
+ );
216
+ }
217
+
218
+ let missingProps = null;
219
+ let extraProps = null;
220
+ let differentProps = null;
221
+
222
+ for (const propName in styleProps) {
223
+ const styleValue = styleProps[propName];
224
+ const preloadValue = preloadProps[propName];
225
+ switch (propName) {
226
+ // Check for difference on specific props that cross over or influence
227
+ // the relationship between the preload and stylesheet
228
+ case 'crossOrigin':
229
+ case 'referrerPolicy':
230
+ case 'media':
231
+ case 'title': {
232
+ if (
233
+ preloadValue !== styleValue &&
234
+ !(preloadValue == null && styleValue == null)
235
+ ) {
236
+ if (styleValue == null) {
237
+ missingProps = missingProps || {};
238
+ missingProps[propName] = preloadValue;
239
+ } else if (preloadValue == null) {
240
+ extraProps = extraProps || {};
241
+ extraProps[propName] = styleValue;
242
+ } else {
243
+ differentProps = differentProps || {};
244
+ differentProps[propName] = {
245
+ original: preloadValue,
246
+ latest: styleValue,
247
+ };
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ if (missingProps || extraProps || differentProps) {
255
+ warnDifferentProps(
256
+ href,
257
+ originalWarningName,
258
+ latestWarningName,
259
+ extraProps,
260
+ missingProps,
261
+ differentProps,
262
+ );
263
+ }
264
+ }
265
+}
266
+
267
+function warnDifferentProps(
268
+ href: string,
269
+ originalName: string,
270
+ latestName: string,
271
+ extraProps: ?{[string]: any},
272
+ missingProps: ?{[string]: any},
273
+ differentProps: ?{[string]: {original: any, latest: any}},
274
+): void {
275
+ if (__DEV__) {
276
+ const juxtaposedNameStatement =
277
+ latestName === originalName
278
+ ? 'an earlier instance of this Resource'
279
+ : `a ${originalName} with the same href`;
280
+
281
+ let comparisonStatement = '';
282
+ if (missingProps !== null && typeof missingProps === 'object') {
283
+ for (const propName in missingProps) {
284
+ comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
285
+ }
286
+ }
287
+ if (extraProps !== null && typeof extraProps === 'object') {
288
+ for (const propName in extraProps) {
289
+ comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
290
+ }
291
+ }
292
+ if (differentProps !== null && typeof differentProps === 'object') {
293
+ for (const propName in differentProps) {
294
+ comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
295
+ }
296
+ }
297
+
298
+ console.error(
299
+ 'A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
300
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
301
+ ' update Resources that share an href to have props that agree. The differences are described below.%s',
302
+ latestName,
303
+ href,
304
+ juxtaposedNameStatement,
305
+ comparisonStatement,
306
+ );
307
+ }
308
+}
309
+
310
+function getResourceNameForWarning(
311
+ type: string,
312
+ props: Object,
313
+ implicit: boolean,
314
+) {
315
+ if (__DEV__) {
316
+ switch (type) {
317
+ case 'style': {
318
+ return 'style Resource';
319
+ }
320
+ case 'preload': {
321
+ if (implicit) {
322
+ return `preload for a ${props.as} Resource`;
323
+ }
324
+ return `preload Resource (as "${props.as}")`;
325
+ }
326
+ }
327
+ }
328
+ return 'Resource';
329
+}
330
+
331
+export function validateHrefKeyedUpdatedProps(
332
+ pendingProps: Props,
333
+ currentProps: Props,
334
+): boolean {
335
+ if (__DEV__) {
336
+ // This function should never be called if we don't have hrefs so we don't bother considering
337
+ // Whether they are null or undefined
338
+ if (pendingProps.href === currentProps.href) {
339
+ // If we have the same href we need all other props to be the same
340
+ let missingProps;
341
+ let extraProps;
342
+ let differentProps;
343
+ const allProps = Array.from(
344
+ new Set(Object.keys(currentProps).concat(Object.keys(pendingProps))),
345
+ );
346
+ for (let i = 0; i < allProps.length; i++) {
347
+ const propName = allProps[i];
348
+ const pendingValue = pendingProps[propName];
349
+ const currentValue = currentProps[propName];
350
+ if (
351
+ pendingValue !== currentValue &&
352
+ !(pendingValue == null && currentValue == null)
353
+ ) {
354
+ if (pendingValue == null) {
355
+ missingProps = missingProps || {};
356
+ missingProps[propName] = currentValue;
357
+ } else if (currentValue == null) {
358
+ extraProps = extraProps || {};
359
+ extraProps[propName] = pendingValue;
360
+ } else {
361
+ differentProps = differentProps || {};
362
+ differentProps[propName] = {
363
+ original: currentValue,
364
+ latest: pendingValue,
365
+ };
366
+ }
367
+ }
368
+ }
369
+ if (missingProps || extraProps || differentProps) {
370
+ const latestWarningName = getResourceNameForWarning(
371
+ 'style',
372
+ currentProps,
373
+ false,
374
+ );
375
+
376
+ let comparisonStatement = '';
377
+ if (missingProps !== null && typeof missingProps === 'object') {
378
+ for (const propName in missingProps) {
379
+ comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
380
+ }
381
+ }
382
+ if (extraProps !== null && typeof extraProps === 'object') {
383
+ for (const propName in extraProps) {
384
+ comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
385
+ }
386
+ }
387
+ if (differentProps !== null && typeof differentProps === 'object') {
388
+ for (const propName in differentProps) {
389
+ comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
390
+ }
391
+ }
392
+ console.error(
393
+ 'A %s with href "%s" recieved new props with different values from the props used' +
394
+ ' when this Resource was first rendered. React will only use the props provided when' +
395
+ ' this resource was first rendered until a new href is provided. Unlike conventional' +
396
+ ' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
397
+ ' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
398
+ ' (href) must have props that agree with each other. The differences are described below.%s',
399
+ latestWarningName,
400
+ currentProps.href,
401
+ comparisonStatement,
402
+ );
403
+ return true;
404
+ }
405
+ }
406
+ }
407
+ return false;
408
+}
409
+
410
+export function validateLinkPropsForStyleResource(props: Props): boolean {
411
+ if (__DEV__) {
412
+ // This should only be called when we know we are opting into Resource semantics (i.e. precedence is not null)
413
+ const {href, onLoad, onError, disabled} = props;
414
+ const allProps = ['onLoad', 'onError', 'disabled'];
415
+ const includedProps = [];
416
+ if (onLoad) includedProps.push('onLoad');
417
+ if (onError) includedProps.push('onError');
418
+ if (disabled != null) includedProps.push('disabled');
419
+
420
+ const allPropsUnionPhrase = propNamesListJoin(allProps, 'or');
421
+ let includedPropsPhrase = propNamesListJoin(includedProps, 'and');
422
+ includedPropsPhrase += includedProps.length === 1 ? ' prop' : ' props';
423
+
424
+ if (includedProps.length) {
425
+ console.error(
426
+ 'A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
427
+ ' When using %s React will opt out of Resource behavior. If you meant for this' +
428
+ ' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.',
429
+ href,
430
+ includedPropsPhrase,
431
+ allPropsUnionPhrase,
432
+ includedPropsPhrase,
433
+ );
434
+ return true;
435
+ }
436
+ }
437
+ return false;
438
+}
439
+
440
+function propNamesListJoin(
441
+ list: Array<string>,
442
+ combinator: 'and' | 'or',
443
+): string {
444
+ switch (list.length) {
445
+ case 0:
446
+ return '';
447
+ case 1:
448
+ return list[0];
449
+ case 2:
450
+ return list[0] + ' ' + combinator + ' ' + list[1];
451
+ default:
452
+ return (
453
+ list.slice(0, -1).join(', ') +
454
+ ', ' +
455
+ combinator +
456
+ ' ' +
457
+ list[list.length - 1]
458
+ );
459
+ }
460
+}
461
+
462
+export function validateLinkPropsForPreloadResource(linkProps: any) {
463
+ if (__DEV__) {
464
+ const {href, as} = linkProps;
465
+ if (as === 'font') {
466
+ const name = getResourceNameForWarning('preload', linkProps, false);
467
+ if (!hasOwnProperty.call(linkProps, 'crossOrigin')) {
468
+ console.error(
469
+ 'A %s with href "%s" did not specify the crossOrigin prop. Font preloads must always use' +
470
+ ' anonymouse CORS mode. To fix add an empty string, "anonymous", or any other string' +
471
+ ' value except "use-credentials" for the crossOrigin prop of all font preloads.',
472
+ name,
473
+ href,
474
+ );
475
+ } else if (linkProps.crossOrigin === 'use-credentials') {
476
+ console.error(
477
+ 'A %s with href "%s" specified a crossOrigin value of "use-credentials". Font preloads must always use' +
478
+ ' anonymouse CORS mode. To fix use an empty string, "anonymous", or any other string' +
479
+ ' value except "use-credentials" for the crossOrigin prop of all font preloads.',
480
+ name,
481
+ href,
482
+ );
483
+ }
484
+ }
485
+ }
486
+}
487
+
488
+export function validatePreloadArguments(href: mixed, options: mixed) {
489
+ if (__DEV__) {
490
+ if (!href || typeof href !== 'string') {
491
+ const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
492
+ console.error(
493
+ 'ReactDOM.preload() expected the first argument to be a string representing an href but found %s instead.',
494
+ typeOfArg,
495
+ );
496
+ } else if (typeof options !== 'object' || options === null) {
497
+ const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
498
+ console.error(
499
+ 'ReactDOM.preload() expected the second argument to be an options argument containing at least an "as" property' +
500
+ ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
501
+ typeOfArg,
502
+ href,
503
+ );
504
+ } else {
505
+ const as = options.as;
506
+ switch (as) {
507
+ // Font specific validation of options
508
+ case 'font': {
509
+ if (options.crossOrigin === 'use-credentials') {
510
+ console.error(
511
+ 'ReactDOM.preload() was called with an "as" type of "font" and with a "crossOrigin" option of "use-credentials".' +
512
+ ' Fonts preloading must use crossOrigin "anonymous" to be functional. Please update your font preload to omit' +
513
+ ' the crossOrigin option or change it to any other value than "use-credentials" (Browsers default all other values' +
514
+ ' to anonymous mode). The href for the preload call where this warning originated is "%s"',
515
+ href,
516
+ );
517
+ }
518
+ break;
519
+ }
520
+ case 'style': {
521
+ break;
522
+ }
523
+
524
+ // We have an invalid as type and need to warn
525
+ default: {
526
+ const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
527
+ console.error(
528
+ 'ReactDOM.preload() expected a valid "as" type in the options (second) argument but found %s instead.' +
529
+ ' Please use one of the following valid values instead: %s. The href for the preload call where this' +
530
+ ' warning originated is "%s".',
531
+ typeOfAs,
532
+ '"style" and "font"',
533
+ href,
534
+ );
535
+ }
536
+ }
537
+ }
538
+ }
539
+}
540
+
541
+export function validatePreinitArguments(href: mixed, options: mixed) {
542
+ if (__DEV__) {
543
+ if (!href || typeof href !== 'string') {
544
+ const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
545
+ console.error(
546
+ 'ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.',
547
+ typeOfArg,
548
+ );
549
+ } else if (typeof options !== 'object' || options === null) {
550
+ const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
551
+ console.error(
552
+ 'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
553
+ ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
554
+ typeOfArg,
555
+ href,
556
+ );
557
+ } else {
558
+ const as = options.as;
559
+ switch (as) {
560
+ case 'font':
561
+ case 'style': {
562
+ break;
563
+ }
564
+
565
+ // We have an invalid as type and need to warn
566
+ default: {
567
+ const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
568
+ console.error(
569
+ 'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
570
+ ' specifying the Resource type. It found %s instead. Currently, the only valid resource type for preinit is "style".' +
571
+ ' The href for the preinit call where this warning originated is "%s".',
572
+ typeOfAs,
573
+ href,
574
+ );
575
+ }
576
+ }
577
+ }
578
+ }
579
+}
580
+
581
+function getValueDescriptorExpectingObjectForWarning(thing: any): string {
582
+ return thing === null
583
+ ? 'null'
584
+ : thing === undefined
585
+ ? 'undefined'
586
+ : thing === ''
587
+ ? 'an empty string'
588
+ : `something with type "${typeof thing}"`;
589
+}
590
+
591
+function getValueDescriptorExpectingEnumForWarning(thing: any): string {
592
+ return thing === null
593
+ ? 'null'
594
+ : thing === undefined
595
+ ? 'undefined'
596
+ : thing === ''
597
+ ? 'an empty string'
598
+ : typeof thing === 'string'
599
+ ? JSON.stringify(thing)
600
+ : `something with type "${typeof thing}"`;
601
+}
packages/react-dom/index.experimental.js
+2
@@ -22,3 +22,5 @@ export {
22
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
23
version,
24
} from './src/client/ReactDOM';
25
+
26
+export {preinit, preload} from 'react-dom-bindings/src/shared/ReactDOMFloat';
packages/react-dom/src/ReactDOMSharedInternals.js
+13
-22
@@ -7,29 +7,20 @@
7
* @flow
8
*/
9
10
-import {batchedUpdates} from 'react-reconciler/src/ReactFiberReconciler';
11
-import {
12
- enqueueStateRestore,
13
- restoreStateIfNeeded,
14
-} from 'react-dom-bindings/src/events/ReactDOMControlledComponent';
15
-import {
16
- getInstanceFromNode,
17
- getNodeFromInstance,
18
- getFiberCurrentPropsFromNode,
19
-} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
10
+type InternalsType = {
11
+ usingClientEntryPoint: boolean,
12
+ Events: [any, any, any, any, any, any],
13
+ Dispatcher: {
14
+ current: mixed,
15
+ },
16
+};
17
21
-const Internals = {
18
+const Internals: InternalsType = ({
19
usingClientEntryPoint: false,
23
- // Keep in sync with ReactTestUtils.js.
24
- // This is an array for better minification.
25
- Events: [
26
- getInstanceFromNode,
27
- getNodeFromInstance,
28
- getFiberCurrentPropsFromNode,
29
- enqueueStateRestore,
30
- restoreStateIfNeeded,
31
- batchedUpdates,
32
- ],
33
-};
20
+ Events: null,
21
+ Dispatcher: {
22
+ current: null,
23
+ },
24
+}: any);
25
26
export default Internals;
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+46
-316
@@ -329,6 +329,7 @@ describe('ReactDOMFizzServer', () => {
329
);
330
pipe(writable);
331
});
332
+
333
expect(getVisibleChildren(container)).toEqual(
334
<div>
335
<div>Loading...</div>
@@ -4319,70 +4320,60 @@ describe('ReactDOMFizzServer', () => {
4320
});
4321
4322
// @gate enableFloat
4322
- it('emits html and head start tags (the preamble) before other content if rendered in the shell', async () => {
4323
+ it('can emit the preamble even if the head renders asynchronously', async () => {
4324
+ function AsyncNoOutput() {
4325
+ readText('nooutput');
4326
+ return null;
4327
+ }
4328
+ function AsyncHead() {
4329
+ readText('head');
4330
+ return (
4331
+ <head data-foo="foo">
4332
+ <title>a title</title>
4333
+ </head>
4334
+ );
4335
+ }
4336
+ function AsyncBody() {
4337
+ readText('body');
4338
+ return (
4339
+ <body data-bar="bar">
4340
+ <link rel="preload" as="style" href="foo" />
4341
+ hello
4342
+ </body>
4343
+ );
4344
+ }
4345
await actIntoEmptyDocument(() => {
4346
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
4325
- <>
4326
- <title data-baz="baz">a title</title>
4327
- <html data-foo="foo">
4328
- <head data-bar="bar" />
4329
- <body>a body</body>
4330
- </html>
4331
- </>,
4347
+ <html data-html="html">
4348
+ <AsyncNoOutput />
4349
+ <AsyncHead />
4350
+ <AsyncBody />
4351
+ </html>,
4352
);
4353
pipe(writable);
4354
});
4355
+ await actIntoEmptyDocument(() => {
4356
+ resolveText('body');
4357
+ });
4358
+ await actIntoEmptyDocument(() => {
4359
+ resolveText('nooutput');
4360
+ });
4361
+ // We need to use actIntoEmptyDocument because act assumes that buffered
4362
+ // content should be fake streamed into the body which is normally true
4363
+ // but in this test the entire shell was delayed and we need the initial
4364
+ // construction to be done to get the parsing right
4365
+ await actIntoEmptyDocument(() => {
4366
+ resolveText('head');
4367
+ });
4368
expect(getVisibleChildren(document)).toEqual(
4336
- <html data-foo="foo">
4337
- <head data-bar="bar">
4338
- <title data-baz="baz">a title</title>
4369
+ <html data-html="html">
4370
+ <head data-foo="foo">
4371
+ <link rel="preload" as="style" href="foo" />
4372
+ <title>a title</title>
4373
</head>
4340
- <body>a body</body>
4374
+ <body data-bar="bar">hello</body>
4375
</html>,
4376
);
4343
-
4344
- // Hydrate the same thing on the client. We expect this to still fail because <title> is not a Resource
4345
- // and is unmatched on hydration
4346
- const errors = [];
4347
- ReactDOMClient.hydrateRoot(
4348
- document,
4349
- <>
4350
- <title data-baz="baz">a title</title>
4351
- <html data-foo="foo">
4352
- <head data-bar="bar" />
4353
- <body>a body</body>
4354
- </html>
4355
- </>,
4356
- {
4357
- onRecoverableError: (err, errInfo) => {
4358
- errors.push(err.message);
4359
- },
4360
- },
4361
- );
4362
- expect(() => {
4363
- try {
4364
- expect(() => {
4365
- expect(Scheduler).toFlushWithoutYielding();
4366
- }).toThrow('Invalid insertion of HTML node in #document node.');
4367
- } catch (e) {
4368
- console.log('e', e);
4369
- }
4370
- }).toErrorDev(
4371
- [
4372
- 'Warning: Expected server HTML to contain a matching <title> in <#document>.',
4373
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
4374
- 'Warning: validateDOMNesting(...): <title> cannot appear as a child of <#document>',
4375
- ],
4376
- {withoutStack: 1},
4377
- );
4378
- expect(errors).toEqual([
4379
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
4380
- 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
4381
- ]);
4382
- expect(getVisibleChildren(document)).toEqual();
4383
- expect(() => {
4384
- expect(Scheduler).toFlushWithoutYielding();
4385
- }).toThrow('The node to be removed is not a child of this node.');
4377
});
4378
4379
// @gate enableFloat
@@ -4431,267 +4422,6 @@ describe('ReactDOMFizzServer', () => {
4422
expect(chunks.pop()).toEqual('</body></html>');
4423
});
4424
4434
- // @gate enableFloat
4435
- it('recognizes stylesheet links as attributes during hydration', async () => {
4436
- await actIntoEmptyDocument(() => {
4437
- const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
4438
- <>
4439
- <link rel="stylesheet" href="foo" precedence="default" />
4440
- <html>
4441
- <head>
4442
- <link rel="author" precedence="this is a nonsense prop" />
4443
- </head>
4444
- <body>a body</body>
4445
- </html>
4446
- </>,
4447
- );
4448
- pipe(writable);
4449
- });
4450
- // precedence for stylesheets is mapped to a valid data attribute that is recognized on the client
4451
- // as opting this node into resource semantics. the use of precedence on the author link is just a
4452
- // non standard attribute which React allows but is not given any special treatment.
4453
- expect(getVisibleChildren(document)).toEqual(
4454
- <html>
4455
- <head>
4456
- <link rel="stylesheet" href="foo" data-rprec="default" />
4457
- <link rel="author" precedence="this is a nonsense prop" />
4458
- </head>
4459
- <body>a body</body>
4460
- </html>,
4461
- );
4462
-
4463
- // It hydrates successfully
4464
- const root = ReactDOMClient.hydrateRoot(
4465
- document,
4466
- <>
4467
- <link rel="stylesheet" href="foo" precedence="default" />
4468
- <html>
4469
- <head>
4470
- <link rel="author" precedence="this is a nonsense prop" />
4471
- </head>
4472
- <body>a body</body>
4473
- </html>
4474
- </>,
4475
- );
4476
- // We manually capture uncaught errors b/c Jest does not play well with errors thrown in
4477
- // microtasks after the test completes even when it is expecting to fail (e.g. when the gate is false)
4478
- // We need to flush the scheduler at the end even if there was an earlier throw otherwise this test will
4479
- // fail even when failure is expected. This is primarily caused by invokeGuardedCallback replaying commit
4480
- // phase errors which get rethrown in a microtask
4481
- const uncaughtErrors = [];
4482
- try {
4483
- expect(Scheduler).toFlushWithoutYielding();
4484
- expect(getVisibleChildren(document)).toEqual(
4485
- <html>
4486
- <head>
4487
- <link rel="stylesheet" href="foo" data-rprec="default" />
4488
- <link rel="author" precedence="this is a nonsense prop" />
4489
- </head>
4490
- <body>a body</body>
4491
- </html>,
4492
- );
4493
- } catch (e) {
4494
- uncaughtErrors.push(e);
4495
- }
4496
- try {
4497
- expect(Scheduler).toFlushWithoutYielding();
4498
- } catch (e) {
4499
- uncaughtErrors.push(e);
4500
- }
4501
-
4502
- root.render(
4503
- <>
4504
- <link rel="stylesheet" href="foo" precedence="default" data-bar="bar" />
4505
- <html>
4506
- <head />
4507
- <body>a body</body>
4508
- </html>
4509
- </>,
4510
- );
4511
- try {
4512
- expect(Scheduler).toFlushWithoutYielding();
4513
- expect(getVisibleChildren(document)).toEqual(
4514
- <html>
4515
- <head>
4516
- <link
4517
- rel="stylesheet"
4518
- href="foo"
4519
- data-rprec="default"
4520
- data-bar="bar"
4521
- />
4522
- </head>
4523
- <body>a body</body>
4524
- </html>,
4525
- );
4526
- } catch (e) {
4527
- uncaughtErrors.push(e);
4528
- }
4529
- try {
4530
- expect(Scheduler).toFlushWithoutYielding();
4531
- } catch (e) {
4532
- uncaughtErrors.push(e);
4533
- }
4534
-
4535
- if (uncaughtErrors.length > 0) {
4536
- throw uncaughtErrors[0];
4537
- }
4538
- });
4539
-
4540
- // Temporarily this test is expected to fail everywhere. When we have resource hoisting
4541
- // it should start to pass and we can adjust the gate accordingly
4542
- // @gate false && enableFloat
4543
- it('should insert missing resources during hydration', async () => {
4544
- await actIntoEmptyDocument(() => {
4545
- const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
4546
- <html>
4547
- <body>foo</body>
4548
- </html>,
4549
- );
4550
- pipe(writable);
4551
- });
4552
-
4553
- const uncaughtErrors = [];
4554
- ReactDOMClient.hydrateRoot(
4555
- document,
4556
- <>
4557
- <link rel="stylesheet" href="foo" precedence="foo" />
4558
- <html>
4559
- <head />
4560
- <body>foo</body>
4561
- </html>
4562
- </>,
4563
- );
4564
- try {
4565
- expect(Scheduler).toFlushWithoutYielding();
4566
- expect(getVisibleChildren(document)).toEqual(
4567
- <html>
4568
- <head>
4569
- <link rel="stylesheet" href="foo" precedence="foo" />
4570
- </head>
4571
- <body>foo</body>
4572
- </html>,
4573
- );
4574
- } catch (e) {
4575
- uncaughtErrors.push(e);
4576
- }
4577
-
4578
- // need to flush again to get the invoke guarded callback error to throw in microtask
4579
- try {
4580
- expect(Scheduler).toFlushWithoutYielding();
4581
- } catch (e) {
4582
- uncaughtErrors.push(e);
4583
- }
4584
-
4585
- if (uncaughtErrors.length) {
4586
- throw uncaughtErrors[0];
4587
- }
4588
- });
4589
-
4590
- // @gate experimental && enableFloat
4591
- it('fail hydration if a suitable resource cannot be found in the DOM for a given location (href)', async () => {
4592
- gate(flags => {
4593
- if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
4594
- throw new Error('bailing out of test');
4595
- }
4596
- });
4597
- await actIntoEmptyDocument(() => {
4598
- const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
4599
- <html>
4600
- <head />
4601
- <body>a body</body>
4602
- </html>,
4603
- );
4604
- pipe(writable);
4605
- });
4606
-
4607
- const errors = [];
4608
- ReactDOMClient.hydrateRoot(
4609
- document,
4610
- <html>
4611
- <head>
4612
- <link rel="stylesheet" href="foo" precedence="low" />
4613
- </head>
4614
- <body>a body</body>
4615
- </html>,
4616
- {
4617
- onRecoverableError(err, errInfo) {
4618
- errors.push(err.message);
4619
- },
4620
- },
4621
- );
4622
- expect(() => {
4623
- expect(Scheduler).toFlushWithoutYielding();
4624
- }).toErrorDev(
4625
- [
4626
- 'Warning: A matching Hydratable Resource was not found in the DOM for <link rel="stylesheet" href="foo">',
4627
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
4628
- ],
4629
- {withoutStack: 1},
4630
- );
4631
- expect(errors).toEqual([
4632
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
4633
- 'Hydration failed because the initial UI does not match what was rendered on the server.',
4634
- 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
4635
- ]);
4636
- });
4637
-
4638
- // @gate experimental && enableFloat
4639
- it('should error in dev when rendering more than one resource for a given location (href)', async () => {
4640
- gate(flags => {
4641
- if (!(__EXPERIMENTAL__ && flags.enableFloat)) {
4642
- throw new Error('bailing out of test');
4643
- }
4644
- });
4645
- await actIntoEmptyDocument(() => {
4646
- const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
4647
- <>
4648
- <link rel="stylesheet" href="foo" precedence="low" />
4649
- <link rel="stylesheet" href="foo" precedence="high" />
4650
- <html>
4651
- <head />
4652
- <body>a body</body>
4653
- </html>
4654
- </>,
4655
- );
4656
- pipe(writable);
4657
- });
4658
- expect(getVisibleChildren(document)).toEqual(
4659
- <html>
4660
- <head>
4661
- <link rel="stylesheet" href="foo" data-rprec="low" />
4662
- <link rel="stylesheet" href="foo" data-rprec="high" />
4663
- </head>
4664
- <body>a body</body>
4665
- </html>,
4666
- );
4667
-
4668
- const errors = [];
4669
- ReactDOMClient.hydrateRoot(
4670
- document,
4671
- <>
4672
- <html>
4673
- <head>
4674
- <link rel="stylesheet" href="foo" precedence="low" />
4675
- <link rel="stylesheet" href="foo" precedence="high" />
4676
- </head>
4677
- <body>a body</body>
4678
- </html>
4679
- </>,
4680
- {
4681
- onRecoverableError(err, errInfo) {
4682
- errors.push(err.message);
4683
- },
4684
- },
4685
- );
4686
- expect(() => {
4687
- expect(Scheduler).toFlushWithoutYielding();
4688
- }).toErrorDev([
4689
- 'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
4690
- 'Warning: Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of "foo"',
4691
- ]);
4692
- expect(errors).toEqual([]);
4693
- });
4694
-
4425
describe('text separators', () => {
4426
// To force performWork to start before resolving AsyncText but before piping we need to wait until
4427
// after scheduleWork which currently uses setImmediate to delay performWork
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
new
+3605
@@ -0,0 +1,3605 @@
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
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let JSDOM;
13
+let Stream;
14
+let Scheduler;
15
+let React;
16
+let ReactDOM;
17
+let ReactDOMClient;
18
+let ReactDOMFizzServer;
19
+let Suspense;
20
+let textCache;
21
+let document;
22
+let writable;
23
+const CSPnonce = null;
24
+let container;
25
+let buffer = '';
26
+let hasErrored = false;
27
+let fatalError = undefined;
28
+
29
+describe('ReactDOMFloat', () => {
30
+ beforeEach(() => {
31
+ jest.resetModules();
32
+ JSDOM = require('jsdom').JSDOM;
33
+ Scheduler = require('scheduler');
34
+ React = require('react');
35
+ ReactDOM = require('react-dom');
36
+ ReactDOMClient = require('react-dom/client');
37
+ ReactDOMFizzServer = require('react-dom/server');
38
+ Stream = require('stream');
39
+ Suspense = React.Suspense;
40
+
41
+ textCache = new Map();
42
+
43
+ // Test Environment
44
+ const jsdom = new JSDOM(
45
+ '<!DOCTYPE html><html><head></head><body><div id="container">',
46
+ {
47
+ runScripts: 'dangerously',
48
+ },
49
+ );
50
+ document = jsdom.window.document;
51
+ container = document.getElementById('container');
52
+
53
+ buffer = '';
54
+ hasErrored = false;
55
+
56
+ writable = new Stream.PassThrough();
57
+ writable.setEncoding('utf8');
58
+ writable.on('data', chunk => {
59
+ buffer += chunk;
60
+ });
61
+ writable.on('error', error => {
62
+ hasErrored = true;
63
+ fatalError = error;
64
+ });
65
+ });
66
+
67
+ function normalizeCodeLocInfo(str) {
68
+ return (
69
+ typeof str === 'string' &&
70
+ str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
71
+ return '\n in ' + name + ' (at **)';
72
+ })
73
+ );
74
+ }
75
+
76
+ function componentStack(components) {
77
+ return components
78
+ .map(component => `\n in ${component} (at **)`)
79
+ .join('');
80
+ }
81
+
82
+ async function act(callback) {
83
+ await callback();
84
+ // Await one turn around the event loop.
85
+ // This assumes that we'll flush everything we have so far.
86
+ await new Promise(resolve => {
87
+ setImmediate(resolve);
88
+ });
89
+ if (hasErrored) {
90
+ throw fatalError;
91
+ }
92
+ // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
93
+ // We also want to execute any scripts that are embedded.
94
+ // We assume that we have now received a proper fragment of HTML.
95
+ const bufferedContent = buffer;
96
+ buffer = '';
97
+ const fakeBody = document.createElement('body');
98
+ fakeBody.innerHTML = bufferedContent;
99
+ const parent =
100
+ container.nodeName === '#document' ? container.body : container;
101
+ while (fakeBody.firstChild) {
102
+ const node = fakeBody.firstChild;
103
+ if (
104
+ node.nodeName === 'SCRIPT' &&
105
+ (CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
106
+ ) {
107
+ const script = document.createElement('script');
108
+ script.textContent = node.textContent;
109
+ fakeBody.removeChild(node);
110
+ parent.appendChild(script);
111
+ } else {
112
+ parent.appendChild(node);
113
+ }
114
+ }
115
+ }
116
+
117
+ async function actIntoEmptyDocument(callback) {
118
+ await callback();
119
+ // Await one turn around the event loop.
120
+ // This assumes that we'll flush everything we have so far.
121
+ await new Promise(resolve => {
122
+ setImmediate(resolve);
123
+ });
124
+ if (hasErrored) {
125
+ throw fatalError;
126
+ }
127
+ // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
128
+ // We also want to execute any scripts that are embedded.
129
+ // We assume that we have now received a proper fragment of HTML.
130
+ const bufferedContent = buffer;
131
+ // Test Environment
132
+ const jsdom = new JSDOM(bufferedContent, {
133
+ runScripts: 'dangerously',
134
+ });
135
+ document = jsdom.window.document;
136
+ container = document;
137
+ buffer = '';
138
+ }
139
+
140
+ function getVisibleChildren(element) {
141
+ const children = [];
142
+ let node = element.firstChild;
143
+ while (node) {
144
+ if (node.nodeType === 1) {
145
+ if (
146
+ node.tagName !== 'SCRIPT' &&
147
+ node.tagName !== 'TEMPLATE' &&
148
+ node.tagName !== 'template' &&
149
+ !node.hasAttribute('hidden') &&
150
+ !node.hasAttribute('aria-hidden')
151
+ ) {
152
+ const props = {};
153
+ const attributes = node.attributes;
154
+ for (let i = 0; i < attributes.length; i++) {
155
+ if (
156
+ attributes[i].name === 'id' &&
157
+ attributes[i].value.includes(':')
158
+ ) {
159
+ // We assume this is a React added ID that's a non-visual implementation detail.
160
+ continue;
161
+ }
162
+ props[attributes[i].name] = attributes[i].value;
163
+ }
164
+ props.children = getVisibleChildren(node);
165
+ children.push(React.createElement(node.tagName.toLowerCase(), props));
166
+ }
167
+ } else if (node.nodeType === 3) {
168
+ children.push(node.data);
169
+ }
170
+ node = node.nextSibling;
171
+ }
172
+ return children.length === 0
173
+ ? undefined
174
+ : children.length === 1
175
+ ? children[0]
176
+ : children;
177
+ }
178
+
179
+ function resolveText(text) {
180
+ const record = textCache.get(text);
181
+ if (record === undefined) {
182
+ const newRecord = {
183
+ status: 'resolved',
184
+ value: text,
185
+ };
186
+ textCache.set(text, newRecord);
187
+ } else if (record.status === 'pending') {
188
+ const thenable = record.value;
189
+ record.status = 'resolved';
190
+ record.value = text;
191
+ thenable.pings.forEach(t => t());
192
+ }
193
+ }
194
+
195
+ function readText(text) {
196
+ const record = textCache.get(text);
197
+ if (record !== undefined) {
198
+ switch (record.status) {
199
+ case 'pending':
200
+ throw record.value;
201
+ case 'rejected':
202
+ throw record.value;
203
+ case 'resolved':
204
+ return record.value;
205
+ }
206
+ } else {
207
+ const thenable = {
208
+ pings: [],
209
+ then(resolve) {
210
+ if (newRecord.status === 'pending') {
211
+ thenable.pings.push(resolve);
212
+ } else {
213
+ Promise.resolve().then(() => resolve(newRecord.value));
214
+ }
215
+ },
216
+ };
217
+
218
+ const newRecord = {
219
+ status: 'pending',
220
+ value: thenable,
221
+ };
222
+ textCache.set(text, newRecord);
223
+
224
+ throw thenable;
225
+ }
226
+ }
227
+
228
+ function AsyncText({text}) {
229
+ return readText(text);
230
+ }
231
+
232
+ // @gate enableFloat
233
+ it('errors if the document does not contain a head when inserting a resource', async () => {
234
+ document.head.parentNode.removeChild(document.head);
235
+ const root = ReactDOMClient.createRoot(document);
236
+ root.render(
237
+ <html>
238
+ <body>
239
+ <link rel="stylesheet" href="foo" precedence="default" />
240
+ foo
241
+ </body>
242
+ </html>,
243
+ );
244
+ expect(() => {
245
+ expect(Scheduler).toFlushWithoutYielding();
246
+ }).toThrow(
247
+ 'While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.',
248
+ );
249
+ });
250
+
251
+ describe('HostResource', () => {
252
+ // @gate enableFloat
253
+ it('warns when you update props to an invalid type', async () => {
254
+ const root = ReactDOMClient.createRoot(container);
255
+ root.render(
256
+ <div>
257
+ <link rel="stylesheet" href="foo" precedence="foo" />
258
+ </div>,
259
+ );
260
+ expect(Scheduler).toFlushWithoutYielding();
261
+ root.render(
262
+ <div>
263
+ <link rel="author" href="bar" />
264
+ </div>,
265
+ );
266
+ expect(() => {
267
+ expect(Scheduler).toFlushWithoutYielding();
268
+ }).toErrorDev(
269
+ 'Warning: A <link> previously rendered as a Resource with href "foo" but was updated with a rel type that is not' +
270
+ ' valid for a Resource type. Generally Resources are not expected to ever have updated' +
271
+ ' props however in some limited circumstances it can be valid when changing the href.' +
272
+ ' When React encounters props that invalidate the Resource it is the same as not rendering' +
273
+ ' a Resource at all. valid rel types for Resources are "font" and "style". The previous' +
274
+ ' rel for this instance was "stylesheet". The updated rel is "author" and the updated href is "bar".',
275
+ );
276
+ expect(getVisibleChildren(document)).toEqual(
277
+ <html>
278
+ <head>
279
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
280
+ <link rel="preload" as="style" href="foo" />
281
+ </head>
282
+ <body>
283
+ <div id="container">
284
+ <div />
285
+ </div>
286
+ </body>
287
+ </html>,
288
+ );
289
+ });
290
+ });
291
+
292
+ describe('ReactDOM.preload', () => {
293
+ // @gate enableFloat
294
+ it('inserts a preload resource into the stream when called during server rendering', async () => {
295
+ function Component() {
296
+ ReactDOM.preload('foo', {as: 'style'});
297
+ return 'foo';
298
+ }
299
+ await actIntoEmptyDocument(() => {
300
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
301
+ <html>
302
+ <head />
303
+ <body>
304
+ <Component />
305
+ </body>
306
+ </html>,
307
+ );
308
+ pipe(writable);
309
+ });
310
+ expect(getVisibleChildren(document)).toEqual(
311
+ <html>
312
+ <head>
313
+ <link rel="preload" as="style" href="foo" />
314
+ </head>
315
+ <body>foo</body>
316
+ </html>,
317
+ );
318
+ });
319
+
320
+ // @gate enableFloat
321
+ it('inserts a preload resource into the document during render when called during client rendering', async () => {
322
+ function Component() {
323
+ ReactDOM.preload('foo', {as: 'style'});
324
+ return 'foo';
325
+ }
326
+ const root = ReactDOMClient.createRoot(container);
327
+ root.render(<Component />);
328
+ expect(Scheduler).toFlushWithoutYielding();
329
+ expect(getVisibleChildren(document)).toEqual(
330
+ <html>
331
+ <head>
332
+ <link rel="preload" as="style" href="foo" />
333
+ </head>
334
+ <body>
335
+ <div id="container">foo</div>
336
+ </body>
337
+ </html>,
338
+ );
339
+ });
340
+
341
+ // @gate enableFloat
342
+ it('inserts a preload resource when called in a layout effect', async () => {
343
+ function App() {
344
+ React.useLayoutEffect(() => {
345
+ ReactDOM.preload('foo', {as: 'style'});
346
+ }, []);
347
+ return 'foobar';
348
+ }
349
+ const root = ReactDOMClient.createRoot(container);
350
+ root.render(<App />);
351
+ expect(Scheduler).toFlushWithoutYielding();
352
+
353
+ expect(getVisibleChildren(document)).toEqual(
354
+ <html>
355
+ <head>
356
+ <link rel="preload" as="style" href="foo" />
357
+ </head>
358
+ <body>
359
+ <div id="container">foobar</div>
360
+ </body>
361
+ </html>,
362
+ );
363
+ });
364
+
365
+ // @gate enableFloat
366
+ it('inserts a preload resource when called in a passive effect', async () => {
367
+ function App() {
368
+ React.useEffect(() => {
369
+ ReactDOM.preload('foo', {as: 'style'});
370
+ }, []);
371
+ return 'foobar';
372
+ }
373
+ const root = ReactDOMClient.createRoot(container);
374
+ root.render(<App />);
375
+ expect(Scheduler).toFlushWithoutYielding();
376
+
377
+ expect(getVisibleChildren(document)).toEqual(
378
+ <html>
379
+ <head>
380
+ <link rel="preload" as="style" href="foo" />
381
+ </head>
382
+ <body>
383
+ <div id="container">foobar</div>
384
+ </body>
385
+ </html>,
386
+ );
387
+ });
388
+
389
+ // @gate enableFloat
390
+ it('inserts a preload resource when called in module scope if a root has already been created', async () => {
391
+ // The requirement that a root be created has to do with bootstrapping the dispatcher.
392
+ // We are intentionally avoiding setting it to the default via import due to cycles and
393
+ // we are trying to avoid doing a mutable initailation in module scope.
394
+ ReactDOM.preload('foo', {as: 'style'});
395
+ ReactDOMClient.createRoot(container);
396
+ ReactDOM.preload('bar', {as: 'style'});
397
+ // We need to use global.document because preload falls back
398
+ // to the window.document global when no other documents have been used
399
+ // The way the JSDOM runtim is created for these tests the local document
400
+ // global does not point to the global.document
401
+ expect(getVisibleChildren(global.document)).toEqual(
402
+ <html>
403
+ <head>
404
+ <link rel="preload" as="style" href="bar" />
405
+ </head>
406
+ <body />
407
+ </html>,
408
+ );
409
+ });
410
+ });
411
+
412
+ describe('ReactDOM.preinit as style', () => {
413
+ // @gate enableFloat
414
+ it('creates a style Resource when called during server rendering before first flush', async () => {
415
+ function Component() {
416
+ ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
417
+ return 'foo';
418
+ }
419
+ await actIntoEmptyDocument(() => {
420
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
421
+ <html>
422
+ <head />
423
+ <body>
424
+ <Component />
425
+ </body>
426
+ </html>,
427
+ );
428
+ pipe(writable);
429
+ });
430
+ expect(getVisibleChildren(document)).toEqual(
431
+ <html>
432
+ <head>
433
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
434
+ </head>
435
+ <body>foo</body>
436
+ </html>,
437
+ );
438
+ });
439
+
440
+ // @gate enableFloat
441
+ it('creates a preload Resource when called during server rendering after first flush', async () => {
442
+ function BlockedOn({text, children}) {
443
+ readText(text);
444
+ return children;
445
+ }
446
+ function Component() {
447
+ ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
448
+ return 'foo';
449
+ }
450
+ await actIntoEmptyDocument(() => {
451
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
452
+ <html>
453
+ <head />
454
+ <body>
455
+ <Suspense fallback="loading...">
456
+ <BlockedOn text="unblock">
457
+ <Component />
458
+ </BlockedOn>
459
+ </Suspense>
460
+ </body>
461
+ </html>,
462
+ );
463
+ pipe(writable);
464
+ });
465
+ await act(() => {
466
+ resolveText('unblock');
467
+ });
468
+ expect(getVisibleChildren(document)).toEqual(
469
+ <html>
470
+ <head />
471
+ <body>
472
+ foo
473
+ <link rel="preload" as="style" href="foo" />
474
+ </body>
475
+ </html>,
476
+ );
477
+ });
478
+
479
+ // @gate enableFloat
480
+ it('inserts a style Resource into the document during render when called during client rendering', async () => {
481
+ function Component() {
482
+ ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
483
+ return 'foo';
484
+ }
485
+ const root = ReactDOMClient.createRoot(container);
486
+ root.render(<Component />);
487
+ expect(Scheduler).toFlushWithoutYielding();
488
+ expect(getVisibleChildren(document)).toEqual(
489
+ <html>
490
+ <head>
491
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
492
+ </head>
493
+ <body>
494
+ <div id="container">foo</div>
495
+ </body>
496
+ </html>,
497
+ );
498
+ });
499
+
500
+ // @gate enableFloat
501
+ it('inserts a preload resource into the document when called in an insertion effect, layout effect, or passive effect', async () => {
502
+ function App() {
503
+ React.useEffect(() => {
504
+ ReactDOM.preinit('passive', {as: 'style', precedence: 'default'});
505
+ }, []);
506
+ React.useLayoutEffect(() => {
507
+ ReactDOM.preinit('layout', {as: 'style', precedence: 'default'});
508
+ });
509
+ React.useInsertionEffect(() => {
510
+ ReactDOM.preinit('insertion', {as: 'style', precedence: 'default'});
511
+ });
512
+ return 'foobar';
513
+ }
514
+ const root = ReactDOMClient.createRoot(container);
515
+ root.render(<App />);
516
+ expect(Scheduler).toFlushWithoutYielding();
517
+
518
+ expect(getVisibleChildren(document)).toEqual(
519
+ <html>
520
+ <head>
521
+ <link rel="preload" as="style" href="insertion" />
522
+ <link rel="preload" as="style" href="layout" />
523
+ <link rel="preload" as="style" href="passive" />
524
+ </head>
525
+ <body>
526
+ <div id="container">foobar</div>
527
+ </body>
528
+ </html>,
529
+ );
530
+ });
531
+
532
+ // @gate enableFloat
533
+ it('inserts a preload resource when called in module scope', async () => {
534
+ // The requirement that a root be created has to do with bootstrapping the dispatcher.
535
+ // We are intentionally avoiding setting it to the default via import due to cycles and
536
+ // we are trying to avoid doing a mutable initailation in module scope.
537
+ ReactDOM.preinit('foo', {as: 'style'});
538
+ ReactDOMClient.hydrateRoot(container, null);
539
+ ReactDOM.preinit('bar', {as: 'style'});
540
+ // We need to use global.document because preload falls back
541
+ // to the window.document global when no other documents have been used
542
+ // The way the JSDOM runtim is created for these tests the local document
543
+ // global does not point to the global.document
544
+ expect(getVisibleChildren(global.document)).toEqual(
545
+ <html>
546
+ <head>
547
+ <link rel="preload" as="style" href="bar" />
548
+ </head>
549
+ <body />
550
+ </html>,
551
+ );
552
+ });
553
+ });
554
+
555
+ describe('style resources', () => {
556
+ // @gate enableFloat
557
+ it('treats link rel stylesheet elements as a style resource when it includes a precedence when server rendering', async () => {
558
+ await actIntoEmptyDocument(() => {
559
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
560
+ <html>
561
+ <head />
562
+ <body>
563
+ <link rel="stylesheet" href="aresource" precedence="foo" />
564
+ <div>hello world</div>
565
+ </body>
566
+ </html>,
567
+ );
568
+ pipe(writable);
569
+ });
570
+
571
+ expect(getVisibleChildren(document)).toEqual(
572
+ <html>
573
+ <head>
574
+ <link rel="stylesheet" href="aresource" data-rprec="foo" />
575
+ </head>
576
+ <body>
577
+ <div>hello world</div>
578
+ </body>
579
+ </html>,
580
+ );
581
+ });
582
+
583
+ // @gate enableFloat
584
+ it('treats link rel stylesheet elements as a style resource when it includes a precedence when client rendering', async () => {
585
+ const root = ReactDOMClient.createRoot(document);
586
+ root.render(
587
+ <html>
588
+ <head />
589
+ <body>
590
+ <link rel="stylesheet" href="aresource" precedence="foo" />
591
+ <div>hello world</div>
592
+ </body>
593
+ </html>,
594
+ );
595
+ expect(Scheduler).toFlushWithoutYielding();
596
+
597
+ expect(getVisibleChildren(document)).toEqual(
598
+ <html>
599
+ <head>
600
+ <link rel="stylesheet" href="aresource" data-rprec="foo" />
601
+ </head>
602
+ <body>
603
+ <div>hello world</div>
604
+ </body>
605
+ </html>,
606
+ );
607
+ });
608
+
609
+ // @gate enableFloat
610
+ it('treats link rel stylesheet elements as a style resource when it includes a precedence when hydrating', async () => {
611
+ await actIntoEmptyDocument(() => {
612
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
613
+ <html>
614
+ <head />
615
+ <body>
616
+ <link rel="stylesheet" href="aresource" precedence="foo" />
617
+ <div>hello world</div>
618
+ </body>
619
+ </html>,
620
+ );
621
+ pipe(writable);
622
+ });
623
+ ReactDOMClient.hydrateRoot(
624
+ document,
625
+ <html>
626
+ <head />
627
+ <body>
628
+ <link rel="stylesheet" href="aresource" precedence="foo" />
629
+ <div>hello world</div>
630
+ </body>
631
+ </html>,
632
+ );
633
+ expect(Scheduler).toFlushWithoutYielding();
634
+
635
+ expect(getVisibleChildren(document)).toEqual(
636
+ <html>
637
+ <head>
638
+ <link rel="stylesheet" href="aresource" data-rprec="foo" />
639
+ </head>
640
+ <body>
641
+ <div>hello world</div>
642
+ </body>
643
+ </html>,
644
+ );
645
+ });
646
+
647
+ // @gate enableFloat
648
+ it('preloads stylesheets without a precedence prop when server rendering', async () => {
649
+ await actIntoEmptyDocument(() => {
650
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
651
+ <html>
652
+ <head />
653
+ <body>
654
+ <link rel="stylesheet" href="notaresource" />
655
+ <div>hello world</div>
656
+ </body>
657
+ </html>,
658
+ );
659
+ pipe(writable);
660
+ });
661
+
662
+ expect(getVisibleChildren(document)).toEqual(
663
+ <html>
664
+ <head>
665
+ <link rel="preload" as="style" href="notaresource" />
666
+ </head>
667
+ <body>
668
+ <link rel="stylesheet" href="notaresource" />
669
+ <div>hello world</div>
670
+ </body>
671
+ </html>,
672
+ );
673
+ });
674
+
675
+ // @gate enableFloat
676
+ it('hoists style resources to the correct precedence', async () => {
677
+ await actIntoEmptyDocument(() => {
678
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
679
+ <html>
680
+ <head />
681
+ <body>
682
+ <link rel="stylesheet" href="foo1" precedence="foo" />
683
+ <link rel="stylesheet" href="default1" precedence="default" />
684
+ <link rel="stylesheet" href="foo2" precedence="foo" />
685
+ <div>hello world</div>
686
+ </body>
687
+ </html>,
688
+ );
689
+ pipe(writable);
690
+ });
691
+ expect(getVisibleChildren(document)).toEqual(
692
+ <html>
693
+ <head>
694
+ <link rel="stylesheet" href="foo1" data-rprec="foo" />
695
+ <link rel="stylesheet" href="foo2" data-rprec="foo" />
696
+ <link rel="stylesheet" href="default1" data-rprec="default" />
697
+ </head>
698
+ <body>
699
+ <div>hello world</div>
700
+ </body>
701
+ </html>,
702
+ );
703
+
704
+ ReactDOMClient.hydrateRoot(
705
+ document,
706
+ <html>
707
+ <head />
708
+ <body>
709
+ <link rel="stylesheet" href="bar1" precedence="bar" />
710
+ <link rel="stylesheet" href="foo3" precedence="foo" />
711
+ <link rel="stylesheet" href="default2" precedence="default" />
712
+ <div>hello world</div>
713
+ </body>
714
+ </html>,
715
+ );
716
+ expect(Scheduler).toFlushWithoutYielding();
717
+ expect(getVisibleChildren(document)).toEqual(
718
+ <html>
719
+ <head>
720
+ <link rel="stylesheet" href="foo1" data-rprec="foo" />
721
+ <link rel="stylesheet" href="foo2" data-rprec="foo" />
722
+ <link rel="stylesheet" href="foo3" data-rprec="foo" />
723
+ <link rel="stylesheet" href="default1" data-rprec="default" />
724
+ <link rel="stylesheet" href="default2" data-rprec="default" />
725
+ <link rel="stylesheet" href="bar1" data-rprec="bar" />
726
+ <link rel="preload" as="style" href="bar1" />
727
+ <link rel="preload" as="style" href="foo3" />
728
+ <link rel="preload" as="style" href="default2" />
729
+ </head>
730
+ <body>
731
+ <div>hello world</div>
732
+ </body>
733
+ </html>,
734
+ );
735
+ });
736
+
737
+ // @gate enableFloat
738
+ it('retains styles even after the last referring Resource unmounts', async () => {
739
+ // This test is true until a future update where there is some form of garbage collection.
740
+ const root = ReactDOMClient.createRoot(document);
741
+
742
+ root.render(
743
+ <html>
744
+ <head />
745
+ <body>
746
+ hello world
747
+ <link rel="stylesheet" href="foo" precedence="foo" />
748
+ </body>
749
+ </html>,
750
+ );
751
+ expect(Scheduler).toFlushWithoutYielding();
752
+
753
+ root.render(
754
+ <html>
755
+ <head />
756
+ <body>hello world</body>
757
+ </html>,
758
+ );
759
+ expect(Scheduler).toFlushWithoutYielding();
760
+ expect(getVisibleChildren(document)).toEqual(
761
+ <html>
762
+ <head>
763
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
764
+ </head>
765
+ <body>hello world</body>
766
+ </html>,
767
+ );
768
+ });
769
+
770
+ // @gate enableFloat
771
+ it('retains styles even when a new html, head, and/body mount', async () => {
772
+ await actIntoEmptyDocument(() => {
773
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
774
+ <html>
775
+ <head />
776
+ <body>
777
+ <link rel="stylesheet" href="foo" precedence="foo" />
778
+ <link rel="stylesheet" href="bar" precedence="bar" />
779
+ server
780
+ </body>
781
+ </html>,
782
+ );
783
+ pipe(writable);
784
+ });
785
+ const errors = [];
786
+ ReactDOMClient.hydrateRoot(
787
+ document,
788
+ <html>
789
+ <head>
790
+ <link rel="stylesheet" href="qux" precedence="qux" />
791
+ <link rel="stylesheet" href="foo" precedence="foo" />
792
+ </head>
793
+ <body>client</body>
794
+ </html>,
795
+ {
796
+ onRecoverableError(error) {
797
+ errors.push(error.message);
798
+ },
799
+ },
800
+ );
801
+ expect(() => {
802
+ expect(Scheduler).toFlushWithoutYielding();
803
+ }).toErrorDev(
804
+ [
805
+ 'Warning: Text content did not match. Server: "server" Client: "client"',
806
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
807
+ ],
808
+ {withoutStack: 1},
809
+ );
810
+ expect(getVisibleChildren(document)).toEqual(
811
+ <html>
812
+ <head>
813
+ <link rel="stylesheet" href="qux" data-rprec="qux" />
814
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
815
+ </head>
816
+ <body>client</body>
817
+ </html>,
818
+ );
819
+ });
820
+
821
+ // Disabling for now since we are going to live with not having a restore step while we consider
822
+ // HostSingletons or other solutions
823
+ // @gate enableFloat
824
+ xit('retains styles in head through head remounts', async () => {
825
+ const root = ReactDOMClient.createRoot(document);
826
+ root.render(
827
+ <html>
828
+ <head key={1} />
829
+ <body>
830
+ <link rel="stylesheet" href="foo" precedence="foo" />
831
+ <link rel="stylesheet" href="bar" precedence="bar" />
832
+ hello
833
+ </body>
834
+ </html>,
835
+ );
836
+ expect(Scheduler).toFlushWithoutYielding();
837
+ expect(getVisibleChildren(document)).toEqual(
838
+ <html>
839
+ <head>
840
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
841
+ <link rel="stylesheet" href="bar" data-rprec="bar" />
842
+ </head>
843
+ <body>hello</body>
844
+ </html>,
845
+ );
846
+
847
+ root.render(
848
+ <html>
849
+ <head key={2} />
850
+ <body>
851
+ <link rel="stylesheet" href="foo" precedence="foo" />
852
+ <link rel="stylesheet" href="bar" precedence="bar" />
853
+ hello
854
+ </body>
855
+ </html>,
856
+ );
857
+ expect(Scheduler).toFlushWithoutYielding();
858
+ expect(getVisibleChildren(document)).toEqual(
859
+ <html>
860
+ <head>
861
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
862
+ <link rel="stylesheet" href="bar" data-rprec="bar" />
863
+ </head>
864
+ <body>hello</body>
865
+ </html>,
866
+ );
867
+ });
868
+ });
869
+
870
+ // @gate enableFloat
871
+ it('client renders a boundary if a style Resource dependency fails to load', async () => {
872
+ function BlockedOn({text, children}) {
873
+ readText(text);
874
+ return children;
875
+ }
876
+ function App() {
877
+ return (
878
+ <html>
879
+ <head />
880
+ <body>
881
+ <Suspense fallback="loading...">
882
+ <BlockedOn text="unblock">
883
+ <link rel="stylesheet" href="foo" precedence="arbitrary" />
884
+ <link rel="stylesheet" href="bar" precedence="arbitrary" />
885
+ Hello
886
+ </BlockedOn>
887
+ </Suspense>
888
+ </body>
889
+ </html>
890
+ );
891
+ }
892
+ await actIntoEmptyDocument(() => {
893
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
894
+ pipe(writable);
895
+ });
896
+
897
+ await act(() => {
898
+ resolveText('unblock');
899
+ });
900
+
901
+ expect(getVisibleChildren(document)).toEqual(
902
+ <html>
903
+ <head>
904
+ <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
905
+ <link rel="stylesheet" href="bar" data-rprec="arbitrary" />
906
+ </head>
907
+ <body>
908
+ loading...
909
+ <link rel="preload" href="foo" as="style" />
910
+ <link rel="preload" href="bar" as="style" />
911
+ </body>
912
+ </html>,
913
+ );
914
+
915
+ await act(() => {
916
+ const barLink = document.querySelector(
917
+ 'link[rel="stylesheet"][href="bar"]',
918
+ );
919
+ const event = document.createEvent('Events');
920
+ event.initEvent('error', true, true);
921
+ barLink.dispatchEvent(event);
922
+ });
923
+
924
+ const boundaryTemplateInstance = document.getElementById('B:0');
925
+ const suspenseInstance = boundaryTemplateInstance.previousSibling;
926
+
927
+ expect(suspenseInstance.data).toEqual('$!');
928
+ expect(boundaryTemplateInstance.dataset.dgst).toBe(
929
+ 'Resource failed to load',
930
+ );
931
+
932
+ expect(getVisibleChildren(document)).toEqual(
933
+ <html>
934
+ <head>
935
+ <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
936
+ <link rel="stylesheet" href="bar" data-rprec="arbitrary" />
937
+ </head>
938
+ <body>
939
+ loading...
940
+ <link rel="preload" href="foo" as="style" />
941
+ <link rel="preload" href="bar" as="style" />
942
+ </body>
943
+ </html>,
944
+ );
945
+
946
+ const errors = [];
947
+ ReactDOMClient.hydrateRoot(document, <App />, {
948
+ onRecoverableError(err, errInfo) {
949
+ errors.push(err.message);
950
+ errors.push(err.digest);
951
+ },
952
+ });
953
+ expect(Scheduler).toFlushWithoutYielding();
954
+ expect(getVisibleChildren(document)).toEqual(
955
+ <html>
956
+ <head>
957
+ <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
958
+ <link rel="stylesheet" href="bar" data-rprec="arbitrary" />
959
+ </head>
960
+ <body>
961
+ <link rel="preload" href="foo" as="style" />
962
+ <link rel="preload" href="bar" as="style" />
963
+ Hello
964
+ </body>
965
+ </html>,
966
+ );
967
+ expect(errors).toEqual([
968
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
969
+ 'Resource failed to load',
970
+ ]);
971
+ });
972
+
973
+ // @gate enableFloat
974
+ it('treats stylesheet links with a precedence as a resource', async () => {
975
+ await actIntoEmptyDocument(() => {
976
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
977
+ <html>
978
+ <head />
979
+ <body>
980
+ <link rel="stylesheet" href="foo" precedence="arbitrary" />
981
+ Hello
982
+ </body>
983
+ </html>,
984
+ );
985
+ pipe(writable);
986
+ });
987
+ expect(getVisibleChildren(document)).toEqual(
988
+ <html>
989
+ <head>
990
+ <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
991
+ </head>
992
+ <body>Hello</body>
993
+ </html>,
994
+ );
995
+
996
+ ReactDOMClient.hydrateRoot(
997
+ document,
998
+ <html>
999
+ <head />
1000
+ <body>Hello</body>
1001
+ </html>,
1002
+ );
1003
+ expect(Scheduler).toFlushWithoutYielding();
1004
+ expect(getVisibleChildren(document)).toEqual(
1005
+ <html>
1006
+ <head>
1007
+ <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
1008
+ </head>
1009
+ <body>Hello</body>
1010
+ </html>,
1011
+ );
1012
+ });
1013
+
1014
+ // @gate enableFloat
1015
+ it('inserts text separators following text when followed by an element that is converted to a resource and thus removed from the html inline', async () => {
1016
+ // If you render many of these as siblings the values get emitted as a single text with no separator sometimes
1017
+ // because the link gets elided as a resource
1018
+ function AsyncTextWithResource({text, href, precedence}) {
1019
+ const value = readText(text);
1020
+ return (
1021
+ <>
1022
+ {value}
1023
+ <link rel="stylesheet" href={href} precedence={precedence} />
1024
+ </>
1025
+ );
1026
+ }
1027
+
1028
+ await actIntoEmptyDocument(() => {
1029
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1030
+ <html>
1031
+ <head />
1032
+ <body>
1033
+ <AsyncTextWithResource text="foo" href="foo" precedence="one" />
1034
+ <AsyncTextWithResource text="bar" href="bar" precedence="two" />
1035
+ <AsyncTextWithResource text="baz" href="baz" precedence="three" />
1036
+ </body>
1037
+ </html>,
1038
+ );
1039
+ pipe(writable);
1040
+ resolveText('foo');
1041
+ resolveText('bar');
1042
+ resolveText('baz');
1043
+ });
1044
+
1045
+ expect(getVisibleChildren(document)).toEqual(
1046
+ <html>
1047
+ <head>
1048
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1049
+ <link rel="stylesheet" href="bar" data-rprec="two" />
1050
+ <link rel="stylesheet" href="baz" data-rprec="three" />
1051
+ </head>
1052
+ <body>
1053
+ {'foo'}
1054
+ {'bar'}
1055
+ {'baz'}
1056
+ </body>
1057
+ </html>,
1058
+ );
1059
+ });
1060
+
1061
+ // @gate enableFloat
1062
+ it('hoists late stylesheets the correct precedence', async () => {
1063
+ function AsyncListItemWithResource({text, href, precedence, ...rest}) {
1064
+ const value = readText(text);
1065
+ return (
1066
+ <li>
1067
+ <link
1068
+ rel="stylesheet"
1069
+ href={href}
1070
+ precedence={precedence}
1071
+ {...rest}
1072
+ />
1073
+ {value}
1074
+ </li>
1075
+ );
1076
+ }
1077
+ function BlockingChildren({text, children}) {
1078
+ readText(text);
1079
+ return children;
1080
+ }
1081
+ await actIntoEmptyDocument(() => {
1082
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1083
+ <html>
1084
+ <head />
1085
+ <body>
1086
+ <link rel="stylesheet" href="initial" precedence="one" />
1087
+ <div>
1088
+ <Suspense fallback="loading foo bar...">
1089
+ <link rel="stylesheet" href="foo" precedence="one" />
1090
+ <ul>
1091
+ <li>
1092
+ <AsyncText text="foo" />
1093
+ </li>
1094
+ <AsyncListItemWithResource
1095
+ text="bar"
1096
+ href="bar"
1097
+ precedence="default"
1098
+ data-foo="foo"
1099
+ crossOrigin="anonymous"
1100
+ />
1101
+ </ul>
1102
+ </Suspense>
1103
+ </div>
1104
+ <div>
1105
+ <Suspense fallback="loading bar baz qux...">
1106
+ <ul>
1107
+ <AsyncListItemWithResource
1108
+ text="bar"
1109
+ href="bar"
1110
+ precedence="default"
1111
+ />
1112
+ <AsyncListItemWithResource
1113
+ text="baz"
1114
+ href="baz"
1115
+ precedence="two"
1116
+ />
1117
+ <AsyncListItemWithResource
1118
+ text="qux"
1119
+ href="qux"
1120
+ precedence="one"
1121
+ />
1122
+ </ul>
1123
+ </Suspense>
1124
+ </div>
1125
+ <div>
1126
+ <Suspense fallback="loading bar baz qux...">
1127
+ <BlockingChildren text="unblock">
1128
+ <ul>
1129
+ <AsyncListItemWithResource
1130
+ text="bar"
1131
+ href="bar"
1132
+ precedence="default"
1133
+ />
1134
+ <AsyncListItemWithResource
1135
+ text="baz"
1136
+ href="baz"
1137
+ precedence="two"
1138
+ />
1139
+ <AsyncListItemWithResource
1140
+ text="qux"
1141
+ href="qux"
1142
+ precedence="one"
1143
+ />
1144
+ </ul>
1145
+ </BlockingChildren>
1146
+ </Suspense>
1147
+ </div>
1148
+ </body>
1149
+ </html>,
1150
+ );
1151
+ pipe(writable);
1152
+ });
1153
+
1154
+ expect(getVisibleChildren(document)).toEqual(
1155
+ <html>
1156
+ <head>
1157
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1158
+ <link rel="preload" href="foo" as="style" />
1159
+ </head>
1160
+ <body>
1161
+ <div>loading foo bar...</div>
1162
+ <div>loading bar baz qux...</div>
1163
+ <div>loading bar baz qux...</div>
1164
+ </body>
1165
+ </html>,
1166
+ );
1167
+
1168
+ await act(() => {
1169
+ resolveText('foo');
1170
+ resolveText('bar');
1171
+ });
1172
+
1173
+ expect(getVisibleChildren(document)).toEqual(
1174
+ <html>
1175
+ <head>
1176
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1177
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1178
+ <link
1179
+ rel="stylesheet"
1180
+ href="bar"
1181
+ data-rprec="default"
1182
+ data-foo="foo"
1183
+ crossorigin="anonymous"
1184
+ />
1185
+ <link rel="preload" href="foo" as="style" />
1186
+ </head>
1187
+ <body>
1188
+ <div>loading foo bar...</div>
1189
+ <div>loading bar baz qux...</div>
1190
+ <div>loading bar baz qux...</div>
1191
+ <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
1192
+ </body>
1193
+ </html>,
1194
+ );
1195
+
1196
+ await act(() => {
1197
+ const link = document.querySelector('link[rel="stylesheet"][href="foo"]');
1198
+ const event = document.createEvent('Events');
1199
+ event.initEvent('load', true, true);
1200
+ link.dispatchEvent(event);
1201
+ });
1202
+
1203
+ expect(getVisibleChildren(document)).toEqual(
1204
+ <html>
1205
+ <head>
1206
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1207
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1208
+ <link
1209
+ rel="stylesheet"
1210
+ href="bar"
1211
+ data-rprec="default"
1212
+ data-foo="foo"
1213
+ crossorigin="anonymous"
1214
+ />
1215
+ <link rel="preload" href="foo" as="style" />
1216
+ </head>
1217
+ <body>
1218
+ <div>loading foo bar...</div>
1219
+ <div>loading bar baz qux...</div>
1220
+ <div>loading bar baz qux...</div>
1221
+ <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
1222
+ </body>
1223
+ </html>,
1224
+ );
1225
+
1226
+ await act(() => {
1227
+ const link = document.querySelector('link[rel="stylesheet"][href="bar"]');
1228
+ const event = document.createEvent('Events');
1229
+ event.initEvent('load', true, true);
1230
+ link.dispatchEvent(event);
1231
+ });
1232
+
1233
+ expect(getVisibleChildren(document)).toEqual(
1234
+ <html>
1235
+ <head>
1236
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1237
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1238
+ <link
1239
+ rel="stylesheet"
1240
+ href="bar"
1241
+ data-rprec="default"
1242
+ data-foo="foo"
1243
+ crossorigin="anonymous"
1244
+ />
1245
+ <link rel="preload" href="foo" as="style" />
1246
+ </head>
1247
+ <body>
1248
+ <div>
1249
+ <ul>
1250
+ <li>foo</li>
1251
+ <li>bar</li>
1252
+ </ul>
1253
+ </div>
1254
+ <div>loading bar baz qux...</div>
1255
+ <div>loading bar baz qux...</div>
1256
+ <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
1257
+ </body>
1258
+ </html>,
1259
+ );
1260
+
1261
+ await act(() => {
1262
+ resolveText('baz');
1263
+ });
1264
+
1265
+ expect(getVisibleChildren(document)).toEqual(
1266
+ <html>
1267
+ <head>
1268
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1269
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1270
+ <link
1271
+ rel="stylesheet"
1272
+ href="bar"
1273
+ data-rprec="default"
1274
+ data-foo="foo"
1275
+ crossorigin="anonymous"
1276
+ />
1277
+ <link rel="preload" href="foo" as="style" />
1278
+ </head>
1279
+ <body>
1280
+ <div>
1281
+ <ul>
1282
+ <li>foo</li>
1283
+ <li>bar</li>
1284
+ </ul>
1285
+ </div>
1286
+ <div>loading bar baz qux...</div>
1287
+ <div>loading bar baz qux...</div>
1288
+ <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
1289
+ <link rel="preload" as="style" href="baz" />
1290
+ </body>
1291
+ </html>,
1292
+ );
1293
+
1294
+ await act(() => {
1295
+ resolveText('qux');
1296
+ });
1297
+
1298
+ expect(getVisibleChildren(document)).toEqual(
1299
+ <html>
1300
+ <head>
1301
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1302
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1303
+ <link rel="stylesheet" href="qux" data-rprec="one" />
1304
+ <link
1305
+ rel="stylesheet"
1306
+ href="bar"
1307
+ data-rprec="default"
1308
+ data-foo="foo"
1309
+ crossorigin="anonymous"
1310
+ />
1311
+ <link rel="stylesheet" href="baz" data-rprec="two" />
1312
+ <link rel="preload" href="foo" as="style" />
1313
+ </head>
1314
+ <body>
1315
+ <div>
1316
+ <ul>
1317
+ <li>foo</li>
1318
+ <li>bar</li>
1319
+ </ul>
1320
+ </div>
1321
+ <div>loading bar baz qux...</div>
1322
+ <div>loading bar baz qux...</div>
1323
+ <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
1324
+ <link rel="preload" as="style" href="baz" />
1325
+ <link rel="preload" as="style" href="qux" />
1326
+ </body>
1327
+ </html>,
1328
+ );
1329
+
1330
+ await act(() => {
1331
+ const bazlink = document.querySelector(
1332
+ 'link[rel="stylesheet"][href="baz"]',
1333
+ );
1334
+ const quxlink = document.querySelector(
1335
+ 'link[rel="stylesheet"][href="qux"]',
1336
+ );
1337
+ const event = document.createEvent('Events');
1338
+ event.initEvent('load', true, true);
1339
+ bazlink.dispatchEvent(event);
1340
+ quxlink.dispatchEvent(event);
1341
+ });
1342
+
1343
+ expect(getVisibleChildren(document)).toEqual(
1344
+ <html>
1345
+ <head>
1346
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1347
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1348
+ <link rel="stylesheet" href="qux" data-rprec="one" />
1349
+ <link
1350
+ rel="stylesheet"
1351
+ href="bar"
1352
+ data-rprec="default"
1353
+ data-foo="foo"
1354
+ crossorigin="anonymous"
1355
+ />
1356
+ <link rel="stylesheet" href="baz" data-rprec="two" />
1357
+ <link rel="preload" href="foo" as="style" />
1358
+ </head>
1359
+ <body>
1360
+ <div>
1361
+ <ul>
1362
+ <li>foo</li>
1363
+ <li>bar</li>
1364
+ </ul>
1365
+ </div>
1366
+ <div>
1367
+ <ul>
1368
+ <li>bar</li>
1369
+ <li>baz</li>
1370
+ <li>qux</li>
1371
+ </ul>
1372
+ </div>
1373
+ <div>loading bar baz qux...</div>
1374
+ <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
1375
+ <link rel="preload" as="style" href="baz" />
1376
+ <link rel="preload" as="style" href="qux" />
1377
+ </body>
1378
+ </html>,
1379
+ );
1380
+
1381
+ await act(() => {
1382
+ resolveText('unblock');
1383
+ });
1384
+
1385
+ expect(getVisibleChildren(document)).toEqual(
1386
+ <html>
1387
+ <head>
1388
+ <link rel="stylesheet" href="initial" data-rprec="one" />
1389
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1390
+ <link rel="stylesheet" href="qux" data-rprec="one" />
1391
+ <link
1392
+ rel="stylesheet"
1393
+ href="bar"
1394
+ data-rprec="default"
1395
+ data-foo="foo"
1396
+ crossorigin="anonymous"
1397
+ />
1398
+ <link rel="stylesheet" href="baz" data-rprec="two" />
1399
+ <link rel="preload" href="foo" as="style" />
1400
+ </head>
1401
+ <body>
1402
+ <div>
1403
+ <ul>
1404
+ <li>foo</li>
1405
+ <li>bar</li>
1406
+ </ul>
1407
+ </div>
1408
+ <div>
1409
+ <ul>
1410
+ <li>bar</li>
1411
+ <li>baz</li>
1412
+ <li>qux</li>
1413
+ </ul>
1414
+ </div>
1415
+ <div>
1416
+ <ul>
1417
+ <li>bar</li>
1418
+ <li>baz</li>
1419
+ <li>qux</li>
1420
+ </ul>
1421
+ </div>
1422
+ <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
1423
+ <link rel="preload" as="style" href="baz" />
1424
+ <link rel="preload" as="style" href="qux" />
1425
+ </body>
1426
+ </html>,
1427
+ );
1428
+ });
1429
+
1430
+ // @gate enableFloat
1431
+ it('normalizes style resource precedence for all boundaries inlined as part of the shell flush', async () => {
1432
+ await actIntoEmptyDocument(() => {
1433
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1434
+ <html>
1435
+ <head />
1436
+ <body>
1437
+ <div>
1438
+ outer
1439
+ <link rel="stylesheet" href="1one" precedence="one" />
1440
+ <link rel="stylesheet" href="1two" precedence="two" />
1441
+ <link rel="stylesheet" href="1three" precedence="three" />
1442
+ <link rel="stylesheet" href="1four" precedence="four" />
1443
+ <Suspense fallback={null}>
1444
+ <div>
1445
+ middle
1446
+ <link rel="stylesheet" href="2one" precedence="one" />
1447
+ <link rel="stylesheet" href="2two" precedence="two" />
1448
+ <link rel="stylesheet" href="2three" precedence="three" />
1449
+ <link rel="stylesheet" href="2four" precedence="four" />
1450
+ <Suspense fallback={null}>
1451
+ <div>
1452
+ inner
1453
+ <link rel="stylesheet" href="3five" precedence="five" />
1454
+ <link rel="stylesheet" href="3one" precedence="one" />
1455
+ <link rel="stylesheet" href="3two" precedence="two" />
1456
+ <link rel="stylesheet" href="3three" precedence="three" />
1457
+ <link rel="stylesheet" href="3four" precedence="four" />
1458
+ </div>
1459
+ </Suspense>
1460
+ </div>
1461
+ </Suspense>
1462
+ <Suspense fallback={null}>
1463
+ <div>middle</div>
1464
+ <link rel="stylesheet" href="4one" precedence="one" />
1465
+ <link rel="stylesheet" href="4two" precedence="two" />
1466
+ <link rel="stylesheet" href="4three" precedence="three" />
1467
+ <link rel="stylesheet" href="4four" precedence="four" />
1468
+ </Suspense>
1469
+ </div>
1470
+ </body>
1471
+ </html>,
1472
+ );
1473
+ pipe(writable);
1474
+ });
1475
+
1476
+ // The reason the href's aren't ordered linearly is that when boundaries complete their resources
1477
+ // get hoisted to the shell directly so they can flush in the head. If a boundary doesn't suspend then
1478
+ // child boundaries will complete before the parent boundary and thus have their resources hoist
1479
+ // early. The reason precedences are still ordered correctly between child and parent is because
1480
+ // the precedence ordering is determined upon first discovernig a resource rather than on hoist and
1481
+ // so it follows render order
1482
+ expect(getVisibleChildren(document)).toEqual(
1483
+ <html>
1484
+ <head>
1485
+ <link rel="stylesheet" href="1one" data-rprec="one" />
1486
+ <link rel="stylesheet" href="3one" data-rprec="one" />
1487
+ <link rel="stylesheet" href="2one" data-rprec="one" />
1488
+ <link rel="stylesheet" href="4one" data-rprec="one" />
1489
+
1490
+ <link rel="stylesheet" href="1two" data-rprec="two" />
1491
+ <link rel="stylesheet" href="3two" data-rprec="two" />
1492
+ <link rel="stylesheet" href="2two" data-rprec="two" />
1493
+ <link rel="stylesheet" href="4two" data-rprec="two" />
1494
+
1495
+ <link rel="stylesheet" href="1three" data-rprec="three" />
1496
+ <link rel="stylesheet" href="3three" data-rprec="three" />
1497
+ <link rel="stylesheet" href="2three" data-rprec="three" />
1498
+ <link rel="stylesheet" href="4three" data-rprec="three" />
1499
+
1500
+ <link rel="stylesheet" href="1four" data-rprec="four" />
1501
+ <link rel="stylesheet" href="3four" data-rprec="four" />
1502
+ <link rel="stylesheet" href="2four" data-rprec="four" />
1503
+ <link rel="stylesheet" href="4four" data-rprec="four" />
1504
+
1505
+ <link rel="stylesheet" href="3five" data-rprec="five" />
1506
+ </head>
1507
+ <body>
1508
+ <div>
1509
+ outer
1510
+ <div>
1511
+ middle<div>inner</div>
1512
+ </div>
1513
+ <div>middle</div>
1514
+ </div>
1515
+ </body>
1516
+ </html>,
1517
+ );
1518
+ });
1519
+
1520
+ // @gate enableFloat
1521
+ it('style resources are inserted according to precedence order on the client', async () => {
1522
+ await actIntoEmptyDocument(() => {
1523
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1524
+ <html>
1525
+ <head />
1526
+ <body>
1527
+ <div>
1528
+ <link rel="stylesheet" href="foo" precedence="one" />
1529
+ <link rel="stylesheet" href="bar" precedence="two" />
1530
+ Hello
1531
+ </div>
1532
+ </body>
1533
+ </html>,
1534
+ );
1535
+ pipe(writable);
1536
+ });
1537
+
1538
+ expect(getVisibleChildren(document)).toEqual(
1539
+ <html>
1540
+ <head>
1541
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1542
+ <link rel="stylesheet" href="bar" data-rprec="two" />
1543
+ </head>
1544
+ <body>
1545
+ <div>Hello</div>
1546
+ </body>
1547
+ </html>,
1548
+ );
1549
+
1550
+ const root = ReactDOMClient.hydrateRoot(
1551
+ document,
1552
+ <html>
1553
+ <head />
1554
+ <body>
1555
+ <div>
1556
+ <link rel="stylesheet" href="foo" precedence="one" />
1557
+ <link rel="stylesheet" href="bar" precedence="two" />
1558
+ Hello
1559
+ </div>
1560
+ </body>
1561
+ </html>,
1562
+ );
1563
+ expect(Scheduler).toFlushWithoutYielding();
1564
+ expect(getVisibleChildren(document)).toEqual(
1565
+ <html>
1566
+ <head>
1567
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1568
+ <link rel="stylesheet" href="bar" data-rprec="two" />
1569
+ </head>
1570
+ <body>
1571
+ <div>Hello</div>
1572
+ </body>
1573
+ </html>,
1574
+ );
1575
+
1576
+ root.render(
1577
+ <html>
1578
+ <head />
1579
+ <body>
1580
+ <div>Hello</div>
1581
+ <link rel="stylesheet" href="baz" precedence="one" />
1582
+ </body>
1583
+ </html>,
1584
+ );
1585
+ expect(Scheduler).toFlushWithoutYielding();
1586
+ expect(getVisibleChildren(document)).toEqual(
1587
+ <html>
1588
+ <head>
1589
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1590
+ <link rel="stylesheet" href="baz" data-rprec="one" />
1591
+ <link rel="stylesheet" href="bar" data-rprec="two" />
1592
+ <link rel="preload" as="style" href="baz" />
1593
+ </head>
1594
+ <body>
1595
+ <div>Hello</div>
1596
+ </body>
1597
+ </html>,
1598
+ );
1599
+ });
1600
+
1601
+ // @gate enableFloat
1602
+ it('inserts preloads in render phase eagerly', async () => {
1603
+ function Throw() {
1604
+ throw new Error('Uh oh!');
1605
+ }
1606
+ class ErrorBoundary extends React.Component {
1607
+ state = {hasError: false, error: null};
1608
+ static getDerivedStateFromError(error) {
1609
+ return {
1610
+ hasError: true,
1611
+ error,
1612
+ };
1613
+ }
1614
+ render() {
1615
+ if (this.state.hasError) {
1616
+ return this.state.error.message;
1617
+ }
1618
+ return this.props.children;
1619
+ }
1620
+ }
1621
+
1622
+ const root = ReactDOMClient.createRoot(container);
1623
+ root.render(
1624
+ <ErrorBoundary>
1625
+ <link rel="stylesheet" href="foo" precedence="default" />
1626
+ <div>foo</div>
1627
+ <Throw />
1628
+ </ErrorBoundary>,
1629
+ );
1630
+ expect(Scheduler).toFlushWithoutYielding();
1631
+ expect(getVisibleChildren(document)).toEqual(
1632
+ <html>
1633
+ <head>
1634
+ <link rel="preload" href="foo" as="style" />
1635
+ </head>
1636
+ <body>
1637
+ <div id="container">Uh oh!</div>
1638
+ </body>
1639
+ </html>,
1640
+ );
1641
+ });
1642
+
1643
+ // @gate enableFloat
1644
+ it('does not emit preinit stylesheets if they are invoked after the shell flushes', async () => {
1645
+ function PreinitsBlockedOn({text}) {
1646
+ readText(text);
1647
+ ReactDOM.preinit('one', {precedence: 'one', as: 'style'});
1648
+ ReactDOM.preinit('two', {precedence: 'two', as: 'style'});
1649
+ return null;
1650
+ }
1651
+ await actIntoEmptyDocument(() => {
1652
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1653
+ <html>
1654
+ <head />
1655
+ <body>
1656
+ <div>
1657
+ <link rel="stylesheet" href="foo" precedence="one" />
1658
+ <link rel="stylesheet" href="bar" precedence="two" />
1659
+ Hello
1660
+ </div>
1661
+ <div>
1662
+ <Suspense fallback={'loading...'}>
1663
+ <PreinitsBlockedOn text="foo" />
1664
+ <AsyncText text="bar" />
1665
+ </Suspense>
1666
+ </div>
1667
+ </body>
1668
+ </html>,
1669
+ );
1670
+ pipe(writable);
1671
+ });
1672
+
1673
+ expect(getVisibleChildren(document)).toEqual(
1674
+ <html>
1675
+ <head>
1676
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1677
+ <link rel="stylesheet" href="bar" data-rprec="two" />
1678
+ </head>
1679
+ <body>
1680
+ <div>Hello</div>
1681
+ <div>loading...</div>
1682
+ </body>
1683
+ </html>,
1684
+ );
1685
+
1686
+ await act(() => {
1687
+ resolveText('foo');
1688
+ });
1689
+ expect(getVisibleChildren(document)).toEqual(
1690
+ <html>
1691
+ <head>
1692
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1693
+ <link rel="stylesheet" href="bar" data-rprec="two" />
1694
+ </head>
1695
+ <body>
1696
+ <div>Hello</div>
1697
+ <div>loading...</div>
1698
+ <link rel="preload" href="one" as="style" />
1699
+ <link rel="preload" href="two" as="style" />
1700
+ </body>
1701
+ </html>,
1702
+ );
1703
+
1704
+ await act(() => {
1705
+ resolveText('bar');
1706
+ });
1707
+ expect(getVisibleChildren(document)).toEqual(
1708
+ <html>
1709
+ <head>
1710
+ <link rel="stylesheet" href="foo" data-rprec="one" />
1711
+ <link rel="stylesheet" href="bar" data-rprec="two" />
1712
+ </head>
1713
+ <body>
1714
+ <div>Hello</div>
1715
+ <div>bar</div>
1716
+ <link rel="preload" href="one" as="style" />
1717
+ <link rel="preload" href="two" as="style" />
1718
+ </body>
1719
+ </html>,
1720
+ );
1721
+ });
1722
+
1723
+ // @gate enableFloat
1724
+ it('will include child boundary style resources in the boundary reveal instruction', async () => {
1725
+ function BlockedOn({text, children}) {
1726
+ readText(text);
1727
+ return children;
1728
+ }
1729
+ await actIntoEmptyDocument(() => {
1730
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1731
+ <html>
1732
+ <head />
1733
+ <body>
1734
+ <div>
1735
+ <Suspense fallback="loading foo...">
1736
+ <BlockedOn text="foo">
1737
+ <div>foo</div>
1738
+ <link rel="stylesheet" href="foo" precedence="default" />
1739
+ <Suspense fallback="loading bar...">
1740
+ <BlockedOn text="bar">
1741
+ <div>bar</div>
1742
+ <link rel="stylesheet" href="bar" precedence="default" />
1743
+ <Suspense fallback="loading baz...">
1744
+ <BlockedOn text="baz">
1745
+ <div>baz</div>
1746
+ <link
1747
+ rel="stylesheet"
1748
+ href="baz"
1749
+ precedence="default"
1750
+ />
1751
+ </BlockedOn>
1752
+ </Suspense>
1753
+ </BlockedOn>
1754
+ </Suspense>
1755
+ </BlockedOn>
1756
+ </Suspense>
1757
+ </div>
1758
+ </body>
1759
+ </html>,
1760
+ );
1761
+ pipe(writable);
1762
+ });
1763
+
1764
+ expect(getVisibleChildren(document)).toEqual(
1765
+ <html>
1766
+ <head />
1767
+ <body>
1768
+ <div>loading foo...</div>
1769
+ </body>
1770
+ </html>,
1771
+ );
1772
+
1773
+ await act(() => {
1774
+ resolveText('bar');
1775
+ });
1776
+ expect(getVisibleChildren(document)).toEqual(
1777
+ <html>
1778
+ <head />
1779
+ <body>
1780
+ <div>loading foo...</div>
1781
+ </body>
1782
+ </html>,
1783
+ );
1784
+
1785
+ await act(() => {
1786
+ resolveText('baz');
1787
+ });
1788
+ expect(getVisibleChildren(document)).toEqual(
1789
+ <html>
1790
+ <head />
1791
+ <body>
1792
+ <div>loading foo...</div>
1793
+ </body>
1794
+ </html>,
1795
+ );
1796
+
1797
+ await act(() => {
1798
+ resolveText('foo');
1799
+ });
1800
+ expect(getVisibleChildren(document)).toEqual(
1801
+ <html>
1802
+ <head>
1803
+ <link rel="stylesheet" href="foo" data-rprec="default" />
1804
+ <link rel="stylesheet" href="bar" data-rprec="default" />
1805
+ <link rel="stylesheet" href="baz" data-rprec="default" />
1806
+ </head>
1807
+ <body>
1808
+ <div>loading foo...</div>
1809
+ <link rel="preload" href="foo" as="style" />
1810
+ <link rel="preload" href="bar" as="style" />
1811
+ <link rel="preload" href="baz" as="style" />
1812
+ </body>
1813
+ </html>,
1814
+ );
1815
+
1816
+ await act(() => {
1817
+ const event = document.createEvent('Events');
1818
+ event.initEvent('load', true, true);
1819
+ Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
1820
+ el => {
1821
+ el.dispatchEvent(event);
1822
+ },
1823
+ );
1824
+ });
1825
+ expect(getVisibleChildren(document)).toEqual(
1826
+ <html>
1827
+ <head>
1828
+ <link rel="stylesheet" href="foo" data-rprec="default" />
1829
+ <link rel="stylesheet" href="bar" data-rprec="default" />
1830
+ <link rel="stylesheet" href="baz" data-rprec="default" />
1831
+ </head>
1832
+ <body>
1833
+ <div>
1834
+ <div>foo</div>
1835
+ <div>bar</div>
1836
+ <div>baz</div>
1837
+ </div>
1838
+ <link rel="preload" href="foo" as="style" />
1839
+ <link rel="preload" href="bar" as="style" />
1840
+ <link rel="preload" href="baz" as="style" />
1841
+ </body>
1842
+ </html>,
1843
+ );
1844
+ });
1845
+
1846
+ // @gate enableFloat
1847
+ it('will hoist resources of child boundaries emitted as part of a partial boundary to the parent boundary', async () => {
1848
+ function BlockedOn({text, children}) {
1849
+ readText(text);
1850
+ return children;
1851
+ }
1852
+ await actIntoEmptyDocument(() => {
1853
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1854
+ <html>
1855
+ <head />
1856
+ <body>
1857
+ <div>
1858
+ <Suspense fallback="loading...">
1859
+ <div>
1860
+ <BlockedOn text="foo">
1861
+ <div>foo</div>
1862
+ <link rel="stylesheet" href="foo" precedence="default" />
1863
+ <Suspense fallback="loading bar...">
1864
+ <BlockedOn text="bar">
1865
+ <div>bar</div>
1866
+ <link
1867
+ rel="stylesheet"
1868
+ href="bar"
1869
+ precedence="default"
1870
+ />
1871
+ <Suspense fallback="loading baz...">
1872
+ <div>
1873
+ <BlockedOn text="baz">
1874
+ <div>baz</div>
1875
+ <link
1876
+ rel="stylesheet"
1877
+ href="baz"
1878
+ precedence="default"
1879
+ />
1880
+ </BlockedOn>
1881
+ </div>
1882
+ </Suspense>
1883
+ </BlockedOn>
1884
+ </Suspense>
1885
+ </BlockedOn>
1886
+ <BlockedOn text="qux">
1887
+ <div>qux</div>
1888
+ <link rel="stylesheet" href="qux" precedence="default" />
1889
+ </BlockedOn>
1890
+ </div>
1891
+ </Suspense>
1892
+ </div>
1893
+ </body>
1894
+ </html>,
1895
+ );
1896
+ pipe(writable);
1897
+ });
1898
+
1899
+ expect(getVisibleChildren(document)).toEqual(
1900
+ <html>
1901
+ <head />
1902
+ <body>
1903
+ <div>loading...</div>
1904
+ </body>
1905
+ </html>,
1906
+ );
1907
+
1908
+ // This will enqueue a style resource in a deep blocked boundary (loading baz...).
1909
+ await act(() => {
1910
+ resolveText('baz');
1911
+ });
1912
+ expect(getVisibleChildren(document)).toEqual(
1913
+ <html>
1914
+ <head />
1915
+ <body>
1916
+ <div>loading...</div>
1917
+ </body>
1918
+ </html>,
1919
+ );
1920
+
1921
+ // This will enqueue a style resource in the intermediate blocked boundary (loading bar...).
1922
+ await act(() => {
1923
+ resolveText('bar');
1924
+ });
1925
+ expect(getVisibleChildren(document)).toEqual(
1926
+ <html>
1927
+ <head />
1928
+ <body>
1929
+ <div>loading...</div>
1930
+ </body>
1931
+ </html>,
1932
+ );
1933
+
1934
+ // This will complete a segment in the top level boundary that is still blocked on another segment.
1935
+ // It will flush the completed segment however the inner boundaries should not emit their style dependencies
1936
+ // because they are not going to be revealed yet. instead their dependencies are hoisted to the blocked
1937
+ // boundary (top level).
1938
+ await act(() => {
1939
+ resolveText('foo');
1940
+ });
1941
+ expect(getVisibleChildren(document)).toEqual(
1942
+ <html>
1943
+ <head />
1944
+ <body>
1945
+ <div>loading...</div>
1946
+ <link rel="preload" href="foo" as="style" />
1947
+ <link rel="preload" href="bar" as="style" />
1948
+ <link rel="preload" href="baz" as="style" />
1949
+ </body>
1950
+ </html>,
1951
+ );
1952
+
1953
+ // This resolves the last blocked segment on the top level boundary so we see all dependencies of the
1954
+ // nested boundaries emitted at this level
1955
+ await act(() => {
1956
+ resolveText('qux');
1957
+ });
1958
+ expect(getVisibleChildren(document)).toEqual(
1959
+ <html>
1960
+ <head>
1961
+ <link rel="stylesheet" href="foo" data-rprec="default" />
1962
+ <link rel="stylesheet" href="bar" data-rprec="default" />
1963
+ <link rel="stylesheet" href="baz" data-rprec="default" />
1964
+ <link rel="stylesheet" href="qux" data-rprec="default" />
1965
+ </head>
1966
+ <body>
1967
+ <div>loading...</div>
1968
+ <link rel="preload" href="foo" as="style" />
1969
+ <link rel="preload" href="bar" as="style" />
1970
+ <link rel="preload" href="baz" as="style" />
1971
+ <link rel="preload" href="qux" as="style" />
1972
+ </body>
1973
+ </html>,
1974
+ );
1975
+
1976
+ // We load all stylesheets and confirm the content is revealed
1977
+ await act(() => {
1978
+ const event = document.createEvent('Events');
1979
+ event.initEvent('load', true, true);
1980
+ Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
1981
+ el => {
1982
+ el.dispatchEvent(event);
1983
+ },
1984
+ );
1985
+ });
1986
+ expect(getVisibleChildren(document)).toEqual(
1987
+ <html>
1988
+ <head>
1989
+ <link rel="stylesheet" href="foo" data-rprec="default" />
1990
+ <link rel="stylesheet" href="bar" data-rprec="default" />
1991
+ <link rel="stylesheet" href="baz" data-rprec="default" />
1992
+ <link rel="stylesheet" href="qux" data-rprec="default" />
1993
+ </head>
1994
+ <body>
1995
+ <div>
1996
+ <div>
1997
+ <div>foo</div>
1998
+ <div>bar</div>
1999
+ <div>
2000
+ <div>baz</div>
2001
+ </div>
2002
+ <div>qux</div>
2003
+ </div>
2004
+ </div>
2005
+ <link rel="preload" href="foo" as="style" />
2006
+ <link rel="preload" href="bar" as="style" />
2007
+ <link rel="preload" href="baz" as="style" />
2008
+ <link rel="preload" href="qux" as="style" />
2009
+ </body>
2010
+ </html>,
2011
+ );
2012
+ });
2013
+
2014
+ // @gate enableFloat
2015
+ it('encodes attributes consistently whether resources are flushed in shell or in late boundaries', async () => {
2016
+ const originalConsoleError = console.error;
2017
+ const mockError = jest.fn();
2018
+ console.error = (...args) => {
2019
+ mockError(...args.map(normalizeCodeLocInfo));
2020
+ };
2021
+ function BlockedOn({text, children}) {
2022
+ readText(text);
2023
+ return children;
2024
+ }
2025
+ function App() {
2026
+ return (
2027
+ <html>
2028
+ <head />
2029
+ <body>
2030
+ <div>
2031
+ <link
2032
+ // This preload is explicit so it can flush with a lot of potential attrs
2033
+ // We will duplicate this as a style that flushes after the shell
2034
+ rel="preload"
2035
+ as="style"
2036
+ href="foo"
2037
+ // precedence is not a special attribute for preloads so this will just flush as is
2038
+ precedence="default"
2039
+ // Some standard link props
2040
+ crossOrigin="anonymous"
2041
+ media="all"
2042
+ integrity="somehash"
2043
+ referrerPolicy="origin"
2044
+ // data and non starndard attributes that should flush
2045
+ data-foo={'"quoted"'}
2046
+ nonStandardAttr="attr"
2047
+ properlyformattednonstandardattr="attr"
2048
+ // attributes that should be filtered out for violating certain rules
2049
+ onSomething="this should be removed b/c event handler"
2050
+ shouldnotincludefunctions={() => {}}
2051
+ norsymbols={Symbol('foo')}
2052
+ />
2053
+ <Suspense fallback={'loading...'}>
2054
+ <BlockedOn text="unblock">
2055
+ <link
2056
+ // This preload is explicit so it can flush with a lot of potential attrs
2057
+ // We will duplicate this as a style that flushes after the shell
2058
+ rel="stylesheet"
2059
+ href="foo"
2060
+ // opt-in property to get this treated as a resource
2061
+ precedence="default"
2062
+ // Some standard link props
2063
+ crossOrigin="anonymous"
2064
+ media="all"
2065
+ integrity="somehash"
2066
+ referrerPolicy="origin"
2067
+ // data and non starndard attributes that should flush
2068
+ data-foo={'"quoted"'}
2069
+ nonStandardAttr="attr"
2070
+ properlyformattednonstandardattr="attr"
2071
+ // attributes that should be filtered out for violating certain rules
2072
+ onSomething="this should be removed b/c event handler"
2073
+ shouldnotincludefunctions={() => {}}
2074
+ norsymbols={Symbol('foo')}
2075
+ />
2076
+ </BlockedOn>
2077
+ </Suspense>
2078
+ </div>
2079
+ </body>
2080
+ </html>
2081
+ );
2082
+ }
2083
+ try {
2084
+ await actIntoEmptyDocument(() => {
2085
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
2086
+ pipe(writable);
2087
+ });
2088
+ expect(getVisibleChildren(document)).toEqual(
2089
+ <html>
2090
+ <head>
2091
+ <link
2092
+ rel="preload"
2093
+ as="style"
2094
+ href="foo"
2095
+ precedence="default"
2096
+ crossorigin="anonymous"
2097
+ media="all"
2098
+ integrity="somehash"
2099
+ referrerpolicy="origin"
2100
+ data-foo={'"quoted"'}
2101
+ nonstandardattr="attr"
2102
+ properlyformattednonstandardattr="attr"
2103
+ />
2104
+ </head>
2105
+ <body>
2106
+ <div>loading...</div>
2107
+ </body>
2108
+ </html>,
2109
+ );
2110
+ if (__DEV__) {
2111
+ expect(mockError).toHaveBeenCalledTimes(2);
2112
+ expect(mockError).toHaveBeenCalledWith(
2113
+ 'Warning: React does not recognize the `%s` prop on a DOM element.' +
2114
+ ' If you intentionally want it to appear in the DOM as a custom attribute,' +
2115
+ ' spell it as lowercase `%s` instead. If you accidentally passed it from a' +
2116
+ ' parent component, remove it from the DOM element.%s',
2117
+ 'nonStandardAttr',
2118
+ 'nonstandardattr',
2119
+ componentStack(['link', 'div', 'body', 'html', 'App']),
2120
+ );
2121
+ expect(mockError).toHaveBeenCalledWith(
2122
+ 'Warning: Invalid values for props %s on <%s> tag. Either remove them from' +
2123
+ ' the element, or pass a string or number value to keep them in the DOM. For' +
2124
+ ' details, see https://reactjs.org/link/attribute-behavior %s',
2125
+ '`shouldnotincludefunctions`, `norsymbols`',
2126
+ 'link',
2127
+ componentStack(['link', 'div', 'body', 'html', 'App']),
2128
+ );
2129
+ mockError.mockClear();
2130
+ } else {
2131
+ expect(mockError).not.toHaveBeenCalled();
2132
+ }
2133
+
2134
+ // Now we flush the stylesheet with the boundary
2135
+ await act(() => {
2136
+ resolveText('unblock');
2137
+ });
2138
+
2139
+ expect(getVisibleChildren(document)).toEqual(
2140
+ <html>
2141
+ <head>
2142
+ <link
2143
+ rel="stylesheet"
2144
+ href="foo"
2145
+ data-rprec="default"
2146
+ crossorigin="anonymous"
2147
+ media="all"
2148
+ integrity="somehash"
2149
+ referrerpolicy="origin"
2150
+ data-foo={'"quoted"'}
2151
+ nonstandardattr="attr"
2152
+ properlyformattednonstandardattr="attr"
2153
+ />
2154
+ <link
2155
+ rel="preload"
2156
+ as="style"
2157
+ href="foo"
2158
+ precedence="default"
2159
+ crossorigin="anonymous"
2160
+ media="all"
2161
+ integrity="somehash"
2162
+ referrerpolicy="origin"
2163
+ data-foo={'"quoted"'}
2164
+ nonstandardattr="attr"
2165
+ properlyformattednonstandardattr="attr"
2166
+ />
2167
+ </head>
2168
+ <body>
2169
+ <div>loading...</div>
2170
+ </body>
2171
+ </html>,
2172
+ );
2173
+ if (__DEV__) {
2174
+ // The way the test is currently set up the props that would warn have already warned
2175
+ // so no new warnings appear. This is really testing the same code pathway so
2176
+ // exercising that more here isn't all that useful
2177
+ expect(mockError).toHaveBeenCalledTimes(0);
2178
+ } else {
2179
+ expect(mockError).not.toHaveBeenCalled();
2180
+ }
2181
+ } finally {
2182
+ console.error = originalConsoleError;
2183
+ }
2184
+ });
2185
+
2186
+ // @gate enableFloat
2187
+ it('boundary style resource dependencies hoist to a parent boundary when flushed inline', async () => {
2188
+ function BlockedOn({text, children}) {
2189
+ readText(text);
2190
+ return children;
2191
+ }
2192
+ await actIntoEmptyDocument(() => {
2193
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
2194
+ <html>
2195
+ <head />
2196
+ <body>
2197
+ <div>
2198
+ <Suspense fallback="loading A...">
2199
+ <BlockedOn text="unblock">
2200
+ <AsyncText text="A" />
2201
+ <link rel="stylesheet" href="A" precedence="A" />
2202
+ <Suspense fallback="loading AA...">
2203
+ <AsyncText text="AA" />
2204
+ <link rel="stylesheet" href="AA" precedence="AA" />
2205
+ <Suspense fallback="loading AAA...">
2206
+ <AsyncText text="AAA" />
2207
+ <link rel="stylesheet" href="AAA" precedence="AAA" />
2208
+ <Suspense fallback="loading AAAA...">
2209
+ <AsyncText text="AAAA" />
2210
+ <link rel="stylesheet" href="AAAA" precedence="AAAA" />
2211
+ </Suspense>
2212
+ </Suspense>
2213
+ </Suspense>
2214
+ </BlockedOn>
2215
+ </Suspense>
2216
+ </div>
2217
+ </body>
2218
+ </html>,
2219
+ );
2220
+ pipe(writable);
2221
+ });
2222
+ expect(getVisibleChildren(document)).toEqual(
2223
+ <html>
2224
+ <head />
2225
+ <body>
2226
+ <div>loading A...</div>
2227
+ </body>
2228
+ </html>,
2229
+ );
2230
+
2231
+ await act(() => {
2232
+ resolveText('unblock');
2233
+ resolveText('AAAA');
2234
+ resolveText('AA');
2235
+ });
2236
+ expect(getVisibleChildren(document)).toEqual(
2237
+ <html>
2238
+ <head />
2239
+ <body>
2240
+ <div>loading A...</div>
2241
+ <link rel="preload" as="style" href="A" />
2242
+ <link rel="preload" as="style" href="AA" />
2243
+ <link rel="preload" as="style" href="AAA" />
2244
+ <link rel="preload" as="style" href="AAAA" />
2245
+ </body>
2246
+ </html>,
2247
+ );
2248
+
2249
+ await act(() => {
2250
+ resolveText('A');
2251
+ });
2252
+ await act(() => {
2253
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2254
+ const event = document.createEvent('Events');
2255
+ event.initEvent('load', true, true);
2256
+ l.dispatchEvent(event);
2257
+ });
2258
+ });
2259
+ expect(getVisibleChildren(document)).toEqual(
2260
+ <html>
2261
+ <head>
2262
+ <link rel="stylesheet" href="A" data-rprec="A" />
2263
+ <link rel="stylesheet" href="AA" data-rprec="AA" />
2264
+ </head>
2265
+ <body>
2266
+ <div>
2267
+ {'A'}
2268
+ {'AA'}
2269
+ {'loading AAA...'}
2270
+ </div>
2271
+ <link rel="preload" as="style" href="A" />
2272
+ <link rel="preload" as="style" href="AA" />
2273
+ <link rel="preload" as="style" href="AAA" />
2274
+ <link rel="preload" as="style" href="AAAA" />
2275
+ </body>
2276
+ </html>,
2277
+ );
2278
+
2279
+ await act(() => {
2280
+ resolveText('AAA');
2281
+ });
2282
+ await act(() => {
2283
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2284
+ const event = document.createEvent('Events');
2285
+ event.initEvent('load', true, true);
2286
+ l.dispatchEvent(event);
2287
+ });
2288
+ });
2289
+ expect(getVisibleChildren(document)).toEqual(
2290
+ <html>
2291
+ <head>
2292
+ <link rel="stylesheet" href="A" data-rprec="A" />
2293
+ <link rel="stylesheet" href="AA" data-rprec="AA" />
2294
+ <link rel="stylesheet" href="AAA" data-rprec="AAA" />
2295
+ <link rel="stylesheet" href="AAAA" data-rprec="AAAA" />
2296
+ </head>
2297
+ <body>
2298
+ <div>
2299
+ {'A'}
2300
+ {'AA'}
2301
+ {'AAA'}
2302
+ {'AAAA'}
2303
+ </div>
2304
+ <link rel="preload" as="style" href="A" />
2305
+ <link rel="preload" as="style" href="AA" />
2306
+ <link rel="preload" as="style" href="AAA" />
2307
+ <link rel="preload" as="style" href="AAAA" />
2308
+ </body>
2309
+ </html>,
2310
+ );
2311
+ });
2312
+
2313
+ // @gate enableFloat
2314
+ it('always enforces crossOrigin "anonymous" for font preloads', async () => {
2315
+ const originalConsoleError = console.error;
2316
+ const mockError = jest.fn();
2317
+ console.error = (...args) => {
2318
+ mockError(...args.map(normalizeCodeLocInfo));
2319
+ };
2320
+ try {
2321
+ await actIntoEmptyDocument(() => {
2322
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
2323
+ <html>
2324
+ <head />
2325
+ <body>
2326
+ <link rel="preload" as="font" href="foo" />
2327
+ <link rel="preload" as="font" href="bar" crossOrigin="foo" />
2328
+ <link
2329
+ rel="preload"
2330
+ as="font"
2331
+ href="baz"
2332
+ crossOrigin="use-credentials"
2333
+ />
2334
+ <link
2335
+ rel="preload"
2336
+ as="font"
2337
+ href="qux"
2338
+ crossOrigin="anonymous"
2339
+ />
2340
+ </body>
2341
+ </html>,
2342
+ );
2343
+ pipe(writable);
2344
+ });
2345
+ expect(getVisibleChildren(document)).toEqual(
2346
+ <html>
2347
+ <head>
2348
+ <link rel="preload" as="font" href="foo" crossorigin="" />
2349
+ <link rel="preload" as="font" href="bar" crossorigin="" />
2350
+ <link rel="preload" as="font" href="baz" crossorigin="" />
2351
+ <link rel="preload" as="font" href="qux" crossorigin="" />
2352
+ </head>
2353
+ <body />
2354
+ </html>,
2355
+ );
2356
+
2357
+ if (__DEV__) {
2358
+ expect(mockError).toHaveBeenCalledTimes(2);
2359
+ expect(mockError).toHaveBeenCalledWith(
2360
+ 'Warning: A %s with href "%s" did not specify the crossOrigin prop. Font preloads must always use' +
2361
+ ' anonymouse CORS mode. To fix add an empty string, "anonymous", or any other string' +
2362
+ ' value except "use-credentials" for the crossOrigin prop of all font preloads.%s',
2363
+ 'preload Resource (as "font")',
2364
+ 'foo',
2365
+ componentStack(['link', 'body', 'html']),
2366
+ );
2367
+ expect(mockError).toHaveBeenCalledWith(
2368
+ 'Warning: A %s with href "%s" specified a crossOrigin value of "use-credentials". Font preloads must always use' +
2369
+ ' anonymouse CORS mode. To fix use an empty string, "anonymous", or any other string' +
2370
+ ' value except "use-credentials" for the crossOrigin prop of all font preloads.%s',
2371
+ 'preload Resource (as "font")',
2372
+ 'baz',
2373
+ componentStack(['link', 'body', 'html']),
2374
+ );
2375
+ } else {
2376
+ expect(mockError).not.toHaveBeenCalled();
2377
+ }
2378
+ } finally {
2379
+ console.error = originalConsoleError;
2380
+ }
2381
+ });
2382
+
2383
+ describe('ReactDOM.pre* function validation', () => {
2384
+ function Preloads({scenarios}) {
2385
+ for (let i = 0; i < scenarios.length; i++) {
2386
+ const href = scenarios[i][0];
2387
+ const options = scenarios[i][1];
2388
+ ReactDOM.preload(href, options);
2389
+ }
2390
+ }
2391
+ function Preinits({scenarios}) {
2392
+ for (let i = 0; i < scenarios.length; i++) {
2393
+ const href = scenarios[i][0];
2394
+ const options = scenarios[i][1];
2395
+ ReactDOM.preinit(href, options);
2396
+ }
2397
+ }
2398
+ async function renderOnServer(Component, scenarios) {
2399
+ const originalConsoleError = console.error;
2400
+ const mockError = jest.fn();
2401
+ console.error = (...args) => {
2402
+ mockError(...args.map(normalizeCodeLocInfo));
2403
+ };
2404
+ try {
2405
+ await actIntoEmptyDocument(() => {
2406
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
2407
+ <html>
2408
+ <head>
2409
+ <Component scenarios={scenarios} />
2410
+ </head>
2411
+ </html>,
2412
+ );
2413
+ pipe(writable);
2414
+ });
2415
+ for (let i = 0; i < scenarios.length; i++) {
2416
+ const assertion = scenarios[i][2];
2417
+ assertion(mockError, i);
2418
+ }
2419
+ } finally {
2420
+ console.error = originalConsoleError;
2421
+ }
2422
+ }
2423
+ async function renderOnClient(Component, scenarios) {
2424
+ const originalConsoleError = console.error;
2425
+ const mockError = jest.fn();
2426
+ console.error = (...args) => {
2427
+ mockError(...args.map(normalizeCodeLocInfo));
2428
+ };
2429
+ try {
2430
+ const root = ReactDOMClient.createRoot(document);
2431
+ root.render(
2432
+ <html>
2433
+ <head>
2434
+ <Component scenarios={scenarios} />
2435
+ </head>
2436
+ </html>,
2437
+ );
2438
+ expect(Scheduler).toFlushWithoutYielding();
2439
+ for (let i = 0; i < scenarios.length; i++) {
2440
+ const assertion = scenarios[i][2];
2441
+ assertion(mockError, i);
2442
+ }
2443
+ } finally {
2444
+ console.error = originalConsoleError;
2445
+ }
2446
+ }
2447
+
2448
+ [
2449
+ ['server', renderOnServer],
2450
+ ['client', renderOnClient],
2451
+ ].forEach(([environment, render]) => {
2452
+ // @gate enableFloat
2453
+ it(
2454
+ 'warns when an invalid href argument is provided to ReactDOM.preload on the ' +
2455
+ environment,
2456
+ async () => {
2457
+ const expectedMessage =
2458
+ 'Warning: ReactDOM.preload() expected the first argument to be a string representing an href but found %s instead.%s';
2459
+ const expectedStack = componentStack(['Preloads', 'head', 'html']);
2460
+ function makeArgs(...substitutions) {
2461
+ return [expectedMessage, ...substitutions, expectedStack];
2462
+ }
2463
+ await render(Preloads, [
2464
+ [
2465
+ '',
2466
+ undefined,
2467
+ (mockError, scenarioNumber) => {
2468
+ if (__DEV__) {
2469
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2470
+ makeArgs('an empty string'),
2471
+ );
2472
+ } else {
2473
+ expect(mockError).not.toHaveBeenCalled();
2474
+ }
2475
+ },
2476
+ ],
2477
+ [
2478
+ undefined,
2479
+ undefined,
2480
+ (mockError, scenarioNumber) => {
2481
+ if (__DEV__) {
2482
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2483
+ makeArgs('undefined'),
2484
+ );
2485
+ } else {
2486
+ expect(mockError).not.toHaveBeenCalled();
2487
+ }
2488
+ },
2489
+ ],
2490
+ [
2491
+ null,
2492
+ undefined,
2493
+ (mockError, scenarioNumber) => {
2494
+ if (__DEV__) {
2495
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2496
+ makeArgs('null'),
2497
+ );
2498
+ } else {
2499
+ expect(mockError).not.toHaveBeenCalled();
2500
+ }
2501
+ },
2502
+ ],
2503
+ [
2504
+ 232132,
2505
+ undefined,
2506
+ (mockError, scenarioNumber) => {
2507
+ if (__DEV__) {
2508
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2509
+ makeArgs('something with type "number"'),
2510
+ );
2511
+ } else {
2512
+ expect(mockError).not.toHaveBeenCalled();
2513
+ }
2514
+ },
2515
+ ],
2516
+ [
2517
+ {},
2518
+ undefined,
2519
+ (mockError, scenarioNumber) => {
2520
+ if (__DEV__) {
2521
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2522
+ makeArgs('something with type "object"'),
2523
+ );
2524
+ } else {
2525
+ expect(mockError).not.toHaveBeenCalled();
2526
+ }
2527
+ },
2528
+ ],
2529
+ ]);
2530
+ },
2531
+ );
2532
+
2533
+ // @gate enableFloat
2534
+ it(
2535
+ 'warns when an invalid href argument is provided to ReactDOM.preinit on the ' +
2536
+ environment,
2537
+ async () => {
2538
+ const expectedMessage =
2539
+ 'Warning: ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.%s';
2540
+ const expectedStack = componentStack(['Preinits', 'head', 'html']);
2541
+ function makeArgs(...substitutions) {
2542
+ return [expectedMessage, ...substitutions, expectedStack];
2543
+ }
2544
+ await render(Preinits, [
2545
+ [
2546
+ '',
2547
+ undefined,
2548
+ (mockError, scenarioNumber) => {
2549
+ if (__DEV__) {
2550
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2551
+ makeArgs('an empty string'),
2552
+ );
2553
+ } else {
2554
+ expect(mockError).not.toHaveBeenCalled();
2555
+ }
2556
+ },
2557
+ ],
2558
+ [
2559
+ undefined,
2560
+ undefined,
2561
+ (mockError, scenarioNumber) => {
2562
+ if (__DEV__) {
2563
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2564
+ makeArgs('undefined'),
2565
+ );
2566
+ } else {
2567
+ expect(mockError).not.toHaveBeenCalled();
2568
+ }
2569
+ },
2570
+ ],
2571
+ [
2572
+ null,
2573
+ undefined,
2574
+ (mockError, scenarioNumber) => {
2575
+ if (__DEV__) {
2576
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2577
+ makeArgs('null'),
2578
+ );
2579
+ } else {
2580
+ expect(mockError).not.toHaveBeenCalled();
2581
+ }
2582
+ },
2583
+ ],
2584
+ [
2585
+ 232132,
2586
+ undefined,
2587
+ (mockError, scenarioNumber) => {
2588
+ if (__DEV__) {
2589
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2590
+ makeArgs('something with type "number"'),
2591
+ );
2592
+ } else {
2593
+ expect(mockError).not.toHaveBeenCalled();
2594
+ }
2595
+ },
2596
+ ],
2597
+ [
2598
+ {},
2599
+ undefined,
2600
+ (mockError, scenarioNumber) => {
2601
+ if (__DEV__) {
2602
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2603
+ makeArgs('something with type "object"'),
2604
+ );
2605
+ } else {
2606
+ expect(mockError).not.toHaveBeenCalled();
2607
+ }
2608
+ },
2609
+ ],
2610
+ ]);
2611
+ },
2612
+ );
2613
+
2614
+ // @gate enableFloat
2615
+ it(
2616
+ 'warns when an invalid options argument is provided to ReactDOM.preload on the ' +
2617
+ environment,
2618
+ async () => {
2619
+ const expectedMessage =
2620
+ 'Warning: ReactDOM.preload() expected the second argument to be an options argument containing at least an "as" property' +
2621
+ ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".%s';
2622
+ const expectedStack = componentStack(['Preloads', 'head', 'html']);
2623
+ function makeArgs(...substitutions) {
2624
+ return [expectedMessage, ...substitutions, expectedStack];
2625
+ }
2626
+ await render(Preloads, [
2627
+ [
2628
+ 'foo',
2629
+ undefined,
2630
+ (mockError, scenarioNumber) => {
2631
+ if (__DEV__) {
2632
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2633
+ makeArgs('undefined', 'foo'),
2634
+ );
2635
+ } else {
2636
+ expect(mockError).not.toHaveBeenCalled();
2637
+ }
2638
+ },
2639
+ ],
2640
+ [
2641
+ 'foo',
2642
+ null,
2643
+ (mockError, scenarioNumber) => {
2644
+ if (__DEV__) {
2645
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2646
+ makeArgs('null', 'foo'),
2647
+ );
2648
+ } else {
2649
+ expect(mockError).not.toHaveBeenCalled();
2650
+ }
2651
+ },
2652
+ ],
2653
+ [
2654
+ 'foo',
2655
+ 'bar',
2656
+ (mockError, scenarioNumber) => {
2657
+ if (__DEV__) {
2658
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2659
+ makeArgs('something with type "string"', 'foo'),
2660
+ );
2661
+ } else {
2662
+ expect(mockError).not.toHaveBeenCalled();
2663
+ }
2664
+ },
2665
+ ],
2666
+ [
2667
+ 'foo',
2668
+ 123,
2669
+ (mockError, scenarioNumber) => {
2670
+ if (__DEV__) {
2671
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2672
+ makeArgs('something with type "number"', 'foo'),
2673
+ );
2674
+ } else {
2675
+ expect(mockError).not.toHaveBeenCalled();
2676
+ }
2677
+ },
2678
+ ],
2679
+ ]);
2680
+ },
2681
+ );
2682
+
2683
+ // @gate enableFloat
2684
+ it(
2685
+ 'warns when an invalid options argument is provided to ReactDOM.preinit on the ' +
2686
+ environment,
2687
+ async () => {
2688
+ const expectedMessage =
2689
+ 'Warning: ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
2690
+ ' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".%s';
2691
+ const expectedStack = componentStack(['Preinits', 'head', 'html']);
2692
+ function makeArgs(...substitutions) {
2693
+ return [expectedMessage, ...substitutions, expectedStack];
2694
+ }
2695
+ await render(Preinits, [
2696
+ [
2697
+ 'foo',
2698
+ undefined,
2699
+ (mockError, scenarioNumber) => {
2700
+ if (__DEV__) {
2701
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2702
+ makeArgs('undefined', 'foo'),
2703
+ );
2704
+ } else {
2705
+ expect(mockError).not.toHaveBeenCalled();
2706
+ }
2707
+ },
2708
+ ],
2709
+ [
2710
+ 'foo',
2711
+ null,
2712
+ (mockError, scenarioNumber) => {
2713
+ if (__DEV__) {
2714
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2715
+ makeArgs('null', 'foo'),
2716
+ );
2717
+ } else {
2718
+ expect(mockError).not.toHaveBeenCalled();
2719
+ }
2720
+ },
2721
+ ],
2722
+ [
2723
+ 'foo',
2724
+ 'bar',
2725
+ (mockError, scenarioNumber) => {
2726
+ if (__DEV__) {
2727
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2728
+ makeArgs('something with type "string"', 'foo'),
2729
+ );
2730
+ } else {
2731
+ expect(mockError).not.toHaveBeenCalled();
2732
+ }
2733
+ },
2734
+ ],
2735
+ [
2736
+ 'foo',
2737
+ 123,
2738
+ (mockError, scenarioNumber) => {
2739
+ if (__DEV__) {
2740
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2741
+ makeArgs('something with type "number"', 'foo'),
2742
+ );
2743
+ } else {
2744
+ expect(mockError).not.toHaveBeenCalled();
2745
+ }
2746
+ },
2747
+ ],
2748
+ ]);
2749
+ },
2750
+ );
2751
+
2752
+ // @gate enableFloat
2753
+ it(
2754
+ 'warns when an invalid "as" option is provided to ReactDOM.preload on the ' +
2755
+ environment,
2756
+ async () => {
2757
+ const expectedMessage =
2758
+ 'Warning: ReactDOM.preload() expected a valid "as" type in the options (second) argument but found %s instead.' +
2759
+ ' Please use one of the following valid values instead: %s. The href for the preload call where this' +
2760
+ ' warning originated is "%s".%s';
2761
+ const expectedStack = componentStack(['Preloads', 'head', 'html']);
2762
+ function makeArgs(...substitutions) {
2763
+ return [expectedMessage, ...substitutions, expectedStack];
2764
+ }
2765
+ await render(Preloads, [
2766
+ [
2767
+ 'foo',
2768
+ {},
2769
+ (mockError, scenarioNumber) => {
2770
+ if (__DEV__) {
2771
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2772
+ makeArgs('undefined', '"style" and "font"', 'foo'),
2773
+ );
2774
+ } else {
2775
+ expect(mockError).not.toHaveBeenCalled();
2776
+ }
2777
+ },
2778
+ ],
2779
+ [
2780
+ 'bar',
2781
+ {as: null},
2782
+ (mockError, scenarioNumber) => {
2783
+ if (__DEV__) {
2784
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2785
+ makeArgs('null', '"style" and "font"', 'bar'),
2786
+ );
2787
+ } else {
2788
+ expect(mockError).not.toHaveBeenCalled();
2789
+ }
2790
+ },
2791
+ ],
2792
+ [
2793
+ 'baz',
2794
+ {as: 123},
2795
+ (mockError, scenarioNumber) => {
2796
+ if (__DEV__) {
2797
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2798
+ makeArgs(
2799
+ 'something with type "number"',
2800
+ '"style" and "font"',
2801
+ 'baz',
2802
+ ),
2803
+ );
2804
+ } else {
2805
+ expect(mockError).not.toHaveBeenCalled();
2806
+ }
2807
+ },
2808
+ ],
2809
+ [
2810
+ 'qux',
2811
+ {as: {}},
2812
+ (mockError, scenarioNumber) => {
2813
+ if (__DEV__) {
2814
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2815
+ makeArgs(
2816
+ 'something with type "object"',
2817
+ '"style" and "font"',
2818
+ 'qux',
2819
+ ),
2820
+ );
2821
+ } else {
2822
+ expect(mockError).not.toHaveBeenCalled();
2823
+ }
2824
+ },
2825
+ ],
2826
+ [
2827
+ 'quux',
2828
+ {as: 'bar'},
2829
+ (mockError, scenarioNumber) => {
2830
+ if (__DEV__) {
2831
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2832
+ makeArgs('"bar"', '"style" and "font"', 'quux'),
2833
+ );
2834
+ } else {
2835
+ expect(mockError).not.toHaveBeenCalled();
2836
+ }
2837
+ },
2838
+ ],
2839
+ ]);
2840
+ },
2841
+ );
2842
+
2843
+ // @gate enableFloat
2844
+ it(
2845
+ 'warns when an invalid "as" option is provided to ReactDOM.preinit on the ' +
2846
+ environment,
2847
+ async () => {
2848
+ const expectedMessage =
2849
+ 'Warning: ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
2850
+ ' specifying the Resource type. It found %s instead. Currently, the only valid resource type for preinit is "style".' +
2851
+ ' The href for the preinit call where this warning originated is "%s".%s';
2852
+ const expectedStack = componentStack(['Preinits', 'head', 'html']);
2853
+ function makeArgs(...substitutions) {
2854
+ return [expectedMessage, ...substitutions, expectedStack];
2855
+ }
2856
+ await render(Preinits, [
2857
+ [
2858
+ 'foo',
2859
+ {},
2860
+ (mockError, scenarioNumber) => {
2861
+ if (__DEV__) {
2862
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2863
+ makeArgs('undefined', 'foo'),
2864
+ );
2865
+ } else {
2866
+ expect(mockError).not.toHaveBeenCalled();
2867
+ }
2868
+ },
2869
+ ],
2870
+ [
2871
+ 'bar',
2872
+ {as: null},
2873
+ (mockError, scenarioNumber) => {
2874
+ if (__DEV__) {
2875
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2876
+ makeArgs('null', 'bar'),
2877
+ );
2878
+ } else {
2879
+ expect(mockError).not.toHaveBeenCalled();
2880
+ }
2881
+ },
2882
+ ],
2883
+ [
2884
+ 'baz',
2885
+ {as: 123},
2886
+ (mockError, scenarioNumber) => {
2887
+ if (__DEV__) {
2888
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2889
+ makeArgs('something with type "number"', 'baz'),
2890
+ );
2891
+ } else {
2892
+ expect(mockError).not.toHaveBeenCalled();
2893
+ }
2894
+ },
2895
+ ],
2896
+ [
2897
+ 'qux',
2898
+ {as: {}},
2899
+ (mockError, scenarioNumber) => {
2900
+ if (__DEV__) {
2901
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2902
+ makeArgs('something with type "object"', 'qux'),
2903
+ );
2904
+ } else {
2905
+ expect(mockError).not.toHaveBeenCalled();
2906
+ }
2907
+ },
2908
+ ],
2909
+ [
2910
+ 'quux',
2911
+ {as: 'bar'},
2912
+ (mockError, scenarioNumber) => {
2913
+ if (__DEV__) {
2914
+ expect(mockError.mock.calls[scenarioNumber]).toEqual(
2915
+ makeArgs('"bar"', 'quux'),
2916
+ );
2917
+ } else {
2918
+ expect(mockError).not.toHaveBeenCalled();
2919
+ }
2920
+ },
2921
+ ],
2922
+ ]);
2923
+ },
2924
+ );
2925
+ });
2926
+ });
2927
+
2928
+ describe('prop validation', () => {
2929
+ // @gate enableFloat
2930
+ it('warns when you change props on a resource unless you also change the href', async () => {
2931
+ const root = ReactDOMClient.createRoot(container);
2932
+ root.render(
2933
+ <div>
2934
+ <link
2935
+ rel="stylesheet"
2936
+ href="foo"
2937
+ precedence="foo"
2938
+ data-something-extra="extra"
2939
+ />
2940
+ <link
2941
+ rel="stylesheet"
2942
+ href="bar"
2943
+ precedence="bar"
2944
+ data-something-extra="extra"
2945
+ />
2946
+ hello
2947
+ </div>,
2948
+ );
2949
+ expect(Scheduler).toFlushWithoutYielding();
2950
+
2951
+ root.render(
2952
+ <div>
2953
+ <link
2954
+ rel="stylesheet"
2955
+ href="foo"
2956
+ precedence="fu"
2957
+ data-something-new="new"
2958
+ />
2959
+ <link
2960
+ rel="stylesheet"
2961
+ href="baz"
2962
+ precedence="baz"
2963
+ data-something-new="new"
2964
+ />
2965
+ hello
2966
+ </div>,
2967
+ );
2968
+ expect(() => {
2969
+ expect(Scheduler).toFlushWithoutYielding();
2970
+ }).toErrorDev(
2971
+ 'Warning: A style Resource with href "foo" recieved new props with different values from the props used' +
2972
+ ' when this Resource was first rendered. React will only use the props provided when' +
2973
+ ' this resource was first rendered until a new href is provided. Unlike conventional' +
2974
+ ' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
2975
+ ' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
2976
+ ' (href) must have props that agree with each other. The differences are described below.' +
2977
+ '\n data-something-extra: missing or null in latest props, "extra" in original props' +
2978
+ '\n data-something-new: "new" in latest props, missing or null in original props' +
2979
+ '\n precedence: "fu" in latest props, "foo" in original props',
2980
+ );
2981
+ expect(getVisibleChildren(document)).toEqual(
2982
+ <html>
2983
+ <head>
2984
+ <link
2985
+ rel="stylesheet"
2986
+ href="foo"
2987
+ data-rprec="foo"
2988
+ data-something-extra="extra"
2989
+ />
2990
+ <link
2991
+ rel="stylesheet"
2992
+ href="bar"
2993
+ data-rprec="bar"
2994
+ data-something-extra="extra"
2995
+ />
2996
+ <link
2997
+ rel="stylesheet"
2998
+ href="baz"
2999
+ data-rprec="baz"
3000
+ data-something-new="new"
3001
+ />
3002
+ <link rel="preload" as="style" href="foo" />
3003
+ <link rel="preload" as="style" href="bar" />
3004
+ <link rel="preload" as="style" href="baz" />
3005
+ </head>
3006
+ <body>
3007
+ <div id="container">
3008
+ <div>hello</div>
3009
+ </div>
3010
+ </body>
3011
+ </html>,
3012
+ );
3013
+ });
3014
+
3015
+ // @gate enableFloat
3016
+ it('warns when style Resource have different values for media for the same href', async () => {
3017
+ const originalConsoleError = console.error;
3018
+ const mockError = jest.fn();
3019
+ console.error = (...args) => {
3020
+ mockError(...args.map(normalizeCodeLocInfo));
3021
+ };
3022
+ try {
3023
+ await actIntoEmptyDocument(() => {
3024
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3025
+ <html>
3026
+ <head>
3027
+ <link
3028
+ rel="stylesheet"
3029
+ href="foo"
3030
+ precedence="foo"
3031
+ media="all"
3032
+ />
3033
+ <link rel="stylesheet" href="foo" precedence="foo" />
3034
+
3035
+ <link rel="stylesheet" href="bar" precedence="bar" />
3036
+ <link
3037
+ rel="stylesheet"
3038
+ href="bar"
3039
+ precedence="bar"
3040
+ media="all"
3041
+ />
3042
+
3043
+ <link
3044
+ rel="stylesheet"
3045
+ href="baz"
3046
+ precedence="baz"
3047
+ media="some"
3048
+ />
3049
+ <link
3050
+ rel="stylesheet"
3051
+ href="baz"
3052
+ precedence="baz"
3053
+ media="all"
3054
+ />
3055
+ </head>
3056
+ </html>,
3057
+ );
3058
+ pipe(writable);
3059
+ });
3060
+ expect(getVisibleChildren(document)).toEqual(
3061
+ <html>
3062
+ <head>
3063
+ <link rel="stylesheet" href="foo" data-rprec="foo" media="all" />
3064
+ <link rel="stylesheet" href="bar" data-rprec="bar" />
3065
+
3066
+ <link rel="stylesheet" href="baz" data-rprec="baz" media="some" />
3067
+ </head>
3068
+ <body />
3069
+ </html>,
3070
+ );
3071
+
3072
+ if (__DEV__) {
3073
+ expect(mockError).toHaveBeenCalledTimes(3);
3074
+ expect(mockError).toHaveBeenCalledWith(
3075
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3076
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3077
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3078
+ 'style Resource',
3079
+ 'foo',
3080
+ 'an earlier instance of this Resource',
3081
+ '\n media: missing or null in latest props, "all" in original props',
3082
+ componentStack(['link', 'head', 'html']),
3083
+ );
3084
+ expect(mockError).toHaveBeenCalledWith(
3085
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3086
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3087
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3088
+ 'style Resource',
3089
+ 'bar',
3090
+ 'an earlier instance of this Resource',
3091
+ '\n media: "all" in latest props, missing or null in original props',
3092
+ componentStack(['link', 'head', 'html']),
3093
+ );
3094
+ expect(mockError).toHaveBeenCalledWith(
3095
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3096
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3097
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3098
+ 'style Resource',
3099
+ 'baz',
3100
+ 'an earlier instance of this Resource',
3101
+ '\n media: "all" in latest props, "some" in original props',
3102
+ componentStack(['link', 'head', 'html']),
3103
+ );
3104
+ } else {
3105
+ expect(mockError).not.toHaveBeenCalled();
3106
+ }
3107
+ } finally {
3108
+ console.error = originalConsoleError;
3109
+ }
3110
+ });
3111
+
3112
+ // @gate enableFloat
3113
+ it('warns when style Resource props differ or are added for the same href', async () => {
3114
+ const originalConsoleError = console.error;
3115
+ const mockError = jest.fn();
3116
+ console.error = (...args) => {
3117
+ mockError(...args.map(normalizeCodeLocInfo));
3118
+ };
3119
+ try {
3120
+ await actIntoEmptyDocument(() => {
3121
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3122
+ <html>
3123
+ <head>
3124
+ <link
3125
+ rel="stylesheet"
3126
+ href="foo"
3127
+ precedence="foo"
3128
+ data-foo="an original value"
3129
+ />
3130
+ <link rel="stylesheet" href="foo" precedence="foo" />
3131
+ <link rel="stylesheet" href="foo" precedence="foonew" />
3132
+
3133
+ <link rel="stylesheet" href="bar" precedence="bar" />
3134
+ <link
3135
+ rel="stylesheet"
3136
+ href="bar"
3137
+ precedence="bar"
3138
+ data-foo="a new value"
3139
+ />
3140
+
3141
+ <link
3142
+ rel="stylesheet"
3143
+ href="baz"
3144
+ precedence="baz"
3145
+ data-foo="an original value"
3146
+ />
3147
+ <link
3148
+ rel="stylesheet"
3149
+ href="baz"
3150
+ precedence="baz"
3151
+ data-foo="a new value"
3152
+ />
3153
+ </head>
3154
+ </html>,
3155
+ );
3156
+ pipe(writable);
3157
+ });
3158
+ expect(getVisibleChildren(document)).toEqual(
3159
+ <html>
3160
+ <head>
3161
+ <link
3162
+ rel="stylesheet"
3163
+ href="foo"
3164
+ data-rprec="foo"
3165
+ data-foo="an original value"
3166
+ />
3167
+ <link rel="stylesheet" href="bar" data-rprec="bar" />
3168
+ <link
3169
+ rel="stylesheet"
3170
+ href="baz"
3171
+ data-rprec="baz"
3172
+ data-foo="an original value"
3173
+ />
3174
+ </head>
3175
+ <body />
3176
+ </html>,
3177
+ );
3178
+
3179
+ if (__DEV__) {
3180
+ expect(mockError).toHaveBeenCalledTimes(3);
3181
+ expect(mockError).toHaveBeenCalledWith(
3182
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3183
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3184
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3185
+ 'style Resource',
3186
+ 'foo',
3187
+ 'an earlier instance of this Resource',
3188
+ '\n precedence: "foonew" in latest props, "foo" in original props',
3189
+ componentStack(['link', 'head', 'html']),
3190
+ );
3191
+ expect(mockError).toHaveBeenCalledWith(
3192
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3193
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3194
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3195
+ 'style Resource',
3196
+ 'bar',
3197
+ 'an earlier instance of this Resource',
3198
+ '\n data-foo: "a new value" in latest props, missing or null in original props',
3199
+ componentStack(['link', 'head', 'html']),
3200
+ );
3201
+ expect(mockError).toHaveBeenCalledWith(
3202
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3203
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3204
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3205
+ 'style Resource',
3206
+ 'baz',
3207
+ 'an earlier instance of this Resource',
3208
+ '\n data-foo: "a new value" in latest props, "an original value" in original props',
3209
+ componentStack(['link', 'head', 'html']),
3210
+ );
3211
+ } else {
3212
+ expect(mockError).not.toHaveBeenCalled();
3213
+ }
3214
+ } finally {
3215
+ console.error = originalConsoleError;
3216
+ }
3217
+ });
3218
+
3219
+ // @gate enableFloat
3220
+ it('warns when style Resource includes any combination of onLoad, onError, or disabled props', async () => {
3221
+ const originalConsoleError = console.error;
3222
+ const mockError = jest.fn();
3223
+ console.error = (...args) => {
3224
+ mockError(...args.map(normalizeCodeLocInfo));
3225
+ };
3226
+ try {
3227
+ await actIntoEmptyDocument(() => {
3228
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3229
+ <html>
3230
+ <head>
3231
+ <link
3232
+ rel="stylesheet"
3233
+ href="foo"
3234
+ precedence="foo"
3235
+ onLoad={() => {}}
3236
+ onError={() => {}}
3237
+ />
3238
+ <link
3239
+ rel="stylesheet"
3240
+ href="bar"
3241
+ precedence="bar"
3242
+ onLoad={() => {}}
3243
+ />
3244
+ <link
3245
+ rel="stylesheet"
3246
+ href="baz"
3247
+ precedence="baz"
3248
+ onError={() => {}}
3249
+ />
3250
+ <link
3251
+ rel="stylesheet"
3252
+ href="qux"
3253
+ precedence="qux"
3254
+ disabled={true}
3255
+ />
3256
+ </head>
3257
+ <body />
3258
+ </html>,
3259
+ );
3260
+ pipe(writable);
3261
+ });
3262
+ // precedence is removed from the stylesheets because it is considered a reserved prop for
3263
+ // stylesheets to opt into resource semantics.
3264
+ expect(getVisibleChildren(document)).toEqual(
3265
+ <html>
3266
+ <head>
3267
+ <link rel="preload" as="style" href="foo" />
3268
+ <link rel="preload" as="style" href="bar" />
3269
+ <link rel="preload" as="style" href="baz" />
3270
+ <link rel="preload" as="style" href="qux" />
3271
+ <link rel="stylesheet" href="foo" />
3272
+ <link rel="stylesheet" href="bar" />
3273
+ <link rel="stylesheet" href="baz" />
3274
+ <link rel="stylesheet" href="qux" disabled="" />
3275
+ </head>
3276
+ <body />
3277
+ </html>,
3278
+ );
3279
+
3280
+ if (__DEV__) {
3281
+ expect(mockError).toHaveBeenCalledTimes(4);
3282
+ expect(mockError).toHaveBeenCalledWith(
3283
+ 'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
3284
+ ' When using %s React will opt out of Resource behavior. If you meant for this' +
3285
+ ' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
3286
+ 'foo',
3287
+ 'onLoad and onError props',
3288
+ 'onLoad, onError, or disabled',
3289
+ 'onLoad and onError props',
3290
+ componentStack(['link', 'head', 'html']),
3291
+ );
3292
+ expect(mockError).toHaveBeenCalledWith(
3293
+ 'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
3294
+ ' When using %s React will opt out of Resource behavior. If you meant for this' +
3295
+ ' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
3296
+ 'bar',
3297
+ 'onLoad prop',
3298
+ 'onLoad, onError, or disabled',
3299
+ 'onLoad prop',
3300
+ componentStack(['link', 'head', 'html']),
3301
+ );
3302
+ expect(mockError).toHaveBeenCalledWith(
3303
+ 'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
3304
+ ' When using %s React will opt out of Resource behavior. If you meant for this' +
3305
+ ' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
3306
+ 'baz',
3307
+ 'onError prop',
3308
+ 'onLoad, onError, or disabled',
3309
+ 'onError prop',
3310
+ componentStack(['link', 'head', 'html']),
3311
+ );
3312
+ expect(mockError).toHaveBeenCalledWith(
3313
+ 'Warning: A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
3314
+ ' When using %s React will opt out of Resource behavior. If you meant for this' +
3315
+ ' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.%s',
3316
+ 'qux',
3317
+ 'disabled prop',
3318
+ 'onLoad, onError, or disabled',
3319
+ 'disabled prop',
3320
+ componentStack(['link', 'head', 'html']),
3321
+ );
3322
+ } else {
3323
+ expect(mockError).not.toHaveBeenCalled();
3324
+ }
3325
+ } finally {
3326
+ console.error = originalConsoleError;
3327
+ }
3328
+ });
3329
+
3330
+ // @gate enableFloat
3331
+ it('warns when preload Resources have new or different values for props', async () => {
3332
+ const originalConsoleError = console.error;
3333
+ const mockError = jest.fn();
3334
+ console.error = (...args) => {
3335
+ mockError(...args.map(normalizeCodeLocInfo));
3336
+ };
3337
+ try {
3338
+ await actIntoEmptyDocument(() => {
3339
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3340
+ <html>
3341
+ <head>
3342
+ <link
3343
+ rel="preload"
3344
+ as="style"
3345
+ href="foo"
3346
+ data-foo="a current value"
3347
+ />
3348
+ <link
3349
+ rel="preload"
3350
+ as="style"
3351
+ href="foo"
3352
+ data-foo="a new value"
3353
+ />
3354
+
3355
+ <link
3356
+ rel="preload"
3357
+ as="style"
3358
+ href="bar"
3359
+ data-bar="a current value"
3360
+ />
3361
+ <link
3362
+ rel="preload"
3363
+ as="font"
3364
+ href="bar"
3365
+ data-bar="a current value"
3366
+ crossOrigin=""
3367
+ />
3368
+ </head>
3369
+ </html>,
3370
+ );
3371
+ pipe(writable);
3372
+ });
3373
+ expect(getVisibleChildren(document)).toEqual(
3374
+ <html>
3375
+ <head>
3376
+ <link
3377
+ rel="preload"
3378
+ as="style"
3379
+ href="foo"
3380
+ data-foo="a current value"
3381
+ />
3382
+ <link
3383
+ rel="preload"
3384
+ as="style"
3385
+ href="bar"
3386
+ data-bar="a current value"
3387
+ />
3388
+ </head>
3389
+ <body />
3390
+ </html>,
3391
+ );
3392
+
3393
+ if (__DEV__) {
3394
+ expect(mockError).toHaveBeenCalledTimes(2);
3395
+ expect(mockError).toHaveBeenCalledWith(
3396
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3397
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3398
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3399
+ 'preload Resource (as "style")',
3400
+ 'foo',
3401
+ 'an earlier instance of this Resource',
3402
+ '\n data-foo: "a new value" in latest props, "a current value" in original props',
3403
+ componentStack(['link', 'head', 'html']),
3404
+ );
3405
+ expect(mockError).toHaveBeenCalledWith(
3406
+ 'Warning: A %s is using the same href "%s" as a %s. This is always an error and React will only keep the first preload' +
3407
+ ' for any given href, discarding subsequent instances. To fix, find where you are using this href in link' +
3408
+ ' tags or in calls to ReactDOM.preload() or ReactDOM.preinit() and either make the Resource types agree or' +
3409
+ ' update the hrefs to be distinct for different Resource types.%s',
3410
+ 'preload Resource (as "font")',
3411
+ 'bar',
3412
+ 'preload Resource (as "style")',
3413
+ componentStack(['link', 'head', 'html']),
3414
+ );
3415
+ } else {
3416
+ expect(mockError).not.toHaveBeenCalled();
3417
+ }
3418
+ } finally {
3419
+ console.error = originalConsoleError;
3420
+ }
3421
+ });
3422
+
3423
+ // @gate enableFloat
3424
+ it('warns when an existing preload Resource has certain specific different props from a style Resource of the same href', async () => {
3425
+ const originalConsoleError = console.error;
3426
+ const mockError = jest.fn();
3427
+ console.error = (...args) => {
3428
+ mockError(...args.map(normalizeCodeLocInfo));
3429
+ };
3430
+ try {
3431
+ await actIntoEmptyDocument(() => {
3432
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3433
+ <html>
3434
+ <head>
3435
+ <link
3436
+ rel="preload"
3437
+ as="style"
3438
+ href="foo"
3439
+ crossOrigin="preload value"
3440
+ />
3441
+ <link
3442
+ rel="stylesheet"
3443
+ href="foo"
3444
+ precedence="foo"
3445
+ crossOrigin="style value"
3446
+ />
3447
+ </head>
3448
+ </html>,
3449
+ );
3450
+ pipe(writable);
3451
+ });
3452
+ expect(getVisibleChildren(document)).toEqual(
3453
+ <html>
3454
+ <head>
3455
+ <link
3456
+ rel="stylesheet"
3457
+ href="foo"
3458
+ data-rprec="foo"
3459
+ crossorigin="style value"
3460
+ />
3461
+ </head>
3462
+ <body />
3463
+ </html>,
3464
+ );
3465
+
3466
+ if (__DEV__) {
3467
+ expect(mockError).toHaveBeenCalledTimes(1);
3468
+ expect(mockError).toHaveBeenCalledWith(
3469
+ 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3470
+ ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3471
+ ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3472
+ 'style Resource',
3473
+ 'foo',
3474
+ 'a preload Resource (as "style") with the same href',
3475
+ '\n crossOrigin: "style value" in latest props, "preload value" in original props',
3476
+ componentStack(['link', 'head', 'html']),
3477
+ );
3478
+ } else {
3479
+ expect(mockError).not.toHaveBeenCalled();
3480
+ }
3481
+ } finally {
3482
+ console.error = originalConsoleError;
3483
+ }
3484
+ });
3485
+ });
3486
+
3487
+ describe('escaping', () => {
3488
+ // @gate enableFloat
3489
+ it('escapes hrefs when selecting matching elements in the document when rendering Resources', async () => {
3490
+ await actIntoEmptyDocument(() => {
3491
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3492
+ <html>
3493
+ <head />
3494
+ <body>
3495
+ <link rel="preload" href="preload" as="style" />
3496
+ <link rel="stylesheet" href="style" precedence="style" />
3497
+ <link rel="stylesheet" href="with\slashes" precedence="style" />
3498
+ <link rel="preload" href={'with\nnewline'} as="style" />
3499
+ <div id="container" />
3500
+ </body>
3501
+ </html>,
3502
+ );
3503
+ pipe(writable);
3504
+ });
3505
+
3506
+ container = document.getElementById('container');
3507
+ const root = ReactDOMClient.createRoot(container);
3508
+ root.render(
3509
+ <div>
3510
+ <link rel="preload" href={'preload"][rel="preload'} as="style" />
3511
+ <link
3512
+ rel="stylesheet"
3513
+ href={'style"][rel="stylesheet'}
3514
+ precedence="style"
3515
+ />
3516
+ <link rel="stylesheet" href={'with\\slashes'} precedence="style" />
3517
+ <link rel="preload" href={'with\nnewline'} as="style" />
3518
+ foo
3519
+ </div>,
3520
+ );
3521
+ expect(Scheduler).toFlushWithoutYielding();
3522
+ expect(getVisibleChildren(document)).toEqual(
3523
+ <html>
3524
+ <head>
3525
+ <link rel="preload" as="style" href="preload" />
3526
+ <link rel="preload" href={'with\nnewline'} as="style" />
3527
+ <link rel="stylesheet" href="style" data-rprec="style" />
3528
+ <link rel="stylesheet" href="with\slashes" data-rprec="style" />
3529
+ <link
3530
+ rel="stylesheet"
3531
+ href={'style"][rel="stylesheet'}
3532
+ data-rprec="style"
3533
+ />
3534
+ <link rel="preload" href={'preload"][rel="preload'} as="style" />
3535
+ <link rel="preload" href={'style"][rel="stylesheet'} as="style" />
3536
+ </head>
3537
+ <body>
3538
+ <div id="container">
3539
+ <div>foo</div>
3540
+ </div>
3541
+ </body>
3542
+ </html>,
3543
+ );
3544
+ });
3545
+
3546
+ // @gate enableFloat
3547
+ it('escapes hrefs when selecting matching elements in the document when using preload and preinit', async () => {
3548
+ await actIntoEmptyDocument(() => {
3549
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3550
+ <html>
3551
+ <head />
3552
+ <body>
3553
+ <link rel="preload" href="preload" as="style" />
3554
+ <link rel="stylesheet" href="style" precedence="style" />
3555
+ <link rel="stylesheet" href="with\slashes" precedence="style" />
3556
+ <link rel="preload" href={'with\nnewline'} as="style" />
3557
+ <div id="container" />
3558
+ </body>
3559
+ </html>,
3560
+ );
3561
+ pipe(writable);
3562
+ });
3563
+
3564
+ function App() {
3565
+ ReactDOM.preload('preload"][rel="preload', {as: 'style'});
3566
+ ReactDOM.preinit('style"][rel="stylesheet', {
3567
+ as: 'style',
3568
+ precedence: 'style',
3569
+ });
3570
+ ReactDOM.preinit('with\\slashes', {
3571
+ as: 'style',
3572
+ precedence: 'style',
3573
+ });
3574
+ ReactDOM.preload('with\nnewline', {as: 'style'});
3575
+ return <div>foo</div>;
3576
+ }
3577
+
3578
+ container = document.getElementById('container');
3579
+ const root = ReactDOMClient.createRoot(container);
3580
+ root.render(<App />);
3581
+ expect(Scheduler).toFlushWithoutYielding();
3582
+ expect(getVisibleChildren(document)).toEqual(
3583
+ <html>
3584
+ <head>
3585
+ <link rel="preload" as="style" href="preload" />
3586
+ <link rel="preload" href={'with\nnewline'} as="style" />
3587
+ <link rel="stylesheet" href="style" data-rprec="style" />
3588
+ <link rel="stylesheet" href="with\slashes" data-rprec="style" />
3589
+ <link
3590
+ rel="stylesheet"
3591
+ href={'style"][rel="stylesheet'}
3592
+ data-rprec="style"
3593
+ />
3594
+ <link rel="preload" href={'preload"][rel="preload'} as="style" />
3595
+ </head>
3596
+ <body>
3597
+ <div id="container">
3598
+ <div>foo</div>
3599
+ </div>
3600
+ </body>
3601
+ </html>,
3602
+ );
3603
+ });
3604
+ });
3605
+});
packages/react-dom/src/client/ReactDOM.js
+22
-2
@@ -50,7 +50,12 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
50
import ReactVersion from 'shared/ReactVersion';
51
import {enableNewReconciler} from 'shared/ReactFeatureFlags';
52
53
-import {getClosestInstanceFromNode} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
53
+import {
54
+ getClosestInstanceFromNode,
55
+ getInstanceFromNode,
56
+ getNodeFromInstance,
57
+ getFiberCurrentPropsFromNode,
58
+} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
59
import {restoreControlledState} from 'react-dom-bindings/src/client/ReactDOMComponent';
60
import {
61
setAttemptSynchronousHydration,
@@ -61,7 +66,11 @@ import {
66
setAttemptHydrationAtPriority,
67
} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
68
import {setBatchingImplementation} from 'react-dom-bindings/src/events/ReactDOMUpdateBatching';
64
-import {setRestoreImplementation} from 'react-dom-bindings/src/events/ReactDOMControlledComponent';
69
+import {
70
+ setRestoreImplementation,
71
+ enqueueStateRestore,
72
+ restoreStateIfNeeded,
73
+} from 'react-dom-bindings/src/events/ReactDOMControlledComponent';
74
import Internals from '../ReactDOMSharedInternals';
75
76
setAttemptSynchronousHydration(attemptSynchronousHydration);
@@ -198,6 +207,17 @@ export {
207
runWithPriority as unstable_runWithPriority,
208
};
209
210
+// Keep in sync with ReactTestUtils.js.
211
+// This is an array for better minification.
212
+Internals.Events = [
213
+ getInstanceFromNode,
214
+ getNodeFromInstance,
215
+ getFiberCurrentPropsFromNode,
216
+ enqueueStateRestore,
217
+ restoreStateIfNeeded,
218
+ batchedUpdates,
219
+];
220
+
221
const foundDevTools = injectIntoDevTools({
222
findFiberByHostInstance: getClosestInstanceFromNode,
223
bundleType: __DEV__ ? 1 : 0,
packages/react-dom/src/client/ReactDOMRoot.js
+11
@@ -13,6 +13,9 @@ import type {
13
TransitionTracingCallbacks,
14
} from 'react-reconciler/src/ReactInternalTypes';
15
16
+import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
17
+const {Dispatcher} = ReactDOMSharedInternals;
18
+import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactDOMFloatClient';
19
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
20
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
21
import {enableFloat} from 'shared/ReactFeatureFlags';
@@ -235,6 +238,10 @@ export function createRoot(
238
);
239
markContainerAsRoot(root.current, container);
240
241
+ if (enableFloat) {
242
+ // Set the default dispatcher to the client dispatcher
243
+ Dispatcher.current = ReactDOMClientDispatcher;
244
+ }
245
const rootContainerElement: Document | Element | DocumentFragment =
246
container.nodeType === COMMENT_NODE
247
? (container.parentNode: any)
@@ -319,6 +326,10 @@ export function hydrateRoot(
326
transitionCallbacks,
327
);
328
markContainerAsRoot(root.current, container);
329
+ if (enableFloat) {
330
+ // Set the default dispatcher to the client dispatcher
331
+ Dispatcher.current = ReactDOMClientDispatcher;
332
+ }
333
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
334
listenToAllSupportedEvents(container);
335
packages/react-dom/src/test-utils/ReactTestUtils.js
+4
-1
@@ -13,6 +13,7 @@ import {
13
ClassComponent,
14
FunctionComponent,
15
HostComponent,
16
+ HostResource,
17
HostText,
18
} from 'react-reconciler/src/ReactWorkTags';
19
import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
@@ -21,6 +22,7 @@ import {
22
rethrowCaughtError,
23
invokeGuardedCallbackAndCatchFirstError,
24
} from 'shared/ReactErrorUtils';
25
+import {enableFloat} from 'shared/ReactFeatureFlags';
26
import assign from 'shared/assign';
27
import isArray from 'shared/isArray';
28
@@ -59,7 +61,8 @@ function findAllInRenderedFiberTreeInternal(fiber, test) {
61
node.tag === HostComponent ||
62
node.tag === HostText ||
63
node.tag === ClassComponent ||
62
- node.tag === FunctionComponent
64
+ node.tag === FunctionComponent ||
65
+ (enableFloat ? node.tag === HostResource : false)
66
) {
67
const publicInst = node.stateNode;
68
if (test(publicInst)) {
packages/react-native-renderer/src/ReactFabricHostConfig.js
+9
@@ -323,6 +323,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
323
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
324
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
325
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
326
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
327
328
export function appendInitialChild(
329
parentInstance: Instance,
@@ -615,3 +616,11 @@ export function detachDeletedInstance(node: Instance): void {
616
export function requestPostPaintCallback(callback: (time: number) => void) {
617
// noop
618
}
619
+
620
+export function prepareRendererToRender(container: Container): void {
621
+ // noop
622
+}
623
+
624
+export function resetRendererAfterRender() {
625
+ // noop
626
+}
packages/react-native-renderer/src/ReactNativeHostConfig.js
+9
@@ -89,6 +89,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
89
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
90
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
91
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
92
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
93
94
export function appendInitialChild(
95
parentInstance: Instance,
@@ -514,3 +515,11 @@ export function detachDeletedInstance(node: Instance): void {
515
export function requestPostPaintCallback(callback: (time: number) => void) {
516
// noop
517
}
518
+
519
+export function prepareRendererToRender(container: Container): void {
520
+ // noop
521
+}
522
+
523
+export function resetRendererAfterRender(): void {
524
+ // noop
525
+}
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+40
@@ -57,6 +57,9 @@ SUSPENSE_UPDATE_TO_COMPLETE[0] = SUSPENSE_UPDATE_TO_COMPLETE_TAG;
57
const SUSPENSE_UPDATE_TO_CLIENT_RENDER = new Uint8Array(1);
58
SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
59
60
+export type Resources = void;
61
+export type BoundaryResources = void;
62
+
63
// Per response,
64
export type ResponseState = {
65
nextSuspenseID: number,
@@ -142,6 +145,7 @@ export function pushStartInstance(
145
props: Object,
146
responseState: ResponseState,
147
formatContext: FormatContext,
148
+ textEmbedded: boolean,
149
): ReactNodeList {
150
target.push(
151
INSTANCE,
@@ -291,6 +295,7 @@ export function writeCompletedBoundaryInstruction(
295
responseState: ResponseState,
296
boundaryID: SuspenseBoundaryID,
297
contentSegmentID: number,
298
+ resources: BoundaryResources,
299
): boolean {
300
writeChunk(destination, SUSPENSE_UPDATE_TO_COMPLETE);
301
writeChunk(destination, formatID(boundaryID));
@@ -309,3 +314,38 @@ export function writeClientRenderBoundaryInstruction(
314
writeChunk(destination, SUSPENSE_UPDATE_TO_CLIENT_RENDER);
315
return writeChunkAndReturn(destination, formatID(boundaryID));
316
}
317
+
318
+export function writeInitialResources(
319
+ destination: Destination,
320
+ resources: Resources,
321
+ responseState: ResponseState,
322
+): boolean {
323
+ return true;
324
+}
325
+
326
+export function writeImmediateResources(
327
+ destination: Destination,
328
+ resources: Resources,
329
+ responseState: ResponseState,
330
+): boolean {
331
+ return true;
332
+}
333
+
334
+export function hoistResources(
335
+ resources: Resources,
336
+ boundaryResources: BoundaryResources,
337
+) {}
338
+
339
+export function hoistResourcesToRoot(
340
+ resources: Resources,
341
+ boundaryResources: BoundaryResources,
342
+) {}
343
+
344
+export function prepareToRender(resources: Resources) {}
345
+export function cleanupAfterRender(previousDispatcher: mixed) {}
346
+export function createResources() {}
347
+export function createBoundaryResources() {}
348
+export function setCurrentlyRenderingBoundaryResourcesTarget(
349
+ resources: Resources,
350
+ boundaryResources: ?BoundaryResources,
351
+) {}
packages/react-noop-renderer/src/ReactNoopServer.js
+19
@@ -51,6 +51,9 @@ type Destination = {
51
stack: Array<Segment | Instance | SuspenseInstance>,
52
};
53
54
+type Resources = null;
55
+type BoundaryResources = null;
56
+
57
const POP = Buffer.from('/', 'utf8');
58
59
function write(destination: Destination, buffer: Uint8Array): void {
@@ -263,6 +266,22 @@ const ReactNoopServer = ReactFizzServer({
266
): boolean {
267
boundary.status = 'client-render';
268
},
269
+
270
+ writeInitialResources() {},
271
+ writeImmediateResources() {},
272
+
273
+ createResources(): Resources {
274
+ return null;
275
+ },
276
+
277
+ createBoundaryResources(): BoundaryResources {
278
+ return null;
279
+ },
280
+
281
+ setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
282
+
283
+ prepareToRender() {},
284
+ cleanupAfterRender() {},
285
});
286
287
type Options = {
packages/react-noop-renderer/src/createReactNoop.js
+2
@@ -478,6 +478,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
478
const endTime = Scheduler.unstable_now();
479
callback(endTime);
480
},
481
+ prepareRendererToRender() {},
482
+ resetRendererAfterRender() {},
483
};
484
485
const hostConfig = useMutation
packages/react-reconciler/src/ReactFiber.new.js
+10
-1
@@ -21,6 +21,7 @@ import type {
21
} from './ReactFiberOffscreenComponent';
22
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
23
24
+import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
25
import {
26
createRootStrictEffectsByDefault,
27
enableCache,
@@ -32,6 +33,7 @@ import {
33
allowConcurrentByDefault,
34
enableTransitionTracing,
35
enableDebugTracing,
36
+ enableFloat,
37
} from 'shared/ReactFeatureFlags';
38
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
39
import {ConcurrentRoot} from './ReactRootTags';
@@ -42,6 +44,7 @@ import {
44
HostComponent,
45
HostText,
46
HostPortal,
47
+ HostResource,
48
ForwardRef,
49
Fragment,
50
Mode,
@@ -494,7 +497,13 @@ export function createFiberFromTypeAndProps(
497
}
498
}
499
} else if (typeof type === 'string') {
497
- fiberTag = HostComponent;
500
+ if (enableFloat && supportsResources) {
501
+ fiberTag = isHostResourceType(type, pendingProps)
502
+ ? HostResource
503
+ : HostComponent;
504
+ } else {
505
+ fiberTag = HostComponent;
506
+ }
507
} else {
508
getTag: switch (type) {
509
case REACT_FRAGMENT_TYPE:
packages/react-reconciler/src/ReactFiber.old.js
+10
-1
@@ -21,6 +21,7 @@ import type {
21
} from './ReactFiberOffscreenComponent';
22
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
23
24
+import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
25
import {
26
createRootStrictEffectsByDefault,
27
enableCache,
@@ -32,6 +33,7 @@ import {
33
allowConcurrentByDefault,
34
enableTransitionTracing,
35
enableDebugTracing,
36
+ enableFloat,
37
} from 'shared/ReactFeatureFlags';
38
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
39
import {ConcurrentRoot} from './ReactRootTags';
@@ -42,6 +44,7 @@ import {
44
HostComponent,
45
HostText,
46
HostPortal,
47
+ HostResource,
48
ForwardRef,
49
Fragment,
50
Mode,
@@ -494,7 +497,13 @@ export function createFiberFromTypeAndProps(
497
}
498
}
499
} else if (typeof type === 'string') {
497
- fiberTag = HostComponent;
500
+ if (enableFloat && supportsResources) {
501
+ fiberTag = isHostResourceType(type, pendingProps)
502
+ ? HostResource
503
+ : HostComponent;
504
+ } else {
505
+ fiberTag = HostComponent;
506
+ }
507
} else {
508
getTag: switch (type) {
509
case REACT_FRAGMENT_TYPE:
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+28
@@ -40,6 +40,7 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new
40
import {
41
enableCPUSuspense,
42
enableUseMutableSource,
43
+ enableFloat,
44
} from 'shared/ReactFeatureFlags';
45
46
import checkPropTypes from 'shared/checkPropTypes';
@@ -54,6 +55,7 @@ import {
55
ClassComponent,
56
HostRoot,
57
HostComponent,
58
+ HostResource,
59
HostText,
60
HostPortal,
61
ForwardRef,
@@ -161,7 +163,9 @@ import {
163
getSuspenseInstanceFallbackErrorDetails,
164
registerSuspenseInstanceRetry,
165
supportsHydration,
166
+ supportsResources,
167
isPrimaryRenderer,
168
+ getResource,
169
} from './ReactFiberHostConfig';
170
import type {SuspenseInstance} from './ReactFiberHostConfig';
171
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@@ -1580,6 +1584,24 @@ function updateHostComponent(
1584
return workInProgress.child;
1585
}
1586
1587
+function updateHostResource(current, workInProgress, renderLanes) {
1588
+ pushHostContext(workInProgress);
1589
+ markRef(current, workInProgress);
1590
+ const currentProps = current === null ? null : current.memoizedProps;
1591
+ workInProgress.memoizedState = getResource(
1592
+ workInProgress.type,
1593
+ workInProgress.pendingProps,
1594
+ currentProps,
1595
+ );
1596
+ reconcileChildren(
1597
+ current,
1598
+ workInProgress,
1599
+ workInProgress.pendingProps.children,
1600
+ renderLanes,
1601
+ );
1602
+ return workInProgress.child;
1603
+}
1604
+
1605
function updateHostText(current, workInProgress) {
1606
if (current === null) {
1607
tryToClaimNextHydratableInstance(workInProgress);
@@ -3651,6 +3673,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3673
}
3674
resetHydrationState();
3675
break;
3676
+ case HostResource:
3677
case HostComponent:
3678
pushHostContext(workInProgress);
3679
break;
@@ -3985,6 +4008,11 @@ function beginWork(
4008
}
4009
case HostRoot:
4010
return updateHostRoot(current, workInProgress, renderLanes);
4011
+ case HostResource:
4012
+ if (enableFloat && supportsResources) {
4013
+ return updateHostResource(current, workInProgress, renderLanes);
4014
+ }
4015
+ // eslint-disable-next-line no-fallthrough
4016
case HostComponent:
4017
return updateHostComponent(current, workInProgress, renderLanes);
4018
case HostText:
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+28
@@ -40,6 +40,7 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old
40
import {
41
enableCPUSuspense,
42
enableUseMutableSource,
43
+ enableFloat,
44
} from 'shared/ReactFeatureFlags';
45
46
import checkPropTypes from 'shared/checkPropTypes';
@@ -54,6 +55,7 @@ import {
55
ClassComponent,
56
HostRoot,
57
HostComponent,
58
+ HostResource,
59
HostText,
60
HostPortal,
61
ForwardRef,
@@ -161,7 +163,9 @@ import {
163
getSuspenseInstanceFallbackErrorDetails,
164
registerSuspenseInstanceRetry,
165
supportsHydration,
166
+ supportsResources,
167
isPrimaryRenderer,
168
+ getResource,
169
} from './ReactFiberHostConfig';
170
import type {SuspenseInstance} from './ReactFiberHostConfig';
171
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@@ -1580,6 +1584,24 @@ function updateHostComponent(
1584
return workInProgress.child;
1585
}
1586
1587
+function updateHostResource(current, workInProgress, renderLanes) {
1588
+ pushHostContext(workInProgress);
1589
+ markRef(current, workInProgress);
1590
+ const currentProps = current === null ? null : current.memoizedProps;
1591
+ workInProgress.memoizedState = getResource(
1592
+ workInProgress.type,
1593
+ workInProgress.pendingProps,
1594
+ currentProps,
1595
+ );
1596
+ reconcileChildren(
1597
+ current,
1598
+ workInProgress,
1599
+ workInProgress.pendingProps.children,
1600
+ renderLanes,
1601
+ );
1602
+ return workInProgress.child;
1603
+}
1604
+
1605
function updateHostText(current, workInProgress) {
1606
if (current === null) {
1607
tryToClaimNextHydratableInstance(workInProgress);
@@ -3651,6 +3673,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3673
}
3674
resetHydrationState();
3675
break;
3676
+ case HostResource:
3677
case HostComponent:
3678
pushHostContext(workInProgress);
3679
break;
@@ -3985,6 +4008,11 @@ function beginWork(
4008
}
4009
case HostRoot:
4010
return updateHostRoot(current, workInProgress, renderLanes);
4011
+ case HostResource:
4012
+ if (enableFloat && supportsResources) {
4013
+ return updateHostResource(current, workInProgress, renderLanes);
4014
+ }
4015
+ // eslint-disable-next-line no-fallthrough
4016
case HostComponent:
4017
return updateHostComponent(current, workInProgress, renderLanes);
4018
case HostText:
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+75
-3
@@ -51,6 +51,7 @@ import {
51
enableTransitionTracing,
52
enableUseEventHook,
53
enableStrictEffects,
54
+ enableFloat,
55
} from 'shared/ReactFeatureFlags';
56
import {
57
FunctionComponent,
@@ -58,6 +59,7 @@ import {
59
ClassComponent,
60
HostRoot,
61
HostComponent,
62
+ HostResource,
63
HostText,
64
HostPortal,
65
Profiler,
@@ -73,7 +75,6 @@ import {
75
CacheComponent,
76
TracingMarkerComponent,
77
} from './ReactWorkTags';
76
-import {detachDeletedInstance} from './ReactFiberHostConfig';
78
import {
79
NoFlags,
80
ContentReset,
@@ -117,6 +118,7 @@ import {
118
supportsMutation,
119
supportsPersistence,
120
supportsHydration,
121
+ supportsResources,
122
commitMount,
123
commitUpdate,
124
resetTextContent,
@@ -141,6 +143,9 @@ import {
143
prepareScopeUpdate,
144
prepareForCommit,
145
beforeActiveInstanceBlur,
146
+ detachDeletedInstance,
147
+ acquireResource,
148
+ releaseResource,
149
} from './ReactFiberHostConfig';
150
import {
151
captureCommitPhaseError,
@@ -493,6 +498,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
498
break;
499
}
500
case HostComponent:
501
+ case HostResource:
502
case HostText:
503
case HostPortal:
504
case IncompleteClassComponent:
@@ -1064,6 +1070,34 @@ function commitLayoutEffectOnFiber(
1070
}
1071
break;
1072
}
1073
+ case HostResource: {
1074
+ if (enableFloat && supportsResources) {
1075
+ recursivelyTraverseLayoutEffects(
1076
+ finishedRoot,
1077
+ finishedWork,
1078
+ committedLanes,
1079
+ );
1080
+
1081
+ if (flags & Update) {
1082
+ const newResource = finishedWork.memoizedState;
1083
+ if (current !== null) {
1084
+ const currentResource = current.memoizedState;
1085
+ if (currentResource !== newResource) {
1086
+ releaseResource(currentResource);
1087
+ }
1088
+ }
1089
+ finishedWork.stateNode = newResource
1090
+ ? acquireResource(newResource)
1091
+ : null;
1092
+ }
1093
+
1094
+ if (flags & Ref) {
1095
+ safelyAttachRef(finishedWork, finishedWork.return);
1096
+ }
1097
+ break;
1098
+ }
1099
+ }
1100
+ // eslint-disable-next-line-no-fallthrough
1101
case HostComponent: {
1102
recursivelyTraverseLayoutEffects(
1103
finishedRoot,
@@ -1450,7 +1484,10 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1484
// children to find all the terminal nodes.
1485
let node: Fiber = finishedWork;
1486
while (true) {
1453
- if (node.tag === HostComponent) {
1487
+ if (
1488
+ node.tag === HostComponent ||
1489
+ (enableFloat && supportsResources ? node.tag === HostResource : false)
1490
+ ) {
1491
if (hostSubtreeRoot === null) {
1492
hostSubtreeRoot = node;
1493
try {
@@ -1522,6 +1559,7 @@ function commitAttachRef(finishedWork: Fiber) {
1559
const instance = finishedWork.stateNode;
1560
let instanceToUse;
1561
switch (finishedWork.tag) {
1562
+ case HostResource:
1563
case HostComponent:
1564
instanceToUse = getPublicInstance(instance);
1565
break;
@@ -1726,7 +1764,8 @@ function isHostParent(fiber: Fiber): boolean {
1764
return (
1765
fiber.tag === HostComponent ||
1766
fiber.tag === HostRoot ||
1729
- fiber.tag === HostPortal
1767
+ fiber.tag === HostPortal ||
1768
+ (enableFloat && supportsResources ? fiber.tag === HostResource : false)
1769
);
1770
}
1771
@@ -1973,6 +2012,23 @@ function commitDeletionEffectsOnFiber(
2012
// into their subtree. There are simpler cases in the inner switch
2013
// that don't modify the stack.
2014
switch (deletedFiber.tag) {
2015
+ case HostResource: {
2016
+ if (enableFloat && supportsResources) {
2017
+ if (!offscreenSubtreeWasHidden) {
2018
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
2019
+ }
2020
+
2021
+ recursivelyTraverseDeletionEffects(
2022
+ finishedRoot,
2023
+ nearestMountedAncestor,
2024
+ deletedFiber,
2025
+ );
2026
+
2027
+ releaseResource(deletedFiber.memoizedState);
2028
+ return;
2029
+ }
2030
+ }
2031
+ // eslint-disable-next-line no-fallthrough
2032
case HostComponent: {
2033
if (!offscreenSubtreeWasHidden) {
2034
safelyDetachRef(deletedFiber, nearestMountedAncestor);
@@ -2469,6 +2525,20 @@ function commitMutationEffectsOnFiber(
2525
}
2526
return;
2527
}
2528
+ case HostResource: {
2529
+ if (enableFloat && supportsResources) {
2530
+ recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2531
+ commitReconciliationEffects(finishedWork);
2532
+
2533
+ if (flags & Ref) {
2534
+ if (current !== null) {
2535
+ safelyDetachRef(current, current.return);
2536
+ }
2537
+ }
2538
+ return;
2539
+ }
2540
+ }
2541
+ // eslint-disable-next-line-no-fallthrough
2542
case HostComponent: {
2543
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2544
commitReconciliationEffects(finishedWork);
@@ -2860,6 +2930,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2930
recursivelyTraverseDisappearLayoutEffects(finishedWork);
2931
break;
2932
}
2933
+ case HostResource:
2934
case HostComponent: {
2935
// TODO (Offscreen) Check: flags & RefStatic
2936
safelyDetachRef(finishedWork, finishedWork.return);
@@ -2961,6 +3032,7 @@ export function reappearLayoutEffects(
3032
// case HostRoot: {
3033
// ...
3034
// }
3035
+ case HostResource:
3036
case HostComponent: {
3037
recursivelyTraverseReappearLayoutEffects(
3038
finishedRoot,
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+75
-3
@@ -51,6 +51,7 @@ import {
51
enableTransitionTracing,
52
enableUseEventHook,
53
enableStrictEffects,
54
+ enableFloat,
55
} from 'shared/ReactFeatureFlags';
56
import {
57
FunctionComponent,
@@ -58,6 +59,7 @@ import {
59
ClassComponent,
60
HostRoot,
61
HostComponent,
62
+ HostResource,
63
HostText,
64
HostPortal,
65
Profiler,
@@ -73,7 +75,6 @@ import {
75
CacheComponent,
76
TracingMarkerComponent,
77
} from './ReactWorkTags';
76
-import {detachDeletedInstance} from './ReactFiberHostConfig';
78
import {
79
NoFlags,
80
ContentReset,
@@ -117,6 +118,7 @@ import {
118
supportsMutation,
119
supportsPersistence,
120
supportsHydration,
121
+ supportsResources,
122
commitMount,
123
commitUpdate,
124
resetTextContent,
@@ -141,6 +143,9 @@ import {
143
prepareScopeUpdate,
144
prepareForCommit,
145
beforeActiveInstanceBlur,
146
+ detachDeletedInstance,
147
+ acquireResource,
148
+ releaseResource,
149
} from './ReactFiberHostConfig';
150
import {
151
captureCommitPhaseError,
@@ -493,6 +498,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
498
break;
499
}
500
case HostComponent:
501
+ case HostResource:
502
case HostText:
503
case HostPortal:
504
case IncompleteClassComponent:
@@ -1064,6 +1070,34 @@ function commitLayoutEffectOnFiber(
1070
}
1071
break;
1072
}
1073
+ case HostResource: {
1074
+ if (enableFloat && supportsResources) {
1075
+ recursivelyTraverseLayoutEffects(
1076
+ finishedRoot,
1077
+ finishedWork,
1078
+ committedLanes,
1079
+ );
1080
+
1081
+ if (flags & Update) {
1082
+ const newResource = finishedWork.memoizedState;
1083
+ if (current !== null) {
1084
+ const currentResource = current.memoizedState;
1085
+ if (currentResource !== newResource) {
1086
+ releaseResource(currentResource);
1087
+ }
1088
+ }
1089
+ finishedWork.stateNode = newResource
1090
+ ? acquireResource(newResource)
1091
+ : null;
1092
+ }
1093
+
1094
+ if (flags & Ref) {
1095
+ safelyAttachRef(finishedWork, finishedWork.return);
1096
+ }
1097
+ break;
1098
+ }
1099
+ }
1100
+ // eslint-disable-next-line-no-fallthrough
1101
case HostComponent: {
1102
recursivelyTraverseLayoutEffects(
1103
finishedRoot,
@@ -1450,7 +1484,10 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1484
// children to find all the terminal nodes.
1485
let node: Fiber = finishedWork;
1486
while (true) {
1453
- if (node.tag === HostComponent) {
1487
+ if (
1488
+ node.tag === HostComponent ||
1489
+ (enableFloat && supportsResources ? node.tag === HostResource : false)
1490
+ ) {
1491
if (hostSubtreeRoot === null) {
1492
hostSubtreeRoot = node;
1493
try {
@@ -1522,6 +1559,7 @@ function commitAttachRef(finishedWork: Fiber) {
1559
const instance = finishedWork.stateNode;
1560
let instanceToUse;
1561
switch (finishedWork.tag) {
1562
+ case HostResource:
1563
case HostComponent:
1564
instanceToUse = getPublicInstance(instance);
1565
break;
@@ -1726,7 +1764,8 @@ function isHostParent(fiber: Fiber): boolean {
1764
return (
1765
fiber.tag === HostComponent ||
1766
fiber.tag === HostRoot ||
1729
- fiber.tag === HostPortal
1767
+ fiber.tag === HostPortal ||
1768
+ (enableFloat && supportsResources ? fiber.tag === HostResource : false)
1769
);
1770
}
1771
@@ -1973,6 +2012,23 @@ function commitDeletionEffectsOnFiber(
2012
// into their subtree. There are simpler cases in the inner switch
2013
// that don't modify the stack.
2014
switch (deletedFiber.tag) {
2015
+ case HostResource: {
2016
+ if (enableFloat && supportsResources) {
2017
+ if (!offscreenSubtreeWasHidden) {
2018
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
2019
+ }
2020
+
2021
+ recursivelyTraverseDeletionEffects(
2022
+ finishedRoot,
2023
+ nearestMountedAncestor,
2024
+ deletedFiber,
2025
+ );
2026
+
2027
+ releaseResource(deletedFiber.memoizedState);
2028
+ return;
2029
+ }
2030
+ }
2031
+ // eslint-disable-next-line no-fallthrough
2032
case HostComponent: {
2033
if (!offscreenSubtreeWasHidden) {
2034
safelyDetachRef(deletedFiber, nearestMountedAncestor);
@@ -2469,6 +2525,20 @@ function commitMutationEffectsOnFiber(
2525
}
2526
return;
2527
}
2528
+ case HostResource: {
2529
+ if (enableFloat && supportsResources) {
2530
+ recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2531
+ commitReconciliationEffects(finishedWork);
2532
+
2533
+ if (flags & Ref) {
2534
+ if (current !== null) {
2535
+ safelyDetachRef(current, current.return);
2536
+ }
2537
+ }
2538
+ return;
2539
+ }
2540
+ }
2541
+ // eslint-disable-next-line-no-fallthrough
2542
case HostComponent: {
2543
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2544
commitReconciliationEffects(finishedWork);
@@ -2860,6 +2930,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2930
recursivelyTraverseDisappearLayoutEffects(finishedWork);
2931
break;
2932
}
2933
+ case HostResource:
2934
case HostComponent: {
2935
// TODO (Offscreen) Check: flags & RefStatic
2936
safelyDetachRef(finishedWork, finishedWork.return);
@@ -2961,6 +3032,7 @@ export function reappearLayoutEffects(
3032
// case HostRoot: {
3033
// ...
3034
// }
3035
+ case HostResource:
3036
case HostComponent: {
3037
recursivelyTraverseReappearLayoutEffects(
3038
finishedRoot,
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+23
@@ -45,6 +45,7 @@ import {
45
ClassComponent,
46
HostRoot,
47
HostComponent,
48
+ HostResource,
49
HostText,
50
HostPortal,
51
ContextProvider,
@@ -92,6 +93,7 @@ import {
93
prepareUpdate,
94
supportsMutation,
95
supportsPersistence,
96
+ supportsResources,
97
cloneInstance,
98
cloneHiddenInstance,
99
cloneHiddenTextInstance,
@@ -144,6 +146,7 @@ import {
146
enableProfilerTimer,
147
enableCache,
148
enableTransitionTracing,
149
+ enableFloat,
150
} from 'shared/ReactFeatureFlags';
151
import {
152
renderDidSuspend,
@@ -954,6 +957,26 @@ function completeWork(
957
}
958
return null;
959
}
960
+ case HostResource: {
961
+ if (enableFloat && supportsResources) {
962
+ popHostContext(workInProgress);
963
+ const currentRef = current ? current.ref : null;
964
+ if (currentRef !== workInProgress.ref) {
965
+ markRef(workInProgress);
966
+ }
967
+ if (
968
+ current === null ||
969
+ current.memoizedState !== workInProgress.memoizedState
970
+ ) {
971
+ // The workInProgress resource is different than the current one or the current
972
+ // one does not exist
973
+ markUpdate(workInProgress);
974
+ }
975
+ bubbleProperties(workInProgress);
976
+ return null;
977
+ }
978
+ }
979
+ // eslint-disable-next-line-no-fallthrough
980
case HostComponent: {
981
popHostContext(workInProgress);
982
const type = workInProgress.type;
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+23
@@ -45,6 +45,7 @@ import {
45
ClassComponent,
46
HostRoot,
47
HostComponent,
48
+ HostResource,
49
HostText,
50
HostPortal,
51
ContextProvider,
@@ -92,6 +93,7 @@ import {
93
prepareUpdate,
94
supportsMutation,
95
supportsPersistence,
96
+ supportsResources,
97
cloneInstance,
98
cloneHiddenInstance,
99
cloneHiddenTextInstance,
@@ -144,6 +146,7 @@ import {
146
enableProfilerTimer,
147
enableCache,
148
enableTransitionTracing,
149
+ enableFloat,
150
} from 'shared/ReactFeatureFlags';
151
import {
152
renderDidSuspend,
@@ -954,6 +957,26 @@ function completeWork(
957
}
958
return null;
959
}
960
+ case HostResource: {
961
+ if (enableFloat && supportsResources) {
962
+ popHostContext(workInProgress);
963
+ const currentRef = current ? current.ref : null;
964
+ if (currentRef !== workInProgress.ref) {
965
+ markRef(workInProgress);
966
+ }
967
+ if (
968
+ current === null ||
969
+ current.memoizedState !== workInProgress.memoizedState
970
+ ) {
971
+ // The workInProgress resource is different than the current one or the current
972
+ // one does not exist
973
+ markUpdate(workInProgress);
974
+ }
975
+ bubbleProperties(workInProgress);
976
+ return null;
977
+ }
978
+ }
979
+ // eslint-disable-next-line-no-fallthrough
980
case HostComponent: {
981
popHostContext(workInProgress);
982
const type = workInProgress.type;
packages/react-reconciler/src/ReactFiberComponentStack.js
+2
@@ -11,6 +11,7 @@ import type {Fiber} from './ReactInternalTypes';
11
12
import {
13
HostComponent,
14
+ HostResource,
15
LazyComponent,
16
SuspenseComponent,
17
SuspenseListComponent,
@@ -34,6 +35,7 @@ function describeFiber(fiber: Fiber): string {
35
: null;
36
const source = __DEV__ ? fiber._debugSource : null;
37
switch (fiber.tag) {
38
+ case HostResource:
39
case HostComponent:
40
return describeBuiltInComponentFrame(fiber.type, source, owner);
41
case LazyComponent:
packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
-2
@@ -24,12 +24,10 @@ export const supportsHydration = false;
24
export const canHydrateInstance = shim;
25
export const canHydrateTextInstance = shim;
26
export const canHydrateSuspenseInstance = shim;
27
-export const isHydratableResource = shim;
27
export const isSuspenseInstancePending = shim;
28
export const isSuspenseInstanceFallback = shim;
29
export const getSuspenseInstanceFallbackErrorDetails = shim;
30
export const registerSuspenseInstanceRetry = shim;
32
-export const getMatchingResourceInstance = shim;
31
export const getNextHydratableSibling = shim;
32
export const getFirstHydratableChild = shim;
33
export const getFirstHydratableChildWithinContainer = shim;
packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js
new
+26
@@ -0,0 +1,26 @@
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
+// Renderers that don't support hydration
11
+// can re-export everything from this module.
12
+
13
+function shim(...args: any) {
14
+ throw new Error(
15
+ 'The current renderer does not support Resources. ' +
16
+ 'This error is likely caused by a bug in React. ' +
17
+ 'Please file an issue.',
18
+ );
19
+}
20
+
21
+// Resources (when unsupported)
22
+export const supportsResources = false;
23
+export const isHostResourceType = shim;
24
+export const getResource = shim;
25
+export const acquireResource = shim;
26
+export const releaseResource = shim;
packages/react-reconciler/src/ReactFiberHotReloading.new.js
+6
-1
@@ -37,6 +37,7 @@ import {
37
FunctionComponent,
38
ForwardRef,
39
HostComponent,
40
+ HostResource,
41
HostPortal,
42
HostRoot,
43
MemoComponent,
@@ -47,6 +48,7 @@ import {
48
REACT_MEMO_TYPE,
49
REACT_LAZY_TYPE,
50
} from 'shared/ReactSymbols';
51
+import {enableFloat} from 'shared/ReactFeatureFlags';
52
53
let resolveFamily: RefreshHandler | null = null;
54
// $FlowFixMe Flow gets confused by a WeakSet feature check below.
@@ -449,7 +451,10 @@ function findChildHostInstancesForFiberShallowly(
451
let node: Fiber = fiber;
452
let foundHostInstances = false;
453
while (true) {
452
- if (node.tag === HostComponent) {
454
+ if (
455
+ node.tag === HostComponent ||
456
+ (enableFloat ? node.tag === HostResource : false)
457
+ ) {
458
// We got a match.
459
foundHostInstances = true;
460
hostInstances.add(node.stateNode);
packages/react-reconciler/src/ReactFiberHotReloading.old.js
+6
-1
@@ -37,6 +37,7 @@ import {
37
FunctionComponent,
38
ForwardRef,
39
HostComponent,
40
+ HostResource,
41
HostPortal,
42
HostRoot,
43
MemoComponent,
@@ -47,6 +48,7 @@ import {
48
REACT_MEMO_TYPE,
49
REACT_LAZY_TYPE,
50
} from 'shared/ReactSymbols';
51
+import {enableFloat} from 'shared/ReactFeatureFlags';
52
53
let resolveFamily: RefreshHandler | null = null;
54
// $FlowFixMe Flow gets confused by a WeakSet feature check below.
@@ -449,7 +451,10 @@ function findChildHostInstancesForFiberShallowly(
451
let node: Fiber = fiber;
452
let foundHostInstances = false;
453
while (true) {
452
- if (node.tag === HostComponent) {
454
+ if (
455
+ node.tag === HostComponent ||
456
+ (enableFloat ? node.tag === HostResource : false)
457
+ ) {
458
// We got a match.
459
foundHostInstances = true;
460
hostInstances.add(node.stateNode);
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
-41
@@ -34,7 +34,6 @@ import {
34
NoFlags,
35
DidCapture,
36
} from './ReactFiberFlags';
37
-import {enableFloat} from 'shared/ReactFeatureFlags';
37
38
import {
39
createFiberFromHostInstanceForDeletion,
@@ -46,9 +45,7 @@ import {
45
canHydrateInstance,
46
canHydrateTextInstance,
47
canHydrateSuspenseInstance,
49
- isHydratableResource,
48
getNextHydratableSibling,
51
- getMatchingResourceInstance,
49
getFirstHydratableChild,
50
getFirstHydratableChildWithinContainer,
51
getFirstHydratableChildWithinSuspenseInstance,
@@ -78,7 +75,6 @@ import {
75
restoreSuspendedTreeContext,
76
} from './ReactFiberTreeContext.new';
77
import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';
81
-import {getRootHostContainer} from './ReactFiberHostContext.new';
78
79
// The deepest Fiber on the stack involved in a hydration context.
80
// This may have been an insertion or a hydration.
@@ -408,19 +404,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
404
if (!isHydrating) {
405
return;
406
}
411
- if (enableFloat) {
412
- if (
413
- fiber.tag === HostComponent &&
414
- isHydratableResource(fiber.type, fiber.pendingProps)
415
- ) {
416
- fiber.stateNode = getMatchingResourceInstance(
417
- fiber.type,
418
- fiber.pendingProps,
419
- getRootHostContainer(),
420
- );
421
- return;
422
- }
423
- }
407
let nextInstance = nextHydratableInstance;
408
if (!nextInstance) {
409
if (shouldClientRenderOnMismatch(fiber)) {
@@ -613,30 +596,6 @@ function popHydrationState(fiber: Fiber): boolean {
596
if (!supportsHydration) {
597
return false;
598
}
616
- if (
617
- enableFloat &&
618
- isHydrating &&
619
- isHydratableResource(fiber.type, fiber.memoizedProps)
620
- ) {
621
- if (fiber.stateNode === null) {
622
- if (__DEV__) {
623
- const rel = fiber.memoizedProps.rel
624
- ? `rel="${fiber.memoizedProps.rel}" `
625
- : '';
626
- const href = fiber.memoizedProps.href
627
- ? `href="${fiber.memoizedProps.href}"`
628
- : '';
629
- console.error(
630
- 'A matching Hydratable Resource was not found in the DOM for <%s %s%s>.',
631
- fiber.type,
632
- rel,
633
- href,
634
- );
635
- }
636
- throwOnHydrationMismatch(fiber);
637
- }
638
- return true;
639
- }
599
if (fiber !== hydrationParentFiber) {
600
// We're deeper than the current hydration context, inside an inserted
601
// tree.
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
-41
@@ -34,7 +34,6 @@ import {
34
NoFlags,
35
DidCapture,
36
} from './ReactFiberFlags';
37
-import {enableFloat} from 'shared/ReactFeatureFlags';
37
38
import {
39
createFiberFromHostInstanceForDeletion,
@@ -46,9 +45,7 @@ import {
45
canHydrateInstance,
46
canHydrateTextInstance,
47
canHydrateSuspenseInstance,
49
- isHydratableResource,
48
getNextHydratableSibling,
51
- getMatchingResourceInstance,
49
getFirstHydratableChild,
50
getFirstHydratableChildWithinContainer,
51
getFirstHydratableChildWithinSuspenseInstance,
@@ -78,7 +75,6 @@ import {
75
restoreSuspendedTreeContext,
76
} from './ReactFiberTreeContext.old';
77
import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';
81
-import {getRootHostContainer} from './ReactFiberHostContext.old';
78
79
// The deepest Fiber on the stack involved in a hydration context.
80
// This may have been an insertion or a hydration.
@@ -408,19 +404,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
404
if (!isHydrating) {
405
return;
406
}
411
- if (enableFloat) {
412
- if (
413
- fiber.tag === HostComponent &&
414
- isHydratableResource(fiber.type, fiber.pendingProps)
415
- ) {
416
- fiber.stateNode = getMatchingResourceInstance(
417
- fiber.type,
418
- fiber.pendingProps,
419
- getRootHostContainer(),
420
- );
421
- return;
422
- }
423
- }
407
let nextInstance = nextHydratableInstance;
408
if (!nextInstance) {
409
if (shouldClientRenderOnMismatch(fiber)) {
@@ -613,30 +596,6 @@ function popHydrationState(fiber: Fiber): boolean {
596
if (!supportsHydration) {
597
return false;
598
}
616
- if (
617
- enableFloat &&
618
- isHydrating &&
619
- isHydratableResource(fiber.type, fiber.memoizedProps)
620
- ) {
621
- if (fiber.stateNode === null) {
622
- if (__DEV__) {
623
- const rel = fiber.memoizedProps.rel
624
- ? `rel="${fiber.memoizedProps.rel}" `
625
- : '';
626
- const href = fiber.memoizedProps.href
627
- ? `href="${fiber.memoizedProps.href}"`
628
- : '';
629
- console.error(
630
- 'A matching Hydratable Resource was not found in the DOM for <%s %s%s>.',
631
- fiber.type,
632
- rel,
633
- href,
634
- );
635
- }
636
- throwOnHydrationMismatch(fiber);
637
- }
638
- return true;
639
- }
599
if (fiber !== hydrationParentFiber) {
600
// We're deeper than the current hydration context, inside an inserted
601
// tree.
packages/react-reconciler/src/ReactFiberTreeReflection.js
+12
-2
@@ -17,12 +17,14 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom
17
import {
18
ClassComponent,
19
HostComponent,
20
+ HostResource,
21
HostRoot,
22
HostPortal,
23
HostText,
24
SuspenseComponent,
25
} from './ReactWorkTags';
26
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
27
+import {enableFloat} from 'shared/ReactFeatureFlags';
28
29
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
30
@@ -273,7 +275,11 @@ export function findCurrentHostFiber(parent: Fiber): Fiber | null {
275
276
function findCurrentHostFiberImpl(node: Fiber) {
277
// Next we'll drill down this component to find the first HostComponent/Text.
276
- if (node.tag === HostComponent || node.tag === HostText) {
278
+ if (
279
+ node.tag === HostComponent ||
280
+ node.tag === HostText ||
281
+ (enableFloat ? node.tag === HostResource : false)
282
+ ) {
283
return node;
284
}
285
@@ -298,7 +304,11 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
304
305
function findCurrentHostFiberWithNoPortalsImpl(node: Fiber) {
306
// Next we'll drill down this component to find the first HostComponent/Text.
301
- if (node.tag === HostComponent || node.tag === HostText) {
307
+ if (
308
+ node.tag === HostComponent ||
309
+ node.tag === HostText ||
310
+ (enableFloat ? node.tag === HostResource : false)
311
+ ) {
312
return node;
313
}
314
packages/react-reconciler/src/ReactFiberUnwindWork.new.js
+3
@@ -19,6 +19,7 @@ import {
19
ClassComponent,
20
HostRoot,
21
HostComponent,
22
+ HostResource,
23
HostPortal,
24
ContextProvider,
25
SuspenseComponent,
@@ -115,6 +116,7 @@ function unwindWork(
116
// We unwound to the root without completing it. Exit.
117
return null;
118
}
119
+ case HostResource:
120
case HostComponent: {
121
// TODO: popHydrationState
122
popHostContext(workInProgress);
@@ -233,6 +235,7 @@ function unwindInterruptedWork(
235
resetMutableSourceWorkInProgressVersions();
236
break;
237
}
238
+ case HostResource:
239
case HostComponent: {
240
popHostContext(interruptedWork);
241
break;
packages/react-reconciler/src/ReactFiberUnwindWork.old.js
+3
@@ -19,6 +19,7 @@ import {
19
ClassComponent,
20
HostRoot,
21
HostComponent,
22
+ HostResource,
23
HostPortal,
24
ContextProvider,
25
SuspenseComponent,
@@ -115,6 +116,7 @@ function unwindWork(
116
// We unwound to the root without completing it. Exit.
117
return null;
118
}
119
+ case HostResource:
120
case HostComponent: {
121
// TODO: popHydrationState
122
popHostContext(workInProgress);
@@ -233,6 +235,7 @@ function unwindInterruptedWork(
235
resetMutableSourceWorkInProgressVersions();
236
break;
237
}
238
+ case HostResource:
239
case HostComponent: {
240
popHostContext(interruptedWork);
241
break;
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+7
-3
@@ -84,6 +84,8 @@ import {
84
supportsMicrotasks,
85
errorHydratingContainer,
86
scheduleMicrotask,
87
+ prepareRendererToRender,
88
+ resetRendererAfterRender,
89
} from './ReactFiberHostConfig';
90
91
import {
@@ -1767,7 +1769,8 @@ function handleThrow(root, thrownValue): void {
1769
}
1770
}
1771
1770
-function pushDispatcher() {
1772
+function pushDispatcher(container) {
1773
+ prepareRendererToRender(container);
1774
const prevDispatcher = ReactCurrentDispatcher.current;
1775
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
1776
if (prevDispatcher === null) {
@@ -1781,6 +1784,7 @@ function pushDispatcher() {
1784
}
1785
1786
function popDispatcher(prevDispatcher) {
1787
+ resetRendererAfterRender();
1788
ReactCurrentDispatcher.current = prevDispatcher;
1789
}
1790
@@ -1850,7 +1854,7 @@ export function renderHasNotSuspendedYet(): boolean {
1854
function renderRootSync(root: FiberRoot, lanes: Lanes) {
1855
const prevExecutionContext = executionContext;
1856
executionContext |= RenderContext;
1853
- const prevDispatcher = pushDispatcher();
1857
+ const prevDispatcher = pushDispatcher(root.containerInfo);
1858
1859
// If the root or lanes have changed, throw out the existing stack
1860
// and prepare a fresh one. Otherwise we'll continue where we left off.
@@ -1950,7 +1954,7 @@ function workLoopSync() {
1954
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1955
const prevExecutionContext = executionContext;
1956
executionContext |= RenderContext;
1953
- const prevDispatcher = pushDispatcher();
1957
+ const prevDispatcher = pushDispatcher(root.containerInfo);
1958
1959
// If the root or lanes have changed, throw out the existing stack
1960
// and prepare a fresh one. Otherwise we'll continue where we left off.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+7
-3
@@ -84,6 +84,8 @@ import {
84
supportsMicrotasks,
85
errorHydratingContainer,
86
scheduleMicrotask,
87
+ prepareRendererToRender,
88
+ resetRendererAfterRender,
89
} from './ReactFiberHostConfig';
90
91
import {
@@ -1767,7 +1769,8 @@ function handleThrow(root, thrownValue): void {
1769
}
1770
}
1771
1770
-function pushDispatcher() {
1772
+function pushDispatcher(container) {
1773
+ prepareRendererToRender(container);
1774
const prevDispatcher = ReactCurrentDispatcher.current;
1775
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
1776
if (prevDispatcher === null) {
@@ -1781,6 +1784,7 @@ function pushDispatcher() {
1784
}
1785
1786
function popDispatcher(prevDispatcher) {
1787
+ resetRendererAfterRender();
1788
ReactCurrentDispatcher.current = prevDispatcher;
1789
}
1790
@@ -1850,7 +1854,7 @@ export function renderHasNotSuspendedYet(): boolean {
1854
function renderRootSync(root: FiberRoot, lanes: Lanes) {
1855
const prevExecutionContext = executionContext;
1856
executionContext |= RenderContext;
1853
- const prevDispatcher = pushDispatcher();
1857
+ const prevDispatcher = pushDispatcher(root.containerInfo);
1858
1859
// If the root or lanes have changed, throw out the existing stack
1860
// and prepare a fresh one. Otherwise we'll continue where we left off.
@@ -1950,7 +1954,7 @@ function workLoopSync() {
1954
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1955
const prevExecutionContext = executionContext;
1956
executionContext |= RenderContext;
1953
- const prevDispatcher = pushDispatcher();
1957
+ const prevDispatcher = pushDispatcher(root.containerInfo);
1958
1959
// If the root or lanes have changed, throw out the existing stack
1960
// and prepare a fresh one. Otherwise we'll continue where we left off.
packages/react-reconciler/src/ReactTestSelectors.js
+26
-9
@@ -10,7 +10,11 @@
10
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11
import type {Instance} from './ReactFiberHostConfig';
12
13
-import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
13
+import {
14
+ HostComponent,
15
+ HostResource,
16
+ HostText,
17
+} from 'react-reconciler/src/ReactWorkTags';
18
import getComponentNameFromType from 'shared/getComponentNameFromType';
19
import {
20
findFiberRoot,
@@ -150,7 +154,7 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
154
((selector: any): HasPseudoClassSelector).value,
155
);
156
case ROLE_TYPE:
153
- if (fiber.tag === HostComponent) {
157
+ if (fiber.tag === HostComponent || fiber.tag === HostResource) {
158
const node = fiber.stateNode;
159
if (
160
matchAccessibilityRole(node, ((selector: any): RoleSelector).value)
@@ -160,7 +164,11 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
164
}
165
break;
166
case TEXT_TYPE:
163
- if (fiber.tag === HostComponent || fiber.tag === HostText) {
167
+ if (
168
+ fiber.tag === HostComponent ||
169
+ fiber.tag === HostText ||
170
+ fiber.tag === HostResource
171
+ ) {
172
const textContent = getTextContent(fiber);
173
if (
174
textContent !== null &&
@@ -171,7 +179,7 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
179
}
180
break;
181
case TEST_NAME_TYPE:
174
- if (fiber.tag === HostComponent) {
182
+ if (fiber.tag === HostComponent || fiber.tag === HostResource) {
183
const dataTestID = fiber.memoizedProps['data-testname'];
184
if (
185
typeof dataTestID === 'string' &&
@@ -217,7 +225,10 @@ function findPaths(root: Fiber, selectors: Array<Selector>): Array<Fiber> {
225
let selectorIndex = ((stack[index++]: any): number);
226
let selector = selectors[selectorIndex];
227
220
- if (fiber.tag === HostComponent && isHiddenSubtree(fiber)) {
228
+ if (
229
+ (fiber.tag === HostComponent || fiber.tag === HostResource) &&
230
+ isHiddenSubtree(fiber)
231
+ ) {
232
continue;
233
} else {
234
while (selector != null && matchSelector(fiber, selector)) {
@@ -249,7 +260,10 @@ function hasMatchingPaths(root: Fiber, selectors: Array<Selector>): boolean {
260
let selectorIndex = ((stack[index++]: any): number);
261
let selector = selectors[selectorIndex];
262
252
- if (fiber.tag === HostComponent && isHiddenSubtree(fiber)) {
263
+ if (
264
+ (fiber.tag === HostComponent || fiber.tag === HostResource) &&
265
+ isHiddenSubtree(fiber)
266
+ ) {
267
continue;
268
} else {
269
while (selector != null && matchSelector(fiber, selector)) {
@@ -289,7 +303,7 @@ export function findAllNodes(
303
let index = 0;
304
while (index < stack.length) {
305
const node = ((stack[index++]: any): Fiber);
292
- if (node.tag === HostComponent) {
306
+ if (node.tag === HostComponent || node.tag === HostResource) {
307
if (isHiddenSubtree(node)) {
308
continue;
309
}
@@ -327,7 +341,10 @@ export function getFindAllNodesFailureDescription(
341
let selectorIndex = ((stack[index++]: any): number);
342
const selector = selectors[selectorIndex];
343
330
- if (fiber.tag === HostComponent && isHiddenSubtree(fiber)) {
344
+ if (
345
+ (fiber.tag === HostComponent || fiber.tag === HostResource) &&
346
+ isHiddenSubtree(fiber)
347
+ ) {
348
continue;
349
} else if (matchSelector(fiber, selector)) {
350
matchedNames.push(selectorToString(selector));
@@ -479,7 +496,7 @@ export function focusWithin(
496
if (isHiddenSubtree(fiber)) {
497
continue;
498
}
482
- if (fiber.tag === HostComponent) {
499
+ if (fiber.tag === HostComponent || fiber.tag === HostResource) {
500
const node = fiber.stateNode;
501
if (setFocusIfFocusable(node)) {
502
return true;
packages/react-reconciler/src/ReactWorkTags.js
+3
-1
@@ -33,7 +33,8 @@ export type WorkTag =
33
| 22
34
| 23
35
| 24
36
- | 25;
36
+ | 25
37
+ | 26;
38
39
export const FunctionComponent = 0;
40
export const ClassComponent = 1;
@@ -60,3 +61,4 @@ export const OffscreenComponent = 22;
61
export const LegacyHiddenComponent = 23;
62
export const CacheComponent = 24;
63
export const TracingMarkerComponent = 25;
64
+export const HostResource = 26;
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+4
@@ -66,6 +66,8 @@ describe('ReactFiberHostContext', () => {
66
getCurrentEventPriority: function() {
67
return DefaultEventPriority;
68
},
69
+ prepareRendererToRender: function() {},
70
+ resetRendererAfterRender: function() {},
71
supportsMutation: true,
72
requestPostPaintCallback: function() {},
73
});
@@ -131,6 +133,8 @@ describe('ReactFiberHostContext', () => {
133
return DefaultEventPriority;
134
},
135
requestPostPaintCallback: function() {},
136
+ prepareRendererToRender: function() {},
137
+ resetRendererAfterRender: function() {},
138
supportsMutation: true,
139
});
140
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+12
-3
@@ -68,6 +68,8 @@ export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope;
68
export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority;
69
export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance;
70
export const requestPostPaintCallback = $$$hostConfig.requestPostPaintCallback;
71
+export const prepareRendererToRender = $$$hostConfig.prepareRendererToRender;
72
+export const resetRendererAfterRender = $$$hostConfig.resetRendererAfterRender;
73
74
// -------------------
75
// Microtasks
@@ -187,6 +189,13 @@ export const didNotFindHydratableTextInstance =
189
export const didNotFindHydratableSuspenseInstance =
190
$$$hostConfig.didNotFindHydratableSuspenseInstance;
191
export const errorHydratingContainer = $$$hostConfig.errorHydratingContainer;
190
-export const isHydratableResource = $$$hostConfig.isHydratableResource;
191
-export const getMatchingResourceInstance =
192
- $$$hostConfig.getMatchingResourceInstance;
192
+
193
+// -------------------
194
+// Resources
195
+// (optional)
196
+// -------------------
197
+export const supportsResources = $$$hostConfig.supportsResources;
198
+export const isHostResourceType = $$$hostConfig.isHostResourceType;
199
+export const getResource = $$$hostConfig.getResource;
200
+export const acquireResource = $$$hostConfig.acquireResource;
201
+export const releaseResource = $$$hostConfig.releaseResource;
packages/react-reconciler/src/getComponentNameFromFiber.js
+2
@@ -19,6 +19,7 @@ import {
19
HostRoot,
20
HostPortal,
21
HostComponent,
22
+ HostResource,
23
HostText,
24
Fragment,
25
Mode,
@@ -77,6 +78,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
78
return getWrappedName(type, type.render, 'ForwardRef');
79
case Fragment:
80
return 'Fragment';
81
+ case HostResource:
82
case HostComponent:
83
// Host component type is the display name (e.g. "div", "View")
84
return type;
packages/react-server-dom-relay/package.json
+2
-1
@@ -11,6 +11,7 @@
11
"scheduler": "^0.11.0"
12
},
13
"peerDependencies": {
14
- "react": "^17.0.0"
14
+ "react": "^17.0.0",
15
+ "react-dom": "^17.0.0"
16
}
17
}
packages/react-server/src/ReactFizzServer.js
+112
-4
@@ -24,6 +24,8 @@ import type {
24
SuspenseBoundaryID,
25
ResponseState,
26
FormatContext,
27
+ Resources,
28
+ BoundaryResources,
29
} from './ReactServerFormatConfig';
30
import type {ContextSnapshot} from './ReactFizzNewContext';
31
import type {ComponentStackNode} from './ReactFizzComponentStack';
@@ -63,6 +65,15 @@ import {
65
UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
66
assignSuspenseBoundaryID,
67
getChildFormatContext,
68
+ writeInitialResources,
69
+ writeImmediateResources,
70
+ hoistResources,
71
+ hoistResourcesToRoot,
72
+ prepareToRender,
73
+ cleanupAfterRender,
74
+ setCurrentlyRenderingBoundaryResourcesTarget,
75
+ createResources,
76
+ createBoundaryResources,
77
} from './ReactServerFormatConfig';
78
import {
79
constructClassInstance,
@@ -147,6 +158,7 @@ type SuspenseBoundary = {
158
completedSegments: Array<Segment>, // completed but not yet flushed segments.
159
byteSize: number, // used to determine whether to inline children boundaries.
160
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
161
+ resources: BoundaryResources,
162
};
163
164
export type Task = {
@@ -199,9 +211,10 @@ export opaque type Request = {
211
nextSegmentId: number,
212
allPendingTasks: number, // when it reaches zero, we can close the connection.
213
pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.
214
+ resources: Resources,
215
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
216
abortableTasks: Set<Task>,
204
- pingedTasks: Array<Task>,
217
+ pingedTasks: Array<Task>, // High priority tasks that should be worked on first.
218
// Queues to flush in order of priority
219
clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.
220
completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
@@ -262,6 +275,7 @@ export function createRequest(
275
): Request {
276
const pingedTasks = [];
277
const abortSet: Set<Task> = new Set();
278
+ const resources: Resources = createResources();
279
const request = {
280
destination: null,
281
responseState,
@@ -274,6 +288,7 @@ export function createRequest(
288
nextSegmentId: 0,
289
allPendingTasks: 0,
290
pendingRootTasks: 0,
291
+ resources,
292
completedRootSegment: null,
293
abortableTasks: abortSet,
294
pingedTasks: pingedTasks,
@@ -337,6 +352,7 @@ function createSuspenseBoundary(
352
byteSize: 0,
353
fallbackAbortableTasks,
354
errorDigest: null,
355
+ resources: createBoundaryResources(),
356
};
357
}
358
@@ -562,6 +578,12 @@ function renderSuspenseBoundary(
578
// we're writing to. If something suspends, it'll spawn new suspended task with that context.
579
task.blockedBoundary = newBoundary;
580
task.blockedSegment = contentRootSegment;
581
+ if (enableFloat) {
582
+ setCurrentlyRenderingBoundaryResourcesTarget(
583
+ request.resources,
584
+ newBoundary.resources,
585
+ );
586
+ }
587
try {
588
// We use the safe form because we don't handle suspending here. Only error handling.
589
renderNode(request, task, content);
@@ -572,6 +594,11 @@ function renderSuspenseBoundary(
594
contentRootSegment.textEmbedded,
595
);
596
contentRootSegment.status = COMPLETED;
597
+ if (enableFloat) {
598
+ if (newBoundary.pendingTasks === 0) {
599
+ hoistCompletedBoundaryResources(request, newBoundary);
600
+ }
601
+ }
602
queueCompletedSegment(newBoundary, contentRootSegment);
603
if (newBoundary.pendingTasks === 0) {
604
// This must have been the last segment we were waiting on. This boundary is now complete.
@@ -592,6 +619,12 @@ function renderSuspenseBoundary(
619
// We don't need to schedule any task because we know the parent has written yet.
620
// We do need to fallthrough to create the fallback though.
621
} finally {
622
+ if (enableFloat) {
623
+ setCurrentlyRenderingBoundaryResourcesTarget(
624
+ request.resources,
625
+ parentBoundary ? parentBoundary.resources : null,
626
+ );
627
+ }
628
task.blockedBoundary = parentBoundary;
629
task.blockedSegment = parentSegment;
630
}
@@ -619,6 +652,19 @@ function renderSuspenseBoundary(
652
popComponentStackInDEV(task);
653
}
654
655
+function hoistCompletedBoundaryResources(
656
+ request: Request,
657
+ completedBoundary: SuspenseBoundary,
658
+): void {
659
+ if (request.completedRootSegment !== null || request.pendingRootTasks > 0) {
660
+ // The Shell has not flushed yet. we can hoist Resources for this boundary
661
+ // all the way to the Root.
662
+ hoistResourcesToRoot(request.resources, completedBoundary.resources);
663
+ }
664
+ // We don't hoist if the root already flushed because late resources will be hoisted
665
+ // as boundaries flush
666
+}
667
+
668
function renderBackupSuspenseBoundary(
669
request: Request,
670
task: Task,
@@ -644,6 +690,7 @@ function renderHostElement(
690
): void {
691
pushBuiltInComponentStackInDEV(task, type);
692
const segment = task.blockedSegment;
693
+
694
const children = pushStartInstance(
695
segment.chunks,
696
request.preamble,
@@ -651,10 +698,12 @@ function renderHostElement(
698
props,
699
request.responseState,
700
segment.formatContext,
701
+ segment.lastPushedText,
702
);
703
segment.lastPushedText = false;
704
const prevContext = segment.formatContext;
705
segment.formatContext = getChildFormatContext(prevContext, type, props);
706
+
707
// We use the non-destructive form because if something suspends, we still
708
// need to pop back up and finish this subtree of HTML.
709
renderNode(request, task, children);
@@ -1733,11 +1782,15 @@ function finishedTask(
1782
queueCompletedSegment(boundary, segment);
1783
}
1784
}
1785
+ if (enableFloat) {
1786
+ hoistCompletedBoundaryResources(request, boundary);
1787
+ }
1788
if (boundary.parentFlushed) {
1789
// The segment might be part of a segment that didn't flush yet, but if the boundary's
1790
// parent flushed, we need to schedule the boundary to be emitted.
1791
request.completedBoundaries.push(boundary);
1792
}
1793
+
1794
// We can now cancel any pending task on the fallback since we won't need to show it anymore.
1795
// This needs to happen after we read the parentFlushed flags because aborting can finish
1796
// work which can trigger user code, which can start flushing, which can change those flags.
@@ -1774,6 +1827,13 @@ function finishedTask(
1827
}
1828
1829
function retryTask(request: Request, task: Task): void {
1830
+ if (enableFloat) {
1831
+ const blockedBoundary = task.blockedBoundary;
1832
+ setCurrentlyRenderingBoundaryResourcesTarget(
1833
+ request.resources,
1834
+ blockedBoundary ? blockedBoundary.resources : null,
1835
+ );
1836
+ }
1837
const segment = task.blockedSegment;
1838
if (segment.status !== PENDING) {
1839
// We completed this by other means before we had a chance to retry it.
@@ -1825,6 +1885,9 @@ function retryTask(request: Request, task: Task): void {
1885
erroredTask(request, task.blockedBoundary, segment, x);
1886
}
1887
} finally {
1888
+ if (enableFloat) {
1889
+ setCurrentlyRenderingBoundaryResourcesTarget(request.resources, null);
1890
+ }
1891
if (__DEV__) {
1892
currentTaskInDEV = prevTaskInDEV;
1893
}
@@ -1838,6 +1901,7 @@ export function performWork(request: Request): void {
1901
const prevContext = getActiveContext();
1902
const prevDispatcher = ReactCurrentDispatcher.current;
1903
ReactCurrentDispatcher.current = Dispatcher;
1904
+ const previousHostDispatcher = prepareToRender(request.resources);
1905
let prevGetCurrentStackImpl;
1906
if (__DEV__) {
1907
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
@@ -1862,6 +1926,8 @@ export function performWork(request: Request): void {
1926
} finally {
1927
setCurrentResponseState(prevResponseState);
1928
ReactCurrentDispatcher.current = prevDispatcher;
1929
+ cleanupAfterRender(previousHostDispatcher);
1930
+
1931
if (__DEV__) {
1932
ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
1933
}
@@ -1900,6 +1966,7 @@ function flushSubtree(
1966
const chunks = segment.chunks;
1967
let chunkIdx = 0;
1968
const children = segment.children;
1969
+
1970
for (let childIdx = 0; childIdx < children.length; childIdx++) {
1971
const nextChild = children[childIdx];
1972
// Write all the chunks up until the next child.
@@ -1998,6 +2065,9 @@ function flushSegment(
2065
2066
return writeEndPendingSuspenseBoundary(destination, request.responseState);
2067
} else {
2068
+ if (enableFloat) {
2069
+ hoistResources(request.resources, boundary.resources);
2070
+ }
2071
// We can inline this boundary's content as a complete boundary.
2072
writeStartCompletedSuspenseBoundary(destination, request.responseState);
2073
@@ -2019,6 +2089,25 @@ function flushSegment(
2089
}
2090
}
2091
2092
+function flushInitialResources(
2093
+ destination: Destination,
2094
+ resources: Resources,
2095
+ responseState: ResponseState,
2096
+): void {
2097
+ writeInitialResources(destination, resources, responseState);
2098
+}
2099
+
2100
+function flushImmediateResources(
2101
+ destination: Destination,
2102
+ request: Request,
2103
+): void {
2104
+ writeImmediateResources(
2105
+ destination,
2106
+ request.resources,
2107
+ request.responseState,
2108
+ );
2109
+}
2110
+
2111
function flushClientRenderedBoundary(
2112
request: Request,
2113
destination: Destination,
@@ -2054,6 +2143,12 @@ function flushCompletedBoundary(
2143
destination: Destination,
2144
boundary: SuspenseBoundary,
2145
): boolean {
2146
+ if (enableFloat) {
2147
+ setCurrentlyRenderingBoundaryResourcesTarget(
2148
+ request.resources,
2149
+ boundary.resources,
2150
+ );
2151
+ }
2152
const completedSegments = boundary.completedSegments;
2153
let i = 0;
2154
for (; i < completedSegments.length; i++) {
@@ -2067,6 +2162,7 @@ function flushCompletedBoundary(
2162
request.responseState,
2163
boundary.id,
2164
boundary.rootSegmentID,
2165
+ boundary.resources,
2166
);
2167
}
2168
@@ -2075,6 +2171,12 @@ function flushPartialBoundary(
2171
destination: Destination,
2172
boundary: SuspenseBoundary,
2173
): boolean {
2174
+ if (enableFloat) {
2175
+ setCurrentlyRenderingBoundaryResourcesTarget(
2176
+ request.resources,
2177
+ boundary.resources,
2178
+ );
2179
+ }
2180
const completedSegments = boundary.completedSegments;
2181
let i = 0;
2182
for (; i < completedSegments.length; i++) {
@@ -2138,8 +2240,6 @@ function flushCompletedQueues(
2240
// that item fully and then yield. At that point we remove the already completed
2241
// items up until the point we completed them.
2242
2141
- // TODO: Emit preloading.
2142
-
2243
let i;
2244
const completedRootSegment = request.completedRootSegment;
2245
if (completedRootSegment !== null) {
@@ -2150,15 +2250,23 @@ function flushCompletedQueues(
2250
// we expect the preamble to be tiny and will ignore backpressure
2251
writeChunk(destination, preamble[i]);
2252
}
2253
+
2254
+ flushInitialResources(
2255
+ destination,
2256
+ request.resources,
2257
+ request.responseState,
2258
+ );
2259
}
2260
2261
flushSegment(request, destination, completedRootSegment);
2262
request.completedRootSegment = null;
2263
writeCompletedRoot(destination, request.responseState);
2264
} else {
2159
- // We haven't flushed the root yet so we don't need to check boundaries further down
2265
+ // We haven't flushed the root yet so we don't need to check any other branches further down
2266
return;
2267
}
2268
+ } else if (enableFloat) {
2269
+ flushImmediateResources(destination, request);
2270
}
2271
2272
// We emit client rendering instructions for already emitted boundaries first.
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+16
@@ -26,6 +26,8 @@
26
declare var $$$hostConfig: any;
27
export opaque type Destination = mixed; // eslint-disable-line no-undef
28
export opaque type ResponseState = mixed;
29
+export opaque type Resources = mixed;
30
+export opaque type BoundaryResources = mixed;
31
export opaque type FormatContext = mixed;
32
export opaque type SuspenseBoundaryID = mixed;
33
@@ -66,3 +68,17 @@ export const writeCompletedBoundaryInstruction =
68
$$$hostConfig.writeCompletedBoundaryInstruction;
69
export const writeClientRenderBoundaryInstruction =
70
$$$hostConfig.writeClientRenderBoundaryInstruction;
71
+export const prepareToRender = $$$hostConfig.prepareToRender;
72
+export const cleanupAfterRender = $$$hostConfig.cleanupAfterRender;
73
+
74
+// -------------------------
75
+// Resources
76
+// -------------------------
77
+export const writeInitialResources = $$$hostConfig.writeInitialResources;
78
+export const writeImmediateResources = $$$hostConfig.writeImmediateResources;
79
+export const hoistResources = $$$hostConfig.hoistResources;
80
+export const hoistResourcesToRoot = $$$hostConfig.hoistResourcesToRoot;
81
+export const createResources = $$$hostConfig.createResources;
82
+export const createBoundaryResources = $$$hostConfig.createBoundaryResources;
83
+export const setCurrentlyRenderingBoundaryResourcesTarget =
84
+ $$$hostConfig.setCurrentlyRenderingBoundaryResourcesTarget;
packages/react-test-renderer/src/ReactTestHostConfig.js
+9
@@ -46,6 +46,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
46
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
47
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
48
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
49
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
50
51
const NO_CONTEXT = {};
52
const UPDATE_SIGNAL = {};
@@ -321,3 +322,11 @@ export function logRecoverableError(error: mixed): void {
322
export function requestPostPaintCallback(callback: (time: number) => void) {
323
// noop
324
}
325
+
326
+export function prepareRendererToRender(container: Container): void {
327
+ // noop
328
+}
329
+
330
+export function resetRendererAfterRender(): void {
331
+ // noop
332
+}
packages/react-test-renderer/src/ReactTestRenderer.js
+3
-1
@@ -27,6 +27,7 @@ import {
27
FunctionComponent,
28
ClassComponent,
29
HostComponent,
30
+ HostResource,
31
HostPortal,
32
HostText,
33
HostRoot,
@@ -196,6 +197,7 @@ function toTree(node: ?Fiber) {
197
instance: null,
198
rendered: childrenToTree(node.child),
199
};
200
+ case HostResource:
201
case HostComponent: {
202
return {
203
nodeType: 'host',
@@ -302,7 +304,7 @@ class ReactTestInstance {
304
}
305
306
get instance() {
305
- if (this._fiber.tag === HostComponent) {
307
+ if (this._fiber.tag === HostComponent || this._fiber.tag === HostResource) {
308
return getPublicInstance(this._fiber.stateNode);
309
} else {
310
return this._fiber.stateNode;
scripts/error-codes/codes.json
+7
-1
@@ -426,5 +426,11 @@
426
"438": "An unsupported type was passed to use(): %s",
427
"439": "We didn't expect to see a forward reference. This is a bug in the React Server.",
428
"440": "A function wrapped in useEvent can't be called during rendering.",
429
- "441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error."
429
+ "441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.",
430
+ "442": "The current renderer does not support Resources. This error is likely caused by a bug in React. Please file an issue.",
431
+ "443": "acquireResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
432
+ "444": "getResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
433
+ "445": "\"currentResources\" was expected to exist. This is a bug in React.",
434
+ "446": "\"currentDocument\" was expected to exist. This is a bug in React.",
435
+ "447": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found."
436
}
scripts/rollup/bundles.js
+9
-9
@@ -275,7 +275,7 @@ const bundles = [
275
global: 'ReactDOMServer',
276
minifyWithProdErrorCodes: true,
277
wrapWithModuleBoundaries: false,
278
- externals: ['react'],
278
+ externals: ['react', 'react-dom'],
279
babel: opts =>
280
Object.assign({}, opts, {
281
plugins: opts.plugins.concat([
@@ -288,7 +288,7 @@ const bundles = [
288
moduleType: RENDERER,
289
entry: 'react-dom/src/server/ReactDOMLegacyServerNode.js',
290
name: 'react-dom-server-legacy.node',
291
- externals: ['react', 'stream'],
291
+ externals: ['react', 'stream', 'react-dom'],
292
minifyWithProdErrorCodes: false,
293
wrapWithModuleBoundaries: false,
294
babel: opts =>
@@ -308,7 +308,7 @@ const bundles = [
308
global: 'ReactDOMServer',
309
minifyWithProdErrorCodes: true,
310
wrapWithModuleBoundaries: false,
311
- externals: ['react'],
311
+ externals: ['react', 'react-dom'],
312
},
313
{
314
bundleTypes: [NODE_DEV, NODE_PROD],
@@ -318,7 +318,7 @@ const bundles = [
318
global: 'ReactDOMServer',
319
minifyWithProdErrorCodes: false,
320
wrapWithModuleBoundaries: false,
321
- externals: ['react', 'util'],
321
+ externals: ['react', 'util', 'react-dom'],
322
},
323
{
324
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
@@ -327,7 +327,7 @@ const bundles = [
327
global: 'ReactDOMServerStreaming',
328
minifyWithProdErrorCodes: false,
329
wrapWithModuleBoundaries: false,
330
- externals: ['react'],
330
+ externals: ['react', 'react-dom'],
331
},
332
333
/******* React DOM Fizz Static *******/
@@ -338,7 +338,7 @@ const bundles = [
338
global: 'ReactDOMStatic',
339
minifyWithProdErrorCodes: true,
340
wrapWithModuleBoundaries: false,
341
- externals: ['react'],
341
+ externals: ['react', 'react-dom'],
342
},
343
{
344
bundleTypes: __EXPERIMENTAL__ ? [NODE_DEV, NODE_PROD] : [],
@@ -348,7 +348,7 @@ const bundles = [
348
global: 'ReactDOMStatic',
349
minifyWithProdErrorCodes: false,
350
wrapWithModuleBoundaries: false,
351
- externals: ['react', 'util', 'stream'],
351
+ externals: ['react', 'util', 'stream', 'react-dom'],
352
},
353
354
/******* React Server DOM Webpack Writer *******/
@@ -359,7 +359,7 @@ const bundles = [
359
global: 'ReactServerDOMWriter',
360
minifyWithProdErrorCodes: false,
361
wrapWithModuleBoundaries: false,
362
- externals: ['react'],
362
+ externals: ['react', 'react-dom'],
363
},
364
{
365
bundleTypes: [NODE_DEV, NODE_PROD],
@@ -368,7 +368,7 @@ const bundles = [
368
global: 'ReactServerDOMWriter',
369
minifyWithProdErrorCodes: false,
370
wrapWithModuleBoundaries: false,
371
- externals: ['react', 'util'],
371
+ externals: ['react', 'util', 'react-dom'],
372
},
373
374
/******* React Server DOM Webpack Reader *******/
scripts/shared/inlinedHostConfigs.js
+6
-1
@@ -130,7 +130,12 @@ module.exports = [
130
'react-server-dom-relay/server',
131
'react-server-dom-relay/src/ReactDOMServerFB.js',
132
],
133
- paths: ['react-dom', 'react-dom-bindings', 'react-server-dom-relay'],
133
+ paths: [
134
+ 'react-dom',
135
+ 'react-dom-bindings',
136
+ 'react-server-dom-relay',
137
+ 'shared/ReactDOMSharedInternals',
138
+ ],
139
isFlowTyped: true,
140
isServerSupported: true,
141
},