@samitouri / QOS-React-2 / commits / 4494f2a86f

[Float] add support for scripts and other enhancements (#25480)

* float enhance!!! Support preinit as script Support resources from async scripts Support saving the precedence place when rendering the shell There was a significant change to the flushing order of resources which follows the general principal of... 1. stuff that blocks display 2. stuff that we know will be used 3. stuff that was explicitly preloaded As a consequence if you preinit a style now it won't automatically flush in the shell unless you actually depend on it in your tree. To avoid races with precedence order we now emit a tag that saves the place amongst the precedence hierarchy so late insertions still end up where they were intended There is also a novel hydration pathway for certain tags. If you render an async script with an onLoad or onError it will always treat it like an insertion rather than a hydration. * restore preinit style flushing behavior and nits

Josh Story committed Oct 17, 2022 at 14:00 UTC 4494f2a86f4bfb94e0e78feef2dda72d93b6aa9e
15 files changed +1397 -495
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+16 -7
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {FloatRoot, StyleResource} from './ReactDOMFloatClient';
10 +import type {
11 + FloatRoot,
12 + StyleResource,
13 + ScriptResource,
14 +} from './ReactDOMFloatClient';
15 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
16 import type {ReactScopeInstance} from 'shared/ReactTypes';
17 import type {
@@ -48,7 +52,7 @@ const internalContainerInstanceKey = '__reactContainer$' + randomKey;
52 const internalEventHandlersKey = '__reactEvents$' + randomKey;
53 const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
54 const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
51 -const internalRootNodeStylesSetKey = '__reactStyles$' + randomKey;
55 +const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
56
57 export function detachDeletedInstance(node: Instance): void {
58 // TODO: This function is only called on host components. I don't think all of
@@ -278,10 +282,15 @@ export function doesTargetHaveEventHandle(
282 return eventHandles.has(eventHandle);
283 }
284
281 -export function getStylesFromRoot(root: FloatRoot): Map<string, StyleResource> {
282 - let styles = (root: any)[internalRootNodeStylesSetKey];
283 - if (!styles) {
284 - styles = (root: any)[internalRootNodeStylesSetKey] = new Map();
285 +export function getResourcesFromRoot(
286 + root: FloatRoot,
287 +): {styles: Map<string, StyleResource>, scripts: Map<string, ScriptResource>} {
288 + let resources = (root: any)[internalRootNodeResourcesKey];
289 + if (!resources) {
290 + resources = (root: any)[internalRootNodeResourcesKey] = {
291 + styles: new Map(),
292 + scripts: new Map(),
293 + };
294 }
286 - return styles;
295 + return resources;
296 }
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+235 -46
@@ -15,15 +15,16 @@ import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
15 import {
16 validateUnmatchedLinkResourceProps,
17 validatePreloadResourceDifference,
18 - validateHrefKeyedUpdatedProps,
18 + validateURLKeyedUpdatedProps,
19 validateStyleResourceDifference,
20 + validateScriptResourceDifference,
21 validateLinkPropsForStyleResource,
22 validateLinkPropsForPreloadResource,
23 validatePreloadArguments,
24 validatePreinitArguments,
25 } from '../shared/ReactDOMResourceValidation';
26 import {createElement, setInitialProperties} from './ReactDOMComponent';
26 -import {getStylesFromRoot} from './ReactDOMComponentTree';
27 +import {getResourcesFromRoot} from './ReactDOMComponentTree';
28 import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
29 import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
30
@@ -33,7 +34,6 @@ type ResourceType = 'style' | 'font' | 'script';
34
35 type PreloadProps = {
36 rel: 'preload',
36 - as: ResourceType,
37 href: string,
38 [string]: mixed,
39 };
@@ -48,7 +48,7 @@ type PreloadResource = {
48 type StyleProps = {
49 rel: 'stylesheet',
50 href: string,
51 - 'data-rprec': string,
51 + 'data-precedence': string,
52 [string]: mixed,
53 };
54 export type StyleResource = {
@@ -72,10 +72,22 @@ export type StyleResource = {
72 instance: ?Element,
73 root: FloatRoot,
74 };
75 +type ScriptProps = {
76 + src: string,
77 + [string]: mixed,
78 +};
79 +export type ScriptResource = {
80 + type: 'script',
81 + src: string,
82 + props: ScriptProps,
83 +
84 + instance: ?Element,
85 + root: FloatRoot,
86 +};
87
88 type Props = {[string]: mixed};
89
78 -type Resource = StyleResource | PreloadResource;
90 +type Resource = StyleResource | ScriptResource | PreloadResource;
91
92 // Brief on purpose due to insertion by script when streaming late boundaries
93 // s = Status
@@ -202,11 +214,12 @@ function preloadPropsFromPreloadOptions(
214 // ReactDOM.preinit
215 // --------------------------------------
216
205 -type PreinitAs = 'style';
217 +type PreinitAs = 'style' | 'script';
218 type PreinitOptions = {
219 as: PreinitAs,
208 - crossOrigin?: string,
220 precedence?: string,
221 + crossOrigin?: string,
222 + integrity?: string,
223 };
224 function preinit(href: string, options: PreinitOptions) {
225 if (__DEV__) {
@@ -243,7 +256,7 @@ function preinit(href: string, options: PreinitOptions) {
256
257 switch (as) {
258 case 'style': {
246 - const styleResources = getStylesFromRoot(resourceRoot);
259 + const styleResources = getResourcesFromRoot(resourceRoot).styles;
260 const precedence = options.precedence || 'default';
261 let resource = styleResources.get(href);
262 if (resource) {
@@ -270,6 +283,28 @@ function preinit(href: string, options: PreinitOptions) {
283 );
284 }
285 acquireResource(resource);
286 + return;
287 + }
288 + case 'script': {
289 + const src = href;
290 + const scriptResources = getResourcesFromRoot(resourceRoot).scripts;
291 + let resource = scriptResources.get(src);
292 + if (resource) {
293 + if (__DEV__) {
294 + const latestProps = scriptPropsFromPreinitOptions(src, options);
295 + validateScriptResourceDifference(resource.props, latestProps);
296 + }
297 + } else {
298 + const resourceProps = scriptPropsFromPreinitOptions(src, options);
299 + resource = createScriptResource(
300 + scriptResources,
301 + resourceRoot,
302 + src,
303 + resourceProps,
304 + );
305 + }
306 + acquireResource(resource);
307 + return;
308 }
309 }
310 }
@@ -285,6 +320,7 @@ function preloadPropsFromPreinitOptions(
320 rel: 'preload',
321 as,
322 crossOrigin: as === 'font' ? '' : options.crossOrigin,
323 + integrity: options.integrity,
324 };
325 }
326
@@ -296,8 +332,20 @@ function stylePropsFromPreinitOptions(
332 return {
333 rel: 'stylesheet',
334 href,
299 - 'data-rprec': precedence,
335 + 'data-precedence': precedence,
336 + crossOrigin: options.crossOrigin,
337 + };
338 +}
339 +
340 +function scriptPropsFromPreinitOptions(
341 + src: string,
342 + options: PreinitOptions,
343 +): ScriptProps {
344 + return {
345 + src,
346 + async: true,
347 crossOrigin: options.crossOrigin,
348 + integrity: options.integrity,
349 };
350 }
351
@@ -314,7 +362,11 @@ type StyleQualifyingProps = {
362 type PreloadQualifyingProps = {
363 rel: 'preload',
364 href: string,
317 - as: ResourceType,
365 + [string]: mixed,
366 +};
367 +type ScriptQualifyingProps = {
368 + src: string,
369 + async: true,
370 [string]: mixed,
371 };
372
@@ -335,13 +387,15 @@ export function getResource(
387 const {rel} = pendingProps;
388 switch (rel) {
389 case 'stylesheet': {
338 - const styleResources = getStylesFromRoot(resourceRoot);
390 + const styleResources = getResourcesFromRoot(resourceRoot).styles;
391 let didWarn;
392 if (__DEV__) {
393 if (currentProps) {
342 - didWarn = validateHrefKeyedUpdatedProps(
394 + didWarn = validateURLKeyedUpdatedProps(
395 pendingProps,
396 currentProps,
397 + 'style',
398 + 'href',
399 );
400 }
401 if (!didWarn) {
@@ -360,7 +414,7 @@ export function getResource(
414 if (!didWarn) {
415 const latestProps = stylePropsFromRawProps(styleRawProps);
416 if ((resource: any)._dev_preload_props) {
363 - adoptPreloadProps(
417 + adoptPreloadPropsForStyle(
418 latestProps,
419 (resource: any)._dev_preload_props,
420 );
@@ -387,8 +441,8 @@ export function getResource(
441 if (__DEV__) {
442 validateLinkPropsForPreloadResource(pendingProps);
443 }
390 - const {href, as} = pendingProps;
391 - if (typeof href === 'string' && isResourceAsType(as)) {
444 + const {href} = pendingProps;
445 + if (typeof href === 'string') {
446 // We've asserted all the specific types for PreloadQualifyingProps
447 const preloadRawProps: PreloadQualifyingProps = (pendingProps: any);
448 let resource = preloadResources.get(href);
@@ -424,6 +478,49 @@ export function getResource(
478 }
479 }
480 }
481 + case 'script': {
482 + const scriptResources = getResourcesFromRoot(resourceRoot).scripts;
483 + let didWarn;
484 + if (__DEV__) {
485 + if (currentProps) {
486 + didWarn = validateURLKeyedUpdatedProps(
487 + pendingProps,
488 + currentProps,
489 + 'script',
490 + 'src',
491 + );
492 + }
493 + }
494 + const {src, async} = pendingProps;
495 + if (async && typeof src === 'string') {
496 + const scriptRawProps: ScriptQualifyingProps = (pendingProps: any);
497 + let resource = scriptResources.get(src);
498 + if (resource) {
499 + if (__DEV__) {
500 + if (!didWarn) {
501 + const latestProps = scriptPropsFromRawProps(scriptRawProps);
502 + if ((resource: any)._dev_preload_props) {
503 + adoptPreloadPropsForScript(
504 + latestProps,
505 + (resource: any)._dev_preload_props,
506 + );
507 + }
508 + validateScriptResourceDifference(resource.props, latestProps);
509 + }
510 + }
511 + } else {
512 + const resourceProps = scriptPropsFromRawProps(scriptRawProps);
513 + resource = createScriptResource(
514 + scriptResources,
515 + resourceRoot,
516 + src,
517 + resourceProps,
518 + );
519 + }
520 + return resource;
521 + }
522 + return null;
523 + }
524 default: {
525 throw new Error(
526 `getResource encountered a resource type it did not expect: "${type}". this is a bug in React.`,
@@ -440,12 +537,17 @@ function preloadPropsFromRawProps(
537
538 function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
539 const props: StyleProps = Object.assign({}, rawProps);
443 - props['data-rprec'] = rawProps.precedence;
540 + props['data-precedence'] = rawProps.precedence;
541 props.precedence = null;
542
543 return props;
544 }
545
546 +function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
547 + const props: ScriptProps = Object.assign({}, rawProps);
548 + return props;
549 +}
550 +
551 // --------------------------------------
552 // Resource Reconciliation
553 // --------------------------------------
@@ -455,6 +557,9 @@ export function acquireResource(resource: Resource): Instance {
557 case 'style': {
558 return acquireStyleResource(resource);
559 }
560 + case 'script': {
561 + return acquireScriptResource(resource);
562 + }
563 case 'preload': {
564 return resource.instance;
565 }
@@ -558,7 +663,7 @@ function createStyleResource(
663 // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
664 // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
665 const preloadProps = hint.props;
561 - adoptPreloadProps(resource.props, hint.props);
666 + adoptPreloadPropsForStyle(resource.props, hint.props);
667 if (__DEV__) {
668 (resource: any)._dev_preload_props = preloadProps;
669 }
@@ -568,7 +673,7 @@ function createStyleResource(
673 return resource;
674 }
675
571 -function adoptPreloadProps(
676 +function adoptPreloadPropsForStyle(
677 styleProps: StyleProps,
678 preloadProps: PreloadProps,
679 ): void {
@@ -576,7 +681,6 @@ function adoptPreloadProps(
681 styleProps.crossOrigin = preloadProps.crossOrigin;
682 if (styleProps.referrerPolicy == null)
683 styleProps.referrerPolicy = preloadProps.referrerPolicy;
579 - if (styleProps.media == null) styleProps.media = preloadProps.media;
684 if (styleProps.title == null) styleProps.title = preloadProps.title;
685 }
686
@@ -610,6 +714,63 @@ function preloadPropsFromStyleProps(props: StyleProps): PreloadProps {
714 };
715 }
716
717 +function createScriptResource(
718 + scriptResources: Map<string, ScriptResource>,
719 + root: FloatRoot,
720 + src: string,
721 + props: ScriptProps,
722 +): ScriptResource {
723 + if (__DEV__) {
724 + if (scriptResources.has(src)) {
725 + console.error(
726 + 'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
727 + );
728 + }
729 + }
730 +
731 + const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src);
732 + const existingEl = root.querySelector(
733 + `script[async][src="${limitedEscapedSrc}"]`,
734 + );
735 + const resource = {
736 + type: 'script',
737 + src,
738 + props,
739 + root,
740 + instance: existingEl || null,
741 + };
742 + scriptResources.set(src, resource);
743 +
744 + if (!existingEl) {
745 + const hint = preloadResources.get(src);
746 + if (hint) {
747 + // If a preload for this style Resource already exists there are certain props we want to adopt
748 + // on the style Resource, primarily focussed on making sure the style network pathways utilize
749 + // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
750 + // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
751 + const preloadProps = hint.props;
752 + adoptPreloadPropsForScript(props, hint.props);
753 + if (__DEV__) {
754 + (resource: any)._dev_preload_props = preloadProps;
755 + }
756 + }
757 + }
758 +
759 + return resource;
760 +}
761 +
762 +function adoptPreloadPropsForScript(
763 + scriptProps: ScriptProps,
764 + preloadProps: PreloadProps,
765 +): void {
766 + if (scriptProps.crossOrigin == null)
767 + scriptProps.crossOrigin = preloadProps.crossOrigin;
768 + if (scriptProps.referrerPolicy == null)
769 + scriptProps.referrerPolicy = preloadProps.referrerPolicy;
770 + if (scriptProps.integrity == null)
771 + scriptProps.referrerPolicy = preloadProps.integrity;
772 +}
773 +
774 function createPreloadResource(
775 ownerDocument: Document,
776 href: string,
@@ -623,7 +784,7 @@ function createPreloadResource(
784 );
785 if (!element) {
786 element = createResourceInstance('link', props, ownerDocument);
626 - insertPreloadInstance(element, ownerDocument);
787 + insertResourceInstance(element, ownerDocument);
788 }
789 return {
790 type: 'preload',
@@ -641,7 +802,7 @@ function acquireStyleResource(resource: StyleResource): Instance {
802 props.href,
803 );
804 const existingEl = root.querySelector(
644 - `link[rel="stylesheet"][data-rprec][href="${limitedEscapedHref}"]`,
805 + `link[rel="stylesheet"][data-precedence][href="${limitedEscapedHref}"]`,
806 );
807 if (existingEl) {
808 resource.instance = existingEl;
@@ -685,6 +846,31 @@ function acquireStyleResource(resource: StyleResource): Instance {
846 return resource.instance;
847 }
848
849 +function acquireScriptResource(resource: ScriptResource): Instance {
850 + if (!resource.instance) {
851 + const {props, root} = resource;
852 + const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(
853 + props.src,
854 + );
855 + const existingEl = root.querySelector(
856 + `script[async][src="${limitedEscapedSrc}"]`,
857 + );
858 + if (existingEl) {
859 + resource.instance = existingEl;
860 + } else {
861 + const instance = createResourceInstance(
862 + 'script',
863 + resource.props,
864 + getDocumentFromRoot(root),
865 + );
866 +
867 + insertResourceInstance(instance, getDocumentFromRoot(root));
868 + resource.instance = instance;
869 + }
870 + }
871 + return resource.instance;
872 +}
873 +
874 function attachLoadListeners(instance: Instance, resource: StyleResource) {
875 const listeners = {};
876 listeners.load = onResourceLoad.bind(
@@ -749,12 +935,14 @@ function insertStyleInstance(
935 precedence: string,
936 root: FloatRoot,
937 ): void {
752 - const nodes = root.querySelectorAll('link[rel="stylesheet"][data-rprec]');
938 + const nodes = root.querySelectorAll(
939 + 'link[rel="stylesheet"][data-precedence]',
940 + );
941 const last = nodes.length ? nodes[nodes.length - 1] : null;
942 let prior = last;
943 for (let i = 0; i < nodes.length; i++) {
944 const node = nodes[i];
757 - const nodePrecedence = node.dataset.rprec;
945 + const nodePrecedence = node.dataset.precedence;
946 if (nodePrecedence === precedence) {
947 prior = node;
948 } else if (prior !== last) {
@@ -780,21 +968,27 @@ function insertStyleInstance(
968 }
969 }
970
783 -function insertPreloadInstance(
971 +function insertResourceInstance(
972 instance: Instance,
973 ownerDocument: Document,
974 ): void {
787 - if (!ownerDocument.contains(instance)) {
788 - const parent = ownerDocument.head;
789 - if (parent) {
790 - parent.appendChild(instance);
791 - } else {
792 - throw new Error(
793 - 'While attempting to insert a Resource, React expected the Document to contain' +
794 - ' a head element but it was not found.',
975 + if (__DEV__) {
976 + if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
977 + console.error(
978 + 'insertResourceInstance was called with a stylesheet. Stylesheets must be' +
979 + ' inserted with insertStyleInstance instead. This is a bug in React.',
980 );
981 }
982 }
983 + const parent = ownerDocument.head;
984 + if (parent) {
985 + parent.appendChild(instance);
986 + } else {
987 + throw new Error(
988 + 'While attempting to insert a Resource, React expected the Document to contain' +
989 + ' a head element but it was not found.',
990 + );
991 + }
992 }
993
994 export function isHostResourceType(type: string, props: Props): boolean {
@@ -815,27 +1009,22 @@ export function isHostResourceType(type: string, props: Props): boolean {
1009 );
1010 }
1011 case 'preload': {
818 - if (__DEV__) {
819 - validateLinkPropsForStyleResource(props);
820 - }
821 - const {href, as, onLoad, onError} = props;
822 - return (
823 - !onLoad &&
824 - !onError &&
825 - typeof href === 'string' &&
826 - isResourceAsType(as)
827 - );
1012 + const {href, onLoad, onError} = props;
1013 + return !onLoad && !onError && typeof href === 'string';
1014 }
1015 }
1016 + return false;
1017 + }
1018 + case 'script': {
1019 + // We don't validate because it is valid to use async with onLoad/onError unlike combining
1020 + // precedence with these for style resources
1021 + const {src, async, onLoad, onError} = props;
1022 + return (async: any) && typeof src === 'string' && !onLoad && !onError;
1023 }
1024 }
1025 return false;
1026 }
1027
835 -function isResourceAsType(as: mixed): boolean {
836 - return as === 'style' || as === 'font' || as === 'script';
837 -}
838 -
1028 // When passing user input into querySelector(All) the embedded string must not alter
1029 // the semantics of the query. This escape function is safe to use when we know the
1030 // provided value is going to be wrapped in double quotes as part of an attribute selector
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+54 -9
@@ -794,6 +794,20 @@ export function bindInstance(
794
795 export const supportsHydration = true;
796
797 +// With Resources, some HostComponent types will never be server rendered and need to be
798 +// inserted without breaking hydration
799 +export function isHydratable(type: string, props: Props): boolean {
800 + if (enableFloat) {
801 + if (type === 'script') {
802 + const {async, onLoad, onError} = (props: any);
803 + return !(async && (onLoad || onError));
804 + }
805 + return true;
806 + } else {
807 + return true;
808 + }
809 +}
810 +
811 export function canHydrateInstance(
812 instance: HydratableInstance,
813 type: string,
@@ -889,12 +903,26 @@ function getNextHydratable(node) {
903 const rel = linkEl.rel;
904 if (
905 rel === 'preload' ||
892 - (rel === 'stylesheet' && linkEl.hasAttribute('data-rprec'))
906 + (rel === 'stylesheet' && linkEl.hasAttribute('data-precedence'))
907 ) {
908 continue;
909 }
910 break;
911 }
912 + case 'STYLE': {
913 + const styleEl: HTMLStyleElement = (element: any);
914 + if (styleEl.hasAttribute('data-precedence')) {
915 + continue;
916 + }
917 + break;
918 + }
919 + case 'SCRIPT': {
920 + const scriptEl: HTMLScriptElement = (element: any);
921 + if (scriptEl.hasAttribute('async')) {
922 + continue;
923 + }
924 + break;
925 + }
926 case 'HTML':
927 case 'HEAD':
928 case 'BODY': {
@@ -908,14 +936,31 @@ function getNextHydratable(node) {
936 } else if (enableFloat) {
937 if (nodeType === ELEMENT_NODE) {
938 const element: Element = (node: any);
911 - if (element.tagName === 'LINK') {
912 - const linkEl: HTMLLinkElement = (element: any);
913 - const rel = linkEl.rel;
914 - if (
915 - rel === 'preload' ||
916 - (rel === 'stylesheet' && linkEl.hasAttribute('data-rprec'))
917 - ) {
918 - continue;
939 + switch (element.tagName) {
940 + case 'LINK': {
941 + const linkEl: HTMLLinkElement = (element: any);
942 + const rel = linkEl.rel;
943 + if (
944 + rel === 'preload' ||
945 + (rel === 'stylesheet' && linkEl.hasAttribute('data-precedence'))
946 + ) {
947 + continue;
948 + }
949 + break;
950 + }
951 + case 'STYLE': {
952 + const styleEl: HTMLStyleElement = (element: any);
953 + if (styleEl.hasAttribute('data-precedence')) {
954 + continue;
955 + }
956 + break;
957 + }
958 + case 'SCRIPT': {
959 + const scriptEl: HTMLScriptElement = (element: any);
960 + if (scriptEl.hasAttribute('async')) {
961 + continue;
962 + }
963 + break;
964 }
965 }
966 break;
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+278 -103
@@ -11,6 +11,8 @@ import {
11 validatePreloadResourceDifference,
12 validateStyleResourceDifference,
13 validateStyleAndHintProps,
14 + validateScriptResourceDifference,
15 + validateScriptAndHintProps,
16 validateLinkPropsForStyleResource,
17 validateLinkPropsForPreloadResource,
18 validatePreloadArguments,
@@ -38,7 +40,7 @@ type PreloadResource = {
40 type StyleProps = {
41 rel: 'stylesheet',
42 href: string,
41 - 'data-rprec': string,
43 + 'data-precedence': string,
44 [string]: mixed,
45 };
46 type StyleResource = {
@@ -50,22 +52,44 @@ type StyleResource = {
52 flushed: boolean,
53 inShell: boolean, // flushedInShell
54 hint: PreloadResource,
55 + set: Set<StyleResource>, // the precedence set this resource should be flushed in
56 };
57
55 -export type Resource = PreloadResource | StyleResource;
58 +type ScriptProps = {
59 + src: string,
60 + [string]: mixed,
61 +};
62 +type ScriptResource = {
63 + type: 'script',
64 + src: string,
65 + props: ScriptProps,
66 +
67 + flushed: boolean,
68 + hint: PreloadResource,
69 +};
70 +
71 +export type Resource = PreloadResource | StyleResource | ScriptResource;
72
73 export type Resources = {
74 // Request local cache
75 preloadsMap: Map<string, PreloadResource>,
76 stylesMap: Map<string, StyleResource>,
77 + scriptsMap: Map<string, ScriptResource>,
78
79 // Flushing queues for Resource dependencies
63 - explicitPreloads: Set<PreloadResource>,
64 - implicitPreloads: Set<PreloadResource>,
80 + fontPreloads: Set<PreloadResource>,
81 + // usedImagePreloads: Set<PreloadResource>,
82 precedences: Map<string, Set<StyleResource>>,
83 + usedStylePreloads: Set<PreloadResource>,
84 + scripts: Set<ScriptResource>,
85 + usedScriptPreloads: Set<PreloadResource>,
86 + explicitStylePreloads: Set<PreloadResource>,
87 + // explicitImagePreloads: Set<PreloadResource>,
88 + explicitScriptPreloads: Set<PreloadResource>,
89
90 // Module-global-like reference for current boundary resources
91 boundaryResources: ?BoundaryResources,
92 + ...
93 };
94
95 // @TODO add bootstrap script to implicit preloads
@@ -74,11 +98,18 @@ export function createResources(): Resources {
98 // persistent
99 preloadsMap: new Map(),
100 stylesMap: new Map(),
101 + scriptsMap: new Map(),
102
103 // cleared on flush
79 - explicitPreloads: new Set(),
80 - implicitPreloads: new Set(),
104 + fontPreloads: new Set(),
105 + // usedImagePreloads: new Set(),
106 precedences: new Map(),
107 + usedStylePreloads: new Set(),
108 + scripts: new Set(),
109 + usedScriptPreloads: new Set(),
110 + explicitStylePreloads: new Set(),
111 + // explicitImagePreloads: new Set(),
112 + explicitScriptPreloads: new Set(),
113
114 // like a module global for currently rendering boundary
115 boundaryResources: null,
@@ -91,13 +122,6 @@ export function createBoundaryResources(): BoundaryResources {
122 return new Set();
123 }
124
94 -export function mergeBoundaryResources(
95 - target: BoundaryResources,
96 - source: BoundaryResources,
97 -) {
98 - source.forEach(resource => target.add(resource));
99 -}
100 -
125 let currentResources: null | Resources = null;
126 const currentResourcesStack = [];
127
@@ -134,6 +158,7 @@ function preload(href: string, options: PreloadOptions) {
158 // simply return and do not warn.
159 return;
160 }
161 + const resources = currentResources;
162 if (__DEV__) {
163 validatePreloadArguments(href, options);
164 }
@@ -144,8 +169,7 @@ function preload(href: string, options: PreloadOptions) {
169 options !== null
170 ) {
171 const as = options.as;
147 - // $FlowFixMe[incompatible-use] found when upgrading Flow
148 - let resource = currentResources.preloadsMap.get(href);
172 + let resource = resources.preloadsMap.get(href);
173 if (resource) {
174 if (__DEV__) {
175 const originallyImplicit =
@@ -160,23 +184,35 @@ function preload(href: string, options: PreloadOptions) {
184 }
185 } else {
186 resource = createPreloadResource(
163 - // $FlowFixMe[incompatible-call] found when upgrading Flow
164 - currentResources,
187 + resources,
188 href,
189 as,
190 preloadPropsFromPreloadOptions(href, as, options),
191 );
192 }
170 - // $FlowFixMe[incompatible-call] found when upgrading Flow
171 - captureExplicitPreloadResourceDependency(currentResources, resource);
193 + switch (as) {
194 + case 'font': {
195 + resources.fontPreloads.add(resource);
196 + break;
197 + }
198 + case 'style': {
199 + resources.explicitStylePreloads.add(resource);
200 + break;
201 + }
202 + case 'script': {
203 + resources.explicitScriptPreloads.add(resource);
204 + break;
205 + }
206 + }
207 }
208 }
209
175 -type PreinitAs = 'style';
210 +type PreinitAs = 'style' | 'script';
211 type PreinitOptions = {
212 as: PreinitAs,
213 precedence?: string,
214 crossOrigin?: string,
215 + integrity?: string,
216 };
217 function preinit(href: string, options: PreinitOptions) {
218 if (!currentResources) {
@@ -188,6 +224,7 @@ function preinit(href: string, options: PreinitOptions) {
224 // simply return and do not warn.
225 return;
226 }
227 + const resources = currentResources;
228 if (__DEV__) {
229 validatePreinitArguments(href, options);
230 }
@@ -200,38 +237,48 @@ function preinit(href: string, options: PreinitOptions) {
237 const as = options.as;
238 switch (as) {
239 case 'style': {
203 - const precedence = options.precedence || 'default';
204 -
205 - // $FlowFixMe[incompatible-use] found when upgrading Flow
206 - let resource = currentResources.stylesMap.get(href);
240 + let resource = resources.stylesMap.get(href);
241 if (resource) {
242 if (__DEV__) {
243 const latestProps = stylePropsFromPreinitOptions(
244 href,
211 - precedence,
245 + resource.precedence,
246 options,
247 );
248 validateStyleResourceDifference(resource.props, latestProps);
249 }
250 } else {
251 + const precedence = options.precedence || 'default';
252 const resourceProps = stylePropsFromPreinitOptions(
253 href,
254 precedence,
255 options,
256 );
257 resource = createStyleResource(
223 - // $FlowFixMe[incompatible-call] found when upgrading Flow
224 - currentResources,
258 + resources,
259 href,
260 precedence,
261 resourceProps,
262 );
263 }
264 + resource.set.add(resource);
265 + resources.explicitStylePreloads.add(resource.hint);
266
231 - // Do not associate preinit style resources with any specific boundary regardless of where it is called
232 - // $FlowFixMe[incompatible-call] found when upgrading Flow
233 - captureStyleResourceDependency(currentResources, null, resource);
234 -
267 + return;
268 + }
269 + case 'script': {
270 + const src = href;
271 + let resource = resources.scriptsMap.get(src);
272 + if (resource) {
273 + if (__DEV__) {
274 + const latestProps = scriptPropsFromPreinitOptions(src, options);
275 + validateScriptResourceDifference(resource.props, latestProps);
276 + }
277 + } else {
278 + const scriptProps = scriptPropsFromPreinitOptions(src, options);
279 + resource = createScriptResource(resources, src, scriptProps);
280 + resources.scripts.add(resource);
281 + }
282 return;
283 }
284 }
@@ -286,6 +333,20 @@ function preloadAsStylePropsFromProps(
333 };
334 }
335
336 +function preloadAsScriptPropsFromProps(
337 + href: string,
338 + props: Props | ScriptProps,
339 +): PreloadProps {
340 + return {
341 + rel: 'preload',
342 + as: 'script',
343 + href,
344 + crossOrigin: props.crossOrigin,
345 + integrity: props.integrity,
346 + referrerPolicy: props.referrerPolicy,
347 + };
348 +}
349 +
350 function createPreloadResource(
351 resources: Resources,
352 href: string,
@@ -320,7 +381,7 @@ function stylePropsFromRawProps(
381 const props: StyleProps = Object.assign({}, rawProps);
382 props.href = href;
383 props.rel = 'stylesheet';
323 - props['data-rprec'] = precedence;
384 + props['data-precedence'] = precedence;
385 delete props.precedence;
386
387 return props;
@@ -334,7 +395,7 @@ function stylePropsFromPreinitOptions(
395 return {
396 rel: 'stylesheet',
397 href,
337 - 'data-rprec': precedence,
398 + 'data-precedence': precedence,
399 crossOrigin: options.crossOrigin,
400 };
401 }
@@ -352,7 +413,15 @@ function createStyleResource(
413 );
414 }
415 }
355 - const {stylesMap, preloadsMap} = resources;
416 + const {stylesMap, preloadsMap, precedences} = resources;
417 +
418 + // If this is the first time we've seen this precedence we encode it's position in our set even though
419 + // we don't add the resource to this set yet
420 + let precedenceSet = precedences.get(precedence);
421 + if (!precedenceSet) {
422 + precedenceSet = new Set();
423 + precedences.set(precedence, precedenceSet);
424 + }
425
426 let hint = preloadsMap.get(href);
427 if (hint) {
@@ -360,16 +429,11 @@ function createStyleResource(
429 // on the style Resource, primarily focussed on making sure the style network pathways utilize
430 // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
431 // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
363 - const preloadProps = hint.props;
364 - if (props.crossOrigin == null) props.crossOrigin = preloadProps.crossOrigin;
365 - if (props.referrerPolicy == null)
366 - props.referrerPolicy = preloadProps.referrerPolicy;
367 - if (props.media == null) props.media = preloadProps.media;
368 - if (props.title == null) props.title = preloadProps.title;
432 + adoptPreloadPropsForStyleProps(props, hint.props);
433
434 if (__DEV__) {
435 validateStyleAndHintProps(
372 - preloadProps,
436 + hint.props,
437 props,
438 (hint: any)._dev_implicit_construction,
439 );
@@ -385,7 +449,7 @@ function createStyleResource(
449 if (__DEV__) {
450 (hint: any)._dev_implicit_construction = true;
451 }
388 - captureImplicitPreloadResourceDependency(resources, hint);
452 + resources.explicitStylePreloads.add(hint);
453 }
454
455 const resource = {
@@ -396,47 +460,107 @@ function createStyleResource(
460 inShell: false,
461 props,
462 hint,
463 + set: precedenceSet,
464 };
465 stylesMap.set(href, resource);
466
467 return resource;
468 }
469
405 -function captureStyleResourceDependency(
406 - resources: Resources,
407 - boundaryResources: ?BoundaryResources,
408 - styleResource: StyleResource,
470 +function adoptPreloadPropsForStyleProps(
471 + resourceProps: StyleProps,
472 + preloadProps: PreloadProps,
473 ): void {
410 - const {precedences} = resources;
411 - const {precedence} = styleResource;
474 + if (resourceProps.crossOrigin == null)
475 + resourceProps.crossOrigin = preloadProps.crossOrigin;
476 + if (resourceProps.referrerPolicy == null)
477 + resourceProps.referrerPolicy = preloadProps.referrerPolicy;
478 + if (resourceProps.title == null) resourceProps.title = preloadProps.title;
479 +}
480 +
481 +function scriptPropsFromPreinitOptions(
482 + src: string,
483 + options: PreinitOptions,
484 +): ScriptProps {
485 + return {
486 + src,
487 + async: true,
488 + crossOrigin: options.crossOrigin,
489 + integrity: options.integrity,
490 + };
491 +}
492 +
493 +function scriptPropsFromRawProps(src: string, rawProps: Props): ScriptProps {
494 + const props = Object.assign({}, rawProps);
495 + props.src = src;
496 + return props;
497 +}
498
413 - if (boundaryResources) {
414 - boundaryResources.add(styleResource);
415 - if (!precedences.has(precedence)) {
416 - precedences.set(precedence, new Set());
499 +function createScriptResource(
500 + resources: Resources,
501 + src: string,
502 + props: ScriptProps,
503 +): ScriptResource {
504 + if (__DEV__) {
505 + if (resources.scriptsMap.has(src)) {
506 + console.error(
507 + 'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
508 + );
509 + }
510 + }
511 + const {scriptsMap, preloadsMap} = resources;
512 +
513 + let hint = preloadsMap.get(src);
514 + if (hint) {
515 + // If a preload for this style Resource already exists there are certain props we want to adopt
516 + // on the style Resource, primarily focussed on making sure the style network pathways utilize
517 + // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
518 + // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
519 + adoptPreloadPropsForScriptProps(props, hint.props);
520 +
521 + if (__DEV__) {
522 + validateScriptAndHintProps(
523 + hint.props,
524 + props,
525 + (hint: any)._dev_implicit_construction,
526 + );
527 }
528 } else {
419 - let set = precedences.get(precedence);
420 - if (!set) {
421 - set = new Set();
422 - precedences.set(precedence, set);
529 + const preloadResourceProps = preloadAsScriptPropsFromProps(src, props);
530 + hint = createPreloadResource(
531 + resources,
532 + src,
533 + 'script',
534 + preloadResourceProps,
535 + );
536 + if (__DEV__) {
537 + (hint: any)._dev_implicit_construction = true;
538 }
424 - set.add(styleResource);
539 + resources.explicitScriptPreloads.add(hint);
540 }
426 -}
541
428 -function captureExplicitPreloadResourceDependency(
429 - resources: Resources,
430 - preloadResource: PreloadResource,
431 -): void {
432 - resources.explicitPreloads.add(preloadResource);
542 + const resource = {
543 + type: 'script',
544 + src,
545 + flushed: false,
546 + props,
547 + hint,
548 + };
549 + scriptsMap.set(src, resource);
550 +
551 + return resource;
552 }
553
435 -function captureImplicitPreloadResourceDependency(
436 - resources: Resources,
437 - preloadResource: PreloadResource,
554 +function adoptPreloadPropsForScriptProps(
555 + resourceProps: ScriptProps,
556 + preloadProps: PreloadProps,
557 ): void {
439 - resources.implicitPreloads.add(preloadResource);
558 + if (resourceProps.crossOrigin == null)
559 + resourceProps.crossOrigin = preloadProps.crossOrigin;
560 + if (resourceProps.referrerPolicy == null)
561 + resourceProps.referrerPolicy = preloadProps.referrerPolicy;
562 + if (resourceProps.integrity == null)
563 + resourceProps.integrity = preloadProps.integrity;
564 }
565
566 // Construct a resource from link props.
@@ -446,6 +570,8 @@ export function resourcesFromLink(props: Props): boolean {
570 '"currentResources" was expected to exist. This is a bug in React.',
571 );
572 }
573 + const resources = currentResources;
574 +
575 const {rel, href} = props;
576 if (!href || typeof href !== 'string') {
577 return false;
@@ -467,11 +593,11 @@ export function resourcesFromLink(props: Props): boolean {
593 validateLinkPropsForStyleResource(props);
594 }
595 // $FlowFixMe[incompatible-use] found when upgrading Flow
470 - let preloadResource = currentResources.preloadsMap.get(href);
596 + let preloadResource = resources.preloadsMap.get(href);
597 if (!preloadResource) {
598 preloadResource = createPreloadResource(
599 // $FlowFixMe[incompatible-call] found when upgrading Flow
474 - currentResources,
600 + resources,
601 href,
602 'style',
603 preloadAsStylePropsFromProps(href, props),
@@ -479,17 +605,14 @@ export function resourcesFromLink(props: Props): boolean {
605 if (__DEV__) {
606 (preloadResource: any)._dev_implicit_construction = true;
607 }
608 + resources.usedStylePreloads.add(preloadResource);
609 }
483 - captureImplicitPreloadResourceDependency(
484 - // $FlowFixMe[incompatible-call] found when upgrading Flow
485 - currentResources,
486 - preloadResource,
487 - );
610 return false;
611 } else {
612 // We are able to convert this link element to a resource exclusively. We construct the relevant Resource
613 // and return true indicating that this link was fully consumed.
492 - let resource = currentResources.stylesMap.get(href);
614 + let resource = resources.stylesMap.get(href);
615 +
616 if (resource) {
617 if (__DEV__) {
618 const resourceProps = stylePropsFromRawProps(
@@ -497,6 +620,7 @@ export function resourcesFromLink(props: Props): boolean {
620 precedence,
621 props,
622 );
623 + adoptPreloadPropsForStyleProps(resourceProps, resource.hint.props);
624 validateStyleResourceDifference(resource.props, resourceProps);
625 }
626 } else {
@@ -508,24 +632,18 @@ export function resourcesFromLink(props: Props): boolean {
632 precedence,
633 resourceProps,
634 );
635 + resources.usedStylePreloads.add(resource.hint);
636 + }
637 + if (resources.boundaryResources) {
638 + resources.boundaryResources.add(resource);
639 + } else {
640 + resource.set.add(resource);
641 }
512 - captureStyleResourceDependency(
513 - // $FlowFixMe[incompatible-call] found when upgrading Flow
514 - currentResources,
515 - // $FlowFixMe[incompatible-use] found when upgrading Flow
516 - currentResources.boundaryResources,
517 - resource,
518 - );
642 return true;
643 }
644 }
645 case 'preload': {
523 - const {as, onLoad, onError} = props;
524 - if (onLoad || onError) {
525 - // these props signal an opt-out of Resource semantics. We don't warn because there is no
526 - // conflicting opt-in like there is with Style Resources
527 - return false;
528 - }
646 + const {as} = props;
647 switch (as) {
648 case 'script':
649 case 'style':
@@ -533,8 +651,7 @@ export function resourcesFromLink(props: Props): boolean {
651 if (__DEV__) {
652 validateLinkPropsForPreloadResource(props);
653 }
536 - // $FlowFixMe[incompatible-use] found when upgrading Flow
537 - let resource = currentResources.preloadsMap.get(href);
654 + let resource = resources.preloadsMap.get(href);
655 if (resource) {
656 if (__DEV__) {
657 const originallyImplicit =
@@ -549,15 +666,26 @@ export function resourcesFromLink(props: Props): boolean {
666 }
667 } else {
668 resource = createPreloadResource(
552 - // $FlowFixMe[incompatible-call] found when upgrading Flow
553 - currentResources,
669 + resources,
670 href,
671 as,
672 preloadPropsFromRawProps(href, as, props),
673 );
674 + switch (as) {
675 + case 'script': {
676 + resources.explicitScriptPreloads.add(resource);
677 + break;
678 + }
679 + case 'style': {
680 + resources.explicitStylePreloads.add(resource);
681 + break;
682 + }
683 + case 'font': {
684 + resources.fontPreloads.add(resource);
685 + break;
686 + }
687 + }
688 }
559 - // $FlowFixMe[incompatible-call] found when upgrading Flow
560 - captureExplicitPreloadResourceDependency(currentResources, resource);
689 return true;
690 }
691 }
@@ -567,12 +695,65 @@ export function resourcesFromLink(props: Props): boolean {
695 return false;
696 }
697
698 +// Construct a resource from link props.
699 +export function resourcesFromScript(props: Props): boolean {
700 + if (!currentResources) {
701 + throw new Error(
702 + '"currentResources" was expected to exist. This is a bug in React.',
703 + );
704 + }
705 + const resources = currentResources;
706 + const {src, async, onLoad, onError} = props;
707 + if (!src || typeof src !== 'string') {
708 + return false;
709 + }
710 +
711 + if (async) {
712 + if (onLoad || onError) {
713 + if (__DEV__) {
714 + // validate
715 + }
716 + let preloadResource = resources.preloadsMap.get(src);
717 + if (!preloadResource) {
718 + preloadResource = createPreloadResource(
719 + // $FlowFixMe[incompatible-call] found when upgrading Flow
720 + resources,
721 + src,
722 + 'script',
723 + preloadAsScriptPropsFromProps(src, props),
724 + );
725 + if (__DEV__) {
726 + (preloadResource: any)._dev_implicit_construction = true;
727 + }
728 + resources.usedScriptPreloads.add(preloadResource);
729 + }
730 + } else {
731 + let resource = resources.scriptsMap.get(src);
732 + if (resource) {
733 + if (__DEV__) {
734 + const latestProps = scriptPropsFromRawProps(src, props);
735 + adoptPreloadPropsForScriptProps(latestProps, resource.hint.props);
736 + validateScriptResourceDifference(resource.props, latestProps);
737 + }
738 + } else {
739 + const resourceProps = scriptPropsFromRawProps(src, props);
740 + resource = createScriptResource(resources, src, resourceProps);
741 + resources.scripts.add(resource);
742 + }
743 + }
744 + return true;
745 + }
746 +
747 + return false;
748 +}
749 +
750 export function hoistResources(
751 resources: Resources,
752 source: BoundaryResources,
753 ): void {
574 - if (resources.boundaryResources) {
575 - mergeBoundaryResources(resources.boundaryResources, source);
754 + const currentBoundaryResources = resources.boundaryResources;
755 + if (currentBoundaryResources) {
756 + source.forEach(resource => currentBoundaryResources.add(resource));
757 source.clear();
758 }
759 }
@@ -581,12 +762,6 @@ export function hoistResourcesToRoot(
762 resources: Resources,
763 boundaryResources: BoundaryResources,
764 ): void {
584 - boundaryResources.forEach(resource => {
585 - // all precedences are set upon discovery. so we know we will have a set here
586 - const set: Set<StyleResource> = (resources.precedences.get(
587 - resource.precedence,
588 - ): any);
589 - set.add(resource);
590 - });
765 + boundaryResources.forEach(resource => resource.set.add(resource));
766 boundaryResources.clear();
767 }
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+134 -50
@@ -64,6 +64,7 @@ import {
64 prepareToRenderResources,
65 finishRenderingResources,
66 resourcesFromLink,
67 + resourcesFromScript,
68 ReactDOMServerDispatcher,
69 } from './ReactDOMFloatServer';
70 export {
@@ -1349,6 +1350,26 @@ function pushStartHtml(
1350 return pushStartGenericElement(target, props, tag, responseState);
1351 }
1352
1353 +function pushStartScript(
1354 + target: Array<Chunk | PrecomputedChunk>,
1355 + props: Object,
1356 + responseState: ResponseState,
1357 + textEmbedded: boolean,
1358 +): ReactNodeList {
1359 + if (enableFloat && resourcesFromScript(props)) {
1360 + if (textEmbedded) {
1361 + // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1362 + // to be safe and assume text will follow by inserting a textSeparator
1363 + target.push(textSeparator);
1364 + }
1365 + // We have converted this link exclusively to a resource and no longer
1366 + // need to emit it
1367 + return null;
1368 + }
1369 +
1370 + return pushStartGenericElement(target, props, 'script', responseState);
1371 +}
1372 +
1373 function pushStartGenericElement(
1374 target: Array<Chunk | PrecomputedChunk>,
1375 props: Object,
@@ -1625,6 +1646,8 @@ export function pushStartInstance(
1646 return pushStartTitle(target, props, responseState);
1647 case 'link':
1648 return pushLink(target, props, responseState, textEmbedded);
1649 + case 'script':
1650 + return pushStartScript(target, props, responseState, textEmbedded);
1651 // Newline eating tags
1652 case 'listing':
1653 case 'pre': {
@@ -2235,57 +2258,90 @@ function escapeJSObjectForInstructionScripts(input: Object): string {
2258 });
2259 }
2260
2261 +const precedencePlaceholderStart = stringToPrecomputedChunk(
2262 + '<style data-precedence="',
2263 +);
2264 +const precedencePlaceholderEnd = stringToPrecomputedChunk('"></style>');
2265 +
2266 export function writeInitialResources(
2267 destination: Destination,
2268 resources: Resources,
2269 responseState: ResponseState,
2270 ): boolean {
2243 - const explicitPreloadsTarget = [];
2244 - const remainingTarget = [];
2271 + function flushLinkResource(resource) {
2272 + if (!resource.flushed) {
2273 + pushLinkImpl(target, resource.props, responseState);
2274 + resource.flushed = true;
2275 + }
2276 + }
2277
2246 - const {precedences, explicitPreloads, implicitPreloads} = resources;
2278 + const target = [];
2279
2248 - // Flush stylesheets first by earliest precedence
2249 - precedences.forEach(precedenceResources => {
2250 - precedenceResources.forEach(resource => {
2251 - // resources should not already be flushed so we elide this check
2252 - pushLinkImpl(remainingTarget, resource.props, responseState);
2253 - resource.flushed = true;
2254 - resource.inShell = true;
2255 - resource.hint.flushed = true;
2256 - });
2280 + const {
2281 + fontPreloads,
2282 + precedences,
2283 + usedStylePreloads,
2284 + scripts,
2285 + usedScriptPreloads,
2286 + explicitStylePreloads,
2287 + explicitScriptPreloads,
2288 + } = resources;
2289 +
2290 + fontPreloads.forEach(r => {
2291 + // font preload Resources should not already be flushed so we elide this check
2292 + pushLinkImpl(target, r.props, responseState);
2293 + r.flushed = true;
2294 });
2295 + fontPreloads.clear();
2296
2259 - explicitPreloads.forEach(resource => {
2260 - if (!resource.flushed) {
2261 - pushLinkImpl(explicitPreloadsTarget, resource.props, responseState);
2262 - resource.flushed = true;
2297 + // Flush stylesheets first by earliest precedence
2298 + precedences.forEach((p, precedence) => {
2299 + if (p.size) {
2300 + p.forEach(r => {
2301 + // resources should not already be flushed so we elide this check
2302 + pushLinkImpl(target, r.props, responseState);
2303 + r.flushed = true;
2304 + r.inShell = true;
2305 + r.hint.flushed = true;
2306 + });
2307 + p.clear();
2308 + } else {
2309 + target.push(
2310 + precedencePlaceholderStart,
2311 + escapeTextForBrowser(stringToChunk(precedence)),
2312 + precedencePlaceholderEnd,
2313 + );
2314 }
2315 });
2265 - explicitPreloads.clear();
2316
2267 - implicitPreloads.forEach(resource => {
2268 - if (!resource.flushed) {
2269 - pushLinkImpl(remainingTarget, resource.props, responseState);
2270 - resource.flushed = true;
2271 - }
2317 + usedStylePreloads.forEach(flushLinkResource);
2318 + usedStylePreloads.clear();
2319 +
2320 + scripts.forEach(r => {
2321 + // should never be flushed already
2322 + pushStartGenericElement(target, r.props, 'script', responseState);
2323 + pushEndInstance(target, target, 'script', r.props);
2324 + r.flushed = true;
2325 + r.hint.flushed = true;
2326 });
2273 - implicitPreloads.clear();
2327 + scripts.clear();
2328 +
2329 + usedScriptPreloads.forEach(flushLinkResource);
2330 + usedScriptPreloads.clear();
2331 +
2332 + explicitStylePreloads.forEach(flushLinkResource);
2333 + explicitStylePreloads.clear();
2334 +
2335 + explicitScriptPreloads.forEach(flushLinkResource);
2336 + explicitScriptPreloads.clear();
2337
2338 let i;
2339 let r = true;
2277 - for (i = 0; i < explicitPreloadsTarget.length - 1; i++) {
2278 - writeChunk(destination, explicitPreloadsTarget[i]);
2279 - }
2280 - if (i < explicitPreloadsTarget.length) {
2281 - r = writeChunkAndReturn(destination, explicitPreloadsTarget[i]);
2282 - }
2283 -
2284 - for (i = 0; i < remainingTarget.length - 1; i++) {
2285 - writeChunk(destination, remainingTarget[i]);
2340 + for (i = 0; i < target.length - 1; i++) {
2341 + writeChunk(destination, target[i]);
2342 }
2287 - if (i < remainingTarget.length) {
2288 - r = writeChunkAndReturn(destination, remainingTarget[i]);
2343 + if (i < target.length) {
2344 + r = writeChunkAndReturn(destination, target[i]);
2345 }
2346 return r;
2347 }
@@ -2295,33 +2351,61 @@ export function writeImmediateResources(
2351 resources: Resources,
2352 responseState: ResponseState,
2353 ): boolean {
2298 - const {explicitPreloads, implicitPreloads} = resources;
2299 - const target = [];
2300 -
2301 - explicitPreloads.forEach(resource => {
2354 + function flushLinkResource(resource) {
2355 if (!resource.flushed) {
2356 pushLinkImpl(target, resource.props, responseState);
2357 resource.flushed = true;
2358 }
2359 + }
2360 +
2361 + const target = [];
2362 +
2363 + const {
2364 + fontPreloads,
2365 + usedStylePreloads,
2366 + scripts,
2367 + usedScriptPreloads,
2368 + explicitStylePreloads,
2369 + explicitScriptPreloads,
2370 + } = resources;
2371 +
2372 + fontPreloads.forEach(r => {
2373 + // font preload Resources should not already be flushed so we elide this check
2374 + pushLinkImpl(target, r.props, responseState);
2375 + r.flushed = true;
2376 });
2307 - explicitPreloads.clear();
2377 + fontPreloads.clear();
2378
2309 - implicitPreloads.forEach(resource => {
2310 - if (!resource.flushed) {
2311 - pushLinkImpl(target, resource.props, responseState);
2312 - resource.flushed = true;
2313 - }
2379 + usedStylePreloads.forEach(flushLinkResource);
2380 + usedStylePreloads.clear();
2381 +
2382 + scripts.forEach(r => {
2383 + // should never be flushed already
2384 + pushStartGenericElement(target, r.props, 'script', responseState);
2385 + pushEndInstance(target, target, 'script', r.props);
2386 + r.flushed = true;
2387 + r.hint.flushed = true;
2388 });
2315 - implicitPreloads.clear();
2389 + scripts.clear();
2390
2317 - let i = 0;
2318 - for (; i < target.length - 1; i++) {
2391 + usedScriptPreloads.forEach(flushLinkResource);
2392 + usedScriptPreloads.clear();
2393 +
2394 + explicitStylePreloads.forEach(flushLinkResource);
2395 + explicitStylePreloads.clear();
2396 +
2397 + explicitScriptPreloads.forEach(flushLinkResource);
2398 + explicitScriptPreloads.clear();
2399 +
2400 + let i;
2401 + let r = true;
2402 + for (i = 0; i < target.length - 1; i++) {
2403 writeChunk(destination, target[i]);
2404 }
2405 if (i < target.length) {
2322 - return writeChunkAndReturn(destination, target[i]);
2406 + r = writeChunkAndReturn(destination, target[i]);
2407 }
2324 - return false;
2408 + return r;
2409 }
2410
2411 function hasStyleResourceDependencies(
@@ -2434,7 +2518,7 @@ function writeStyleResourceDependency(
2518 case 'href':
2519 case 'rel':
2520 case 'precedence':
2437 - case 'data-rprec': {
2521 + case 'data-precedence': {
2522 break;
2523 }
2524 case 'children':
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js
+5 -3
@@ -57,9 +57,11 @@ export function completeBoundaryWithStyles(
57 let lastResource, node;
58
59 // Seed the precedence list with existing resources
60 - const nodes = thisDocument.querySelectorAll('link[data-rprec]');
60 + const nodes = thisDocument.querySelectorAll(
61 + 'link[data-precedence],style[data-precedence]',
62 + );
63 for (let i = 0; (node = nodes[i++]); ) {
62 - precedences.set(node.dataset['rprec'], (lastResource = node));
64 + precedences.set(node.dataset['precedence'], (lastResource = node));
65 }
66
67 let i = 0;
@@ -89,7 +91,7 @@ export function completeBoundaryWithStyles(
91 resourceEl = thisDocument.createElement('link');
92 resourceEl.href = href;
93 resourceEl.rel = 'stylesheet';
92 - resourceEl.dataset['rprec'] = precedence = style[j++];
94 + resourceEl.dataset['precedence'] = precedence = style[j++];
95 while ((attr = style[j++])) {
96 resourceEl.setAttribute(attr, style[j++]);
97 }
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1 -1
@@ -6,6 +6,6 @@ export const clientRenderBoundary =
6 export const completeBoundary =
7 '$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
8 export const completeBoundaryWithStyles =
9 - '$RM=new Map;\n$RR=function(p,q,v){function r(l){this.s=l}for(var t=$RC,u=$RM,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=v[e++];){var k=0;h=c[k++];if(b=u.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,w){a.onload=l;a.onerror=w});b.then(r.bind(b,"l"),r.bind(b,"e"));u.set(h,\nb);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(t.bind(null,p,q,""),t.bind(null,p,q,"Resource failed to load"))};';
9 + '$RM=new Map;\n$RR=function(p,q,v){function r(l){this.s=l}for(var t=$RC,u=$RM,m=new Map,n=document,g,e,f=n.querySelectorAll("link[data-precedence],style[data-precedence]"),d=0;e=f[d++];)m.set(e.dataset.precedence,g=e);e=0;f=[];for(var c,h,b,a;c=v[e++];){var k=0;h=c[k++];if(b=u.get(h))"l"!==b.s&&f.push(b);else{a=n.createElement("link");a.href=h;a.rel="stylesheet";for(a.dataset.precedence=d=c[k++];b=c[k++];)a.setAttribute(b,c[k++]);b=a._p=new Promise(function(l,w){a.onload=l;a.onerror=w});b.then(r.bind(b,\n"l"),r.bind(b,"e"));u.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(t.bind(null,p,q,""),t.bind(null,p,q,"Resource failed to load"))};';
10 export const completeSegment =
11 '$RS=function(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)};';
packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js
+165 -19
@@ -115,6 +115,7 @@ export function validatePreloadResourceDifference(
115 if (missingProps || extraProps || differentProps) {
116 warnDifferentProps(
117 href,
118 + 'href',
119 originalWarningName,
120 latestWarningName,
121 extraProps,
@@ -156,7 +157,7 @@ export function validateStyleResourceDifference(
157 const originalValue = originalProps[propName];
158
159 if (propValue != null && propValue !== originalValue) {
159 - propName = propName === 'data-rprec' ? 'precedence' : propName;
160 + propName = propName === 'data-precedence' ? 'precedence' : propName;
161 if (originalValue == null) {
162 extraProps = extraProps || {};
163 extraProps[propName] = propValue;
@@ -173,6 +174,7 @@ export function validateStyleResourceDifference(
174 if (missingProps || extraProps || differentProps) {
175 warnDifferentProps(
176 href,
177 + 'href',
178 originalWarningName,
179 latestWarningName,
180 extraProps,
@@ -183,6 +185,58 @@ export function validateStyleResourceDifference(
185 }
186 }
187
188 +export function validateScriptResourceDifference(
189 + originalProps: any,
190 + latestProps: any,
191 +) {
192 + if (__DEV__) {
193 + const {src} = originalProps;
194 + // eslint-disable-next-line no-labels
195 + const originalWarningName = getResourceNameForWarning(
196 + 'script',
197 + originalProps,
198 + false,
199 + );
200 + const latestWarningName = getResourceNameForWarning(
201 + 'script',
202 + latestProps,
203 + false,
204 + );
205 + let extraProps = null;
206 + let differentProps = null;
207 +
208 + for (const propName in latestProps) {
209 + const propValue = latestProps[propName];
210 + const originalValue = originalProps[propName];
211 +
212 + if (propValue != null && propValue !== originalValue) {
213 + if (originalValue == null) {
214 + extraProps = extraProps || {};
215 + extraProps[propName] = propValue;
216 + } else {
217 + differentProps = differentProps || {};
218 + differentProps[propName] = {
219 + original: originalValue,
220 + latest: propValue,
221 + };
222 + }
223 + }
224 + }
225 +
226 + if (extraProps || differentProps) {
227 + warnDifferentProps(
228 + src,
229 + 'src',
230 + originalWarningName,
231 + latestWarningName,
232 + extraProps,
233 + null,
234 + differentProps,
235 + );
236 + }
237 + }
238 +}
239 +
240 export function validateStyleAndHintProps(
241 preloadProps: any,
242 styleProps: any,
@@ -205,7 +259,7 @@ export function validateStyleAndHintProps(
259 if (preloadProps.as !== 'style') {
260 console.error(
261 'While creating a %s for href "%s" a %s for this same href was found. When preloading a stylesheet the' +
208 - ' "as" prop must be of type "style". This most likely ocurred by rending a preload link with an incorrect' +
262 + ' "as" prop must be of type "style". This most likely ocurred by rendering a preload link with an incorrect' +
263 ' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
264 latestWarningName,
265 href,
@@ -252,6 +306,86 @@ export function validateStyleAndHintProps(
306 if (missingProps || extraProps || differentProps) {
307 warnDifferentProps(
308 href,
309 + 'href',
310 + originalWarningName,
311 + latestWarningName,
312 + extraProps,
313 + missingProps,
314 + differentProps,
315 + );
316 + }
317 + }
318 +}
319 +
320 +export function validateScriptAndHintProps(
321 + preloadProps: any,
322 + scriptProps: any,
323 + implicitPreload: boolean,
324 +) {
325 + if (__DEV__) {
326 + const {href} = preloadProps;
327 +
328 + const originalWarningName = getResourceNameForWarning(
329 + 'preload',
330 + preloadProps,
331 + implicitPreload,
332 + );
333 + const latestWarningName = getResourceNameForWarning(
334 + 'script',
335 + scriptProps,
336 + false,
337 + );
338 +
339 + if (preloadProps.as !== 'script') {
340 + console.error(
341 + 'While creating a %s for href "%s" a %s for this same url was found. When preloading a script the' +
342 + ' "as" prop must be of type "script". This most likely ocurred by rendering a preload link with an incorrect' +
343 + ' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
344 + latestWarningName,
345 + href,
346 + originalWarningName,
347 + );
348 + }
349 +
350 + let missingProps = null;
351 + let extraProps = null;
352 + let differentProps = null;
353 +
354 + for (const propName in scriptProps) {
355 + const scriptValue = scriptProps[propName];
356 + const preloadValue = preloadProps[propName];
357 + switch (propName) {
358 + // Check for difference on specific props that cross over or influence
359 + // the relationship between the preload and stylesheet
360 + case 'crossOrigin':
361 + case 'referrerPolicy':
362 + case 'integrity': {
363 + if (
364 + preloadValue !== scriptValue &&
365 + !(preloadValue == null && scriptValue == null)
366 + ) {
367 + if (scriptValue == null) {
368 + missingProps = missingProps || {};
369 + missingProps[propName] = preloadValue;
370 + } else if (preloadValue == null) {
371 + extraProps = extraProps || {};
372 + extraProps[propName] = scriptValue;
373 + } else {
374 + differentProps = differentProps || {};
375 + differentProps[propName] = {
376 + original: preloadValue,
377 + latest: scriptValue,
378 + };
379 + }
380 + }
381 + }
382 + }
383 + }
384 +
385 + if (missingProps || extraProps || differentProps) {
386 + warnDifferentProps(
387 + href,
388 + 'href',
389 originalWarningName,
390 latestWarningName,
391 extraProps,
@@ -263,7 +397,8 @@ export function validateStyleAndHintProps(
397 }
398
399 function warnDifferentProps(
266 - href: string,
400 + url: string,
401 + urlPropKey: string,
402 originalName: string,
403 latestName: string,
404 extraProps: ?{[string]: any},
@@ -274,7 +409,7 @@ function warnDifferentProps(
409 const juxtaposedNameStatement =
410 latestName === originalName
411 ? 'an earlier instance of this Resource'
277 - : `a ${originalName} with the same href`;
412 + : `a ${originalName} with the same ${urlPropKey}`;
413
414 let comparisonStatement = '';
415 if (missingProps !== null && typeof missingProps === 'object') {
@@ -294,12 +429,14 @@ function warnDifferentProps(
429 }
430
431 console.error(
297 - 'A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
432 + 'A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
433 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
299 - ' update Resources that share an href to have props that agree. The differences are described below.%s',
434 + ' update Resources that share an %s to have props that agree. The differences are described below.%s',
435 latestName,
301 - href,
436 + urlPropKey,
437 + url,
438 juxtaposedNameStatement,
439 + urlPropKey,
440 comparisonStatement,
441 );
442 }
@@ -315,6 +452,9 @@ function getResourceNameForWarning(
452 case 'style': {
453 return 'style Resource';
454 }
455 + case 'script': {
456 + return 'script Resource';
457 + }
458 case 'preload': {
459 if (implicit) {
460 return `preload for a ${props.as} Resource`;
@@ -326,15 +466,17 @@ function getResourceNameForWarning(
466 return 'Resource';
467 }
468
329 -export function validateHrefKeyedUpdatedProps(
469 +export function validateURLKeyedUpdatedProps(
470 pendingProps: Props,
471 currentProps: Props,
472 + resourceType: 'style' | 'script' | 'href',
473 + urlPropKey: 'href' | 'src',
474 ): boolean {
475 if (__DEV__) {
334 - // This function should never be called if we don't have hrefs so we don't bother considering
476 + // This function should never be called if we don't have /srcs so we don't bother considering
477 // Whether they are null or undefined
336 - if (pendingProps.href === currentProps.href) {
337 - // If we have the same href we need all other props to be the same
478 + if (pendingProps[urlPropKey] === currentProps[urlPropKey]) {
479 + // If we have the same href/src we need all other props to be the same
480 let missingProps;
481 let extraProps;
482 let differentProps;
@@ -366,7 +508,7 @@ export function validateHrefKeyedUpdatedProps(
508 }
509 if (missingProps || extraProps || differentProps) {
510 const latestWarningName = getResourceNameForWarning(
369 - 'style',
511 + resourceType,
512 currentProps,
513 false,
514 );
@@ -388,14 +530,17 @@ export function validateHrefKeyedUpdatedProps(
530 }
531 }
532 console.error(
391 - 'A %s with href "%s" recieved new props with different values from the props used' +
533 + 'A %s with %s "%s" recieved new props with different values from the props used' +
534 ' when this Resource was first rendered. React will only use the props provided when' +
393 - ' this resource was first rendered until a new href is provided. Unlike conventional' +
535 + ' this resource was first rendered until a new %s is provided. Unlike conventional' +
536 ' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
537 ' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
396 - ' (href) must have props that agree with each other. The differences are described below.%s',
538 + ' (%s) must have props that agree with each other. The differences are described below.%s',
539 latestWarningName,
398 - currentProps.href,
540 + urlPropKey,
541 + currentProps[urlPropKey],
542 + urlPropKey,
543 + urlPropKey,
544 comparisonStatement,
545 );
546 return true;
@@ -556,7 +701,8 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
701 } else {
702 const as = options.as;
703 switch (as) {
559 - case 'style': {
704 + case 'style':
705 + case 'script': {
706 break;
707 }
708
@@ -565,8 +711,8 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
711 const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
712 console.error(
713 'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
568 - ' specifying the Resource type. It found %s instead. Currently, the only valid resource type for preinit is "style".' +
569 - ' The href for the preinit call where this warning originated is "%s".',
714 + ' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' +
715 + ' and "script". The href for the preinit call where this warning originated is "%s".',
716 typeOfAs,
717 href,
718 );
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+487 -253
@@ -137,17 +137,23 @@ describe('ReactDOMFloat', () => {
137 buffer = '';
138 }
139
140 - function getVisibleChildren(element) {
140 + function getMeaningfulChildren(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')
146 + // some tags are ambiguous and might be hidden because they look like non-meaningful children
147 + // so we have a global override where if this data attribute is included we also include the node
148 + node.hasAttribute('data-meaningful') ||
149 + (node.tagName === 'SCRIPT' &&
150 + node.hasAttribute('src') &&
151 + node.hasAttribute('async')) ||
152 + (node.tagName !== 'SCRIPT' &&
153 + node.tagName !== 'TEMPLATE' &&
154 + node.tagName !== 'template' &&
155 + !node.hasAttribute('hidden') &&
156 + !node.hasAttribute('aria-hidden'))
157 ) {
158 const props = {};
159 const attributes = node.attributes;
@@ -161,7 +167,7 @@ describe('ReactDOMFloat', () => {
167 }
168 props[attributes[i].name] = attributes[i].value;
169 }
164 - props.children = getVisibleChildren(node);
170 + props.children = getMeaningfulChildren(node);
171 children.push(React.createElement(node.tagName.toLowerCase(), props));
172 }
173 } else if (node.nodeType === 3) {
@@ -264,7 +270,7 @@ describe('ReactDOMFloat', () => {
270 <App />,
271 )}</head><body>foo</body></html>`;
272 });
267 - expect(getVisibleChildren(document)).toEqual(
273 + expect(getMeaningfulChildren(document)).toEqual(
274 <html>
275 <head>
276 <link rel="preload" href="foo" as="style" />
@@ -291,7 +297,7 @@ describe('ReactDOMFloat', () => {
297 <App />,
298 )}<body>foo</body></html>`;
299 });
294 - expect(getVisibleChildren(document)).toEqual(
300 + expect(getMeaningfulChildren(document)).toEqual(
301 <html>
302 <head>
303 <link rel="preload" href="foo" as="style" />
@@ -327,10 +333,10 @@ describe('ReactDOMFloat', () => {
333 ' a Resource at all. valid rel types for Resources are "stylesheet" and "preload". The previous' +
334 ' rel for this instance was "stylesheet". The updated rel is "author" and the updated href is "bar".',
335 );
330 - expect(getVisibleChildren(document)).toEqual(
336 + expect(getMeaningfulChildren(document)).toEqual(
337 <html>
338 <head>
333 - <link rel="stylesheet" href="foo" data-rprec="foo" />
339 + <link rel="stylesheet" href="foo" data-precedence="foo" />
340 <link rel="preload" as="style" href="foo" />
341 </head>
342 <body>
@@ -361,7 +367,7 @@ describe('ReactDOMFloat', () => {
367 );
368 pipe(writable);
369 });
364 - expect(getVisibleChildren(document)).toEqual(
370 + expect(getMeaningfulChildren(document)).toEqual(
371 <html>
372 <head>
373 <link rel="preload" as="style" href="foo" />
@@ -380,7 +386,7 @@ describe('ReactDOMFloat', () => {
386 const root = ReactDOMClient.createRoot(container);
387 root.render(<Component />);
388 expect(Scheduler).toFlushWithoutYielding();
383 - expect(getVisibleChildren(document)).toEqual(
389 + expect(getMeaningfulChildren(document)).toEqual(
390 <html>
391 <head>
392 <link rel="preload" as="style" href="foo" />
@@ -404,7 +410,7 @@ describe('ReactDOMFloat', () => {
410 root.render(<App />);
411 expect(Scheduler).toFlushWithoutYielding();
412
407 - expect(getVisibleChildren(document)).toEqual(
413 + expect(getMeaningfulChildren(document)).toEqual(
414 <html>
415 <head>
416 <link rel="preload" as="style" href="foo" />
@@ -428,7 +434,7 @@ describe('ReactDOMFloat', () => {
434 root.render(<App />);
435 expect(Scheduler).toFlushWithoutYielding();
436
431 - expect(getVisibleChildren(document)).toEqual(
437 + expect(getMeaningfulChildren(document)).toEqual(
438 <html>
439 <head>
440 <link rel="preload" as="style" href="foo" />
@@ -452,7 +458,7 @@ describe('ReactDOMFloat', () => {
458 // to the window.document global when no other documents have been used
459 // The way the JSDOM runtim is created for these tests the local document
460 // global does not point to the global.document
455 - expect(getVisibleChildren(global.document)).toEqual(
461 + expect(getMeaningfulChildren(global.document)).toEqual(
462 <html>
463 <head>
464 <link rel="preload" as="style" href="bar" />
@@ -505,7 +511,7 @@ describe('ReactDOMFloat', () => {
511 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<ServerApp />);
512 pipe(writable);
513 });
508 - expect(getVisibleChildren(document)).toEqual(
514 + expect(getMeaningfulChildren(document)).toEqual(
515 <html>
516 <head>
517 <link rel="preload" as="script" href="foo" integrity="foo hash" />
@@ -526,7 +532,7 @@ describe('ReactDOMFloat', () => {
532 ReactDOMClient.hydrateRoot(document, <ClientApp />);
533 expect(Scheduler).toFlushWithoutYielding();
534
529 - expect(getVisibleChildren(document)).toEqual(
535 + expect(getMeaningfulChildren(document)).toEqual(
536 <html>
537 <head>
538 <link rel="preload" as="script" href="foo" integrity="foo hash" />
@@ -558,7 +564,7 @@ describe('ReactDOMFloat', () => {
564 // @gate enableFloat
565 it('creates a style Resource when called during server rendering before first flush', async () => {
566 function Component() {
561 - ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
567 + ReactDOM.preinit('foo', {as: 'style'});
568 return 'foo';
569 }
570 await actIntoEmptyDocument(() => {
@@ -572,10 +578,10 @@ describe('ReactDOMFloat', () => {
578 );
579 pipe(writable);
580 });
575 - expect(getVisibleChildren(document)).toEqual(
581 + expect(getMeaningfulChildren(document)).toEqual(
582 <html>
583 <head>
578 - <link rel="stylesheet" href="foo" data-rprec="foo" />
584 + <link rel="stylesheet" href="foo" data-precedence="default" />
585 </head>
586 <body>foo</body>
587 </html>,
@@ -610,7 +616,7 @@ describe('ReactDOMFloat', () => {
616 await act(() => {
617 resolveText('unblock');
618 });
613 - expect(getVisibleChildren(document)).toEqual(
619 + expect(getMeaningfulChildren(document)).toEqual(
620 <html>
621 <head />
622 <body>
@@ -630,10 +636,10 @@ describe('ReactDOMFloat', () => {
636 const root = ReactDOMClient.createRoot(container);
637 root.render(<Component />);
638 expect(Scheduler).toFlushWithoutYielding();
633 - expect(getVisibleChildren(document)).toEqual(
639 + expect(getMeaningfulChildren(document)).toEqual(
640 <html>
641 <head>
636 - <link rel="stylesheet" href="foo" data-rprec="foo" />
642 + <link rel="stylesheet" href="foo" data-precedence="foo" />
643 </head>
644 <body>
645 <div id="container">foo</div>
@@ -660,7 +666,7 @@ describe('ReactDOMFloat', () => {
666 root.render(<App />);
667 expect(Scheduler).toFlushWithoutYielding();
668
663 - expect(getVisibleChildren(document)).toEqual(
669 + expect(getMeaningfulChildren(document)).toEqual(
670 <html>
671 <head>
672 <link rel="preload" as="style" href="insertion" />
@@ -686,7 +692,7 @@ describe('ReactDOMFloat', () => {
692 // to the window.document global when no other documents have been used
693 // The way the JSDOM runtim is created for these tests the local document
694 // global does not point to the global.document
689 - expect(getVisibleChildren(global.document)).toEqual(
695 + expect(getMeaningfulChildren(global.document)).toEqual(
696 <html>
697 <head>
698 <link rel="preload" as="style" href="bar" />
@@ -697,6 +703,53 @@ describe('ReactDOMFloat', () => {
703 });
704 });
705
706 + describe('ReactDOM.preinit as script', () => {
707 + // @gate enableFloat
708 + it('can preinit a script', async () => {
709 + function App({srcs}) {
710 + srcs.forEach(src => ReactDOM.preinit(src, {as: 'script'}));
711 + return (
712 + <html>
713 + <head>
714 + <title>title</title>
715 + </head>
716 + <body>foo</body>
717 + </html>
718 + );
719 + }
720 + await actIntoEmptyDocument(() => {
721 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
722 + <App srcs={['server', 'shared']} />,
723 + );
724 + pipe(writable);
725 + });
726 + expect(getMeaningfulChildren(document)).toEqual(
727 + <html>
728 + <head>
729 + <script src="server" async="" />
730 + <script src="shared" async="" />
731 + <title>title</title>
732 + </head>
733 + <body>foo</body>
734 + </html>,
735 + );
736 +
737 + ReactDOMClient.hydrateRoot(document, <App srcs={['client', 'shared']} />);
738 + expect(Scheduler).toFlushWithoutYielding();
739 + expect(getMeaningfulChildren(document)).toEqual(
740 + <html>
741 + <head>
742 + <script src="server" async="" />
743 + <script src="shared" async="" />
744 + <title>title</title>
745 + <script src="client" async="" />
746 + </head>
747 + <body>foo</body>
748 + </html>,
749 + );
750 + });
751 + });
752 +
753 describe('document encapsulation', () => {
754 // @gate enableFloat
755 it('can support styles inside portals to a shadowRoot', async () => {
@@ -721,10 +774,10 @@ describe('ReactDOMFloat', () => {
774 </>,
775 );
776 expect(Scheduler).toFlushWithoutYielding();
724 - expect(getVisibleChildren(document)).toEqual(
777 + expect(getMeaningfulChildren(document)).toEqual(
778 <html>
779 <head>
727 - <link rel="stylesheet" href="foo" data-rprec="default" />
780 + <link rel="stylesheet" href="foo" data-precedence="default" />
781 <link rel="preload" href="foo" as="style" />
782 </head>
783 <body>
@@ -732,11 +785,11 @@ describe('ReactDOMFloat', () => {
785 </body>
786 </html>,
787 );
735 - expect(getVisibleChildren(shadow)).toEqual([
788 + expect(getMeaningfulChildren(shadow)).toEqual([
789 <link
790 rel="stylesheet"
791 href="foo"
739 - data-rprec="different"
792 + data-precedence="different"
793 data-extra-prop="foo"
794 />,
795 <div>shadow</div>,
@@ -781,10 +834,10 @@ describe('ReactDOMFloat', () => {
834 </>,
835 );
836 expect(Scheduler).toFlushWithoutYielding();
784 - expect(getVisibleChildren(document)).toEqual(
837 + expect(getMeaningfulChildren(document)).toEqual(
838 <html>
839 <head>
787 - <link rel="stylesheet" href="foo" data-rprec="default" />
840 + <link rel="stylesheet" href="foo" data-precedence="default" />
841 <link rel="preload" href="foo" as="style" />
842 <link rel="preload" href="bar" as="style" />
843 <link rel="preload" href="baz" as="style" />
@@ -795,11 +848,11 @@ describe('ReactDOMFloat', () => {
848 </body>
849 </html>,
850 );
798 - expect(getVisibleChildren(shadow)).toEqual([
799 - <link rel="stylesheet" href="foo" data-rprec="one" />,
800 - <link rel="stylesheet" href="baz" data-rprec="one" />,
801 - <link rel="stylesheet" href="bar" data-rprec="two" />,
802 - <link rel="stylesheet" href="qux" data-rprec="three" />,
851 + expect(getMeaningfulChildren(shadow)).toEqual([
852 + <link rel="stylesheet" href="foo" data-precedence="one" />,
853 + <link rel="stylesheet" href="baz" data-precedence="one" />,
854 + <link rel="stylesheet" href="bar" data-precedence="two" />,
855 + <link rel="stylesheet" href="qux" data-precedence="three" />,
856 <div>
857 <div id="shadowcontainer1">
858 <div>2</div>
@@ -829,10 +882,10 @@ describe('ReactDOMFloat', () => {
882 pipe(writable);
883 });
884
832 - expect(getVisibleChildren(document)).toEqual(
885 + expect(getMeaningfulChildren(document)).toEqual(
886 <html>
887 <head>
835 - <link rel="stylesheet" href="aresource" data-rprec="foo" />
888 + <link rel="stylesheet" href="aresource" data-precedence="foo" />
889 </head>
890 <body>
891 <div>hello world</div>
@@ -855,10 +908,10 @@ describe('ReactDOMFloat', () => {
908 );
909 expect(Scheduler).toFlushWithoutYielding();
910
858 - expect(getVisibleChildren(document)).toEqual(
911 + expect(getMeaningfulChildren(document)).toEqual(
912 <html>
913 <head>
861 - <link rel="stylesheet" href="aresource" data-rprec="foo" />
914 + <link rel="stylesheet" href="aresource" data-precedence="foo" />
915 </head>
916 <body>
917 <div>hello world</div>
@@ -893,10 +946,10 @@ describe('ReactDOMFloat', () => {
946 );
947 expect(Scheduler).toFlushWithoutYielding();
948
896 - expect(getVisibleChildren(document)).toEqual(
949 + expect(getMeaningfulChildren(document)).toEqual(
950 <html>
951 <head>
899 - <link rel="stylesheet" href="aresource" data-rprec="foo" />
952 + <link rel="stylesheet" href="aresource" data-precedence="foo" />
953 </head>
954 <body>
955 <div>hello world</div>
@@ -920,7 +973,7 @@ describe('ReactDOMFloat', () => {
973 pipe(writable);
974 });
975
923 - expect(getVisibleChildren(document)).toEqual(
976 + expect(getMeaningfulChildren(document)).toEqual(
977 <html>
978 <head>
979 <link rel="preload" as="style" href="notaresource" />
@@ -949,12 +1002,12 @@ describe('ReactDOMFloat', () => {
1002 );
1003 pipe(writable);
1004 });
952 - expect(getVisibleChildren(document)).toEqual(
1005 + expect(getMeaningfulChildren(document)).toEqual(
1006 <html>
1007 <head>
955 - <link rel="stylesheet" href="foo1" data-rprec="foo" />
956 - <link rel="stylesheet" href="foo2" data-rprec="foo" />
957 - <link rel="stylesheet" href="default1" data-rprec="default" />
1008 + <link rel="stylesheet" href="foo1" data-precedence="foo" />
1009 + <link rel="stylesheet" href="foo2" data-precedence="foo" />
1010 + <link rel="stylesheet" href="default1" data-precedence="default" />
1011 </head>
1012 <body>
1013 <div>hello world</div>
@@ -975,15 +1028,15 @@ describe('ReactDOMFloat', () => {
1028 </html>,
1029 );
1030 expect(Scheduler).toFlushWithoutYielding();
978 - expect(getVisibleChildren(document)).toEqual(
1031 + expect(getMeaningfulChildren(document)).toEqual(
1032 <html>
1033 <head>
981 - <link rel="stylesheet" href="foo1" data-rprec="foo" />
982 - <link rel="stylesheet" href="foo2" data-rprec="foo" />
983 - <link rel="stylesheet" href="foo3" data-rprec="foo" />
984 - <link rel="stylesheet" href="default1" data-rprec="default" />
985 - <link rel="stylesheet" href="default2" data-rprec="default" />
986 - <link rel="stylesheet" href="bar1" data-rprec="bar" />
1034 + <link rel="stylesheet" href="foo1" data-precedence="foo" />
1035 + <link rel="stylesheet" href="foo2" data-precedence="foo" />
1036 + <link rel="stylesheet" href="foo3" data-precedence="foo" />
1037 + <link rel="stylesheet" href="default1" data-precedence="default" />
1038 + <link rel="stylesheet" href="default2" data-precedence="default" />
1039 + <link rel="stylesheet" href="bar1" data-precedence="bar" />
1040 <link rel="preload" as="style" href="bar1" />
1041 <link rel="preload" as="style" href="foo3" />
1042 <link rel="preload" as="style" href="default2" />
@@ -1018,10 +1071,10 @@ describe('ReactDOMFloat', () => {
1071 </html>,
1072 );
1073 expect(Scheduler).toFlushWithoutYielding();
1021 - expect(getVisibleChildren(document)).toEqual(
1074 + expect(getMeaningfulChildren(document)).toEqual(
1075 <html>
1076 <head>
1024 - <link rel="stylesheet" href="foo" data-rprec="foo" />
1077 + <link rel="stylesheet" href="foo" data-precedence="foo" />
1078 </head>
1079 <body>hello world</body>
1080 </html>,
@@ -1068,12 +1121,12 @@ describe('ReactDOMFloat', () => {
1121 ],
1122 {withoutStack: 1},
1123 );
1071 - expect(getVisibleChildren(document)).toEqual(
1124 + expect(getMeaningfulChildren(document)).toEqual(
1125 <html>
1126 <head>
1074 - <link rel="stylesheet" href="foo" data-rprec="foo" />
1075 - <link rel="stylesheet" href="bar" data-rprec="bar" />
1076 - <link rel="stylesheet" href="qux" data-rprec="qux" />
1127 + <link rel="stylesheet" href="foo" data-precedence="foo" />
1128 + <link rel="stylesheet" href="bar" data-precedence="bar" />
1129 + <link rel="stylesheet" href="qux" data-precedence="qux" />
1130 </head>
1131 <body>client</body>
1132 </html>,
@@ -1120,11 +1173,11 @@ describe('ReactDOMFloat', () => {
1173 ],
1174 {withoutStack: 1},
1175 );
1123 - expect(getVisibleChildren(document)).toEqual(
1176 + expect(getMeaningfulChildren(document)).toEqual(
1177 <html>
1178 <head>
1126 - <link rel="stylesheet" href="qux" data-rprec="qux" />
1127 - <link rel="stylesheet" href="foo" data-rprec="foo" />
1179 + <link rel="stylesheet" href="qux" data-precedence="qux" />
1180 + <link rel="stylesheet" href="foo" data-precedence="foo" />
1181 </head>
1182 <body>client</body>
1183 </html>,
@@ -1146,11 +1199,11 @@ describe('ReactDOMFloat', () => {
1199 </html>,
1200 );
1201 expect(Scheduler).toFlushWithoutYielding();
1149 - expect(getVisibleChildren(document)).toEqual(
1202 + expect(getMeaningfulChildren(document)).toEqual(
1203 <html>
1204 <head>
1152 - <link rel="stylesheet" href="foo" data-rprec="foo" />
1153 - <link rel="stylesheet" href="bar" data-rprec="bar" />
1205 + <link rel="stylesheet" href="foo" data-precedence="foo" />
1206 + <link rel="stylesheet" href="bar" data-precedence="bar" />
1207 </head>
1208 <body>hello</body>
1209 </html>,
@@ -1170,12 +1223,12 @@ describe('ReactDOMFloat', () => {
1223 expect(Scheduler).toFlushWithoutYielding();
1224 // The reason we do not see preloads in the head is they are inserted synchronously
1225 // during render and then when the new singleton mounts it resets it's content, retaining only styles
1173 - expect(getVisibleChildren(document)).toEqual(
1226 + expect(getMeaningfulChildren(document)).toEqual(
1227 <html>
1228 <head>
1176 - <link rel="stylesheet" href="foo" data-rprec="foo" />
1177 - <link rel="stylesheet" href="bar" data-rprec="bar" />
1178 - <link rel="stylesheet" href="baz" data-rprec="baz" />
1229 + <link rel="stylesheet" href="foo" data-precedence="foo" />
1230 + <link rel="stylesheet" href="bar" data-precedence="bar" />
1231 + <link rel="stylesheet" href="baz" data-precedence="baz" />
1232 </head>
1233 <body>hello</body>
1234 </html>,
@@ -1183,6 +1236,72 @@ describe('ReactDOMFloat', () => {
1236 });
1237 });
1238
1239 + describe('script resources', () => {
1240 + // @gate enableFloat
1241 + it('treats async scripts without onLoad or onError as Resources', async () => {
1242 + await actIntoEmptyDocument(() => {
1243 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1244 + <html>
1245 + <head />
1246 + <body>
1247 + <script src="foo" async={true} />
1248 + <script src="bar" async={true} onLoad={() => {}} />
1249 + <script src="baz" data-meaningful="" />
1250 + hello world
1251 + </body>
1252 + </html>,
1253 + );
1254 + pipe(writable);
1255 + });
1256 + // The plain async script is converted to a resource and emitted as part of the shell
1257 + // The async script with onLoad is preloaded in the shell but is expecting to be added
1258 + // during hydration. This is novel, the script is NOT a HostResource but it also will
1259 + // never hydrate
1260 + // The regular script is just a normal html that should hydrate with a HostComponent
1261 + expect(getMeaningfulChildren(document)).toEqual(
1262 + <html>
1263 + <head>
1264 + <script src="foo" async="" />
1265 + <link rel="preload" href="bar" as="script" />
1266 + </head>
1267 + <body>
1268 + <script src="baz" data-meaningful="" />
1269 + hello world
1270 + </body>
1271 + </html>,
1272 + );
1273 +
1274 + ReactDOMClient.hydrateRoot(
1275 + document,
1276 + <html>
1277 + <head />
1278 + <body>
1279 + <script src="foo" async={true} />
1280 + <script src="bar" async={true} onLoad={() => {}} />
1281 + <script src="baz" data-meaningful="" />
1282 + hello world
1283 + </body>
1284 + </html>,
1285 + );
1286 + expect(Scheduler).toFlushWithoutYielding();
1287 + // The async script with onLoad is inserted in the right place but does not cause the hydration
1288 + // to fail.
1289 + expect(getMeaningfulChildren(document)).toEqual(
1290 + <html>
1291 + <head>
1292 + <script src="foo" async="" />
1293 + <link rel="preload" href="bar" as="script" />
1294 + </head>
1295 + <body>
1296 + <script src="bar" async="" />
1297 + <script src="baz" data-meaningful="" />
1298 + hello world
1299 + </body>
1300 + </html>,
1301 + );
1302 + });
1303 + });
1304 +
1305 // @gate enableFloat
1306 it('client renders a boundary if a style Resource dependency fails to load', async () => {
1307 function BlockedOn({text, children}) {
@@ -1214,11 +1333,11 @@ describe('ReactDOMFloat', () => {
1333 resolveText('unblock');
1334 });
1335
1217 - expect(getVisibleChildren(document)).toEqual(
1336 + expect(getMeaningfulChildren(document)).toEqual(
1337 <html>
1338 <head>
1220 - <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
1221 - <link rel="stylesheet" href="bar" data-rprec="arbitrary" />
1339 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1340 + <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1341 </head>
1342 <body>
1343 loading...
@@ -1245,11 +1364,11 @@ describe('ReactDOMFloat', () => {
1364 'Resource failed to load',
1365 );
1366
1248 - expect(getVisibleChildren(document)).toEqual(
1367 + expect(getMeaningfulChildren(document)).toEqual(
1368 <html>
1369 <head>
1251 - <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
1252 - <link rel="stylesheet" href="bar" data-rprec="arbitrary" />
1370 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1371 + <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1372 </head>
1373 <body>
1374 loading...
@@ -1267,11 +1386,11 @@ describe('ReactDOMFloat', () => {
1386 },
1387 });
1388 expect(Scheduler).toFlushWithoutYielding();
1270 - expect(getVisibleChildren(document)).toEqual(
1389 + expect(getMeaningfulChildren(document)).toEqual(
1390 <html>
1391 <head>
1273 - <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
1274 - <link rel="stylesheet" href="bar" data-rprec="arbitrary" />
1392 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1393 + <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1394 </head>
1395 <body>
1396 <link rel="preload" href="foo" as="style" />
@@ -1300,10 +1419,10 @@ describe('ReactDOMFloat', () => {
1419 );
1420 pipe(writable);
1421 });
1303 - expect(getVisibleChildren(document)).toEqual(
1422 + expect(getMeaningfulChildren(document)).toEqual(
1423 <html>
1424 <head>
1306 - <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
1425 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1426 </head>
1427 <body>Hello</body>
1428 </html>,
@@ -1317,10 +1436,10 @@ describe('ReactDOMFloat', () => {
1436 </html>,
1437 );
1438 expect(Scheduler).toFlushWithoutYielding();
1320 - expect(getVisibleChildren(document)).toEqual(
1439 + expect(getMeaningfulChildren(document)).toEqual(
1440 <html>
1441 <head>
1323 - <link rel="stylesheet" href="foo" data-rprec="arbitrary" />
1442 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1443 </head>
1444 <body>Hello</body>
1445 </html>,
@@ -1358,12 +1477,12 @@ describe('ReactDOMFloat', () => {
1477 resolveText('baz');
1478 });
1479
1361 - expect(getVisibleChildren(document)).toEqual(
1480 + expect(getMeaningfulChildren(document)).toEqual(
1481 <html>
1482 <head>
1364 - <link rel="stylesheet" href="foo" data-rprec="one" />
1365 - <link rel="stylesheet" href="bar" data-rprec="two" />
1366 - <link rel="stylesheet" href="baz" data-rprec="three" />
1483 + <link rel="stylesheet" href="foo" data-precedence="one" />
1484 + <link rel="stylesheet" href="bar" data-precedence="two" />
1485 + <link rel="stylesheet" href="baz" data-precedence="three" />
1486 </head>
1487 <body>
1488 {'foo'}
@@ -1394,12 +1513,16 @@ describe('ReactDOMFloat', () => {
1513 readText(text);
1514 return children;
1515 }
1516 + function PresetPrecedence() {
1517 + ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
1518 + }
1519 await actIntoEmptyDocument(() => {
1520 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1521 <html>
1522 <head />
1523 <body>
1524 <link rel="stylesheet" href="initial" precedence="one" />
1525 + <PresetPrecedence />
1526 <div>
1527 <Suspense fallback="loading foo bar...">
1528 <link rel="stylesheet" href="foo" precedence="one" />
@@ -1467,10 +1590,11 @@ describe('ReactDOMFloat', () => {
1590 pipe(writable);
1591 });
1592
1470 - expect(getVisibleChildren(document)).toEqual(
1593 + expect(getMeaningfulChildren(document)).toEqual(
1594 <html>
1595 <head>
1473 - <link rel="stylesheet" href="initial" data-rprec="one" />
1596 + <link rel="stylesheet" href="initial" data-precedence="one" />
1597 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1598 <link rel="preload" href="foo" as="style" />
1599 </head>
1600 <body>
@@ -1486,15 +1610,16 @@ describe('ReactDOMFloat', () => {
1610 resolveText('bar');
1611 });
1612
1489 - expect(getVisibleChildren(document)).toEqual(
1613 + expect(getMeaningfulChildren(document)).toEqual(
1614 <html>
1615 <head>
1492 - <link rel="stylesheet" href="initial" data-rprec="one" />
1493 - <link rel="stylesheet" href="foo" data-rprec="one" />
1616 + <link rel="stylesheet" href="initial" data-precedence="one" />
1617 + <link rel="stylesheet" href="foo" data-precedence="one" />
1618 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1619 <link
1620 rel="stylesheet"
1621 href="bar"
1497 - data-rprec="default"
1622 + data-precedence="default"
1623 data-foo="foo"
1624 crossorigin="anonymous"
1625 />
@@ -1516,15 +1641,16 @@ describe('ReactDOMFloat', () => {
1641 link.dispatchEvent(event);
1642 });
1643
1519 - expect(getVisibleChildren(document)).toEqual(
1644 + expect(getMeaningfulChildren(document)).toEqual(
1645 <html>
1646 <head>
1522 - <link rel="stylesheet" href="initial" data-rprec="one" />
1523 - <link rel="stylesheet" href="foo" data-rprec="one" />
1647 + <link rel="stylesheet" href="initial" data-precedence="one" />
1648 + <link rel="stylesheet" href="foo" data-precedence="one" />
1649 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1650 <link
1651 rel="stylesheet"
1652 href="bar"
1527 - data-rprec="default"
1653 + data-precedence="default"
1654 data-foo="foo"
1655 crossorigin="anonymous"
1656 />
@@ -1546,15 +1672,16 @@ describe('ReactDOMFloat', () => {
1672 link.dispatchEvent(event);
1673 });
1674
1549 - expect(getVisibleChildren(document)).toEqual(
1675 + expect(getMeaningfulChildren(document)).toEqual(
1676 <html>
1677 <head>
1552 - <link rel="stylesheet" href="initial" data-rprec="one" />
1553 - <link rel="stylesheet" href="foo" data-rprec="one" />
1678 + <link rel="stylesheet" href="initial" data-precedence="one" />
1679 + <link rel="stylesheet" href="foo" data-precedence="one" />
1680 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1681 <link
1682 rel="stylesheet"
1683 href="bar"
1557 - data-rprec="default"
1684 + data-precedence="default"
1685 data-foo="foo"
1686 crossorigin="anonymous"
1687 />
@@ -1578,15 +1705,16 @@ describe('ReactDOMFloat', () => {
1705 resolveText('baz');
1706 });
1707
1581 - expect(getVisibleChildren(document)).toEqual(
1708 + expect(getMeaningfulChildren(document)).toEqual(
1709 <html>
1710 <head>
1584 - <link rel="stylesheet" href="initial" data-rprec="one" />
1585 - <link rel="stylesheet" href="foo" data-rprec="one" />
1711 + <link rel="stylesheet" href="initial" data-precedence="one" />
1712 + <link rel="stylesheet" href="foo" data-precedence="one" />
1713 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1714 <link
1715 rel="stylesheet"
1716 href="bar"
1589 - data-rprec="default"
1717 + data-precedence="default"
1718 data-foo="foo"
1719 crossorigin="anonymous"
1720 />
@@ -1611,20 +1739,21 @@ describe('ReactDOMFloat', () => {
1739 resolveText('qux');
1740 });
1741
1614 - expect(getVisibleChildren(document)).toEqual(
1742 + expect(getMeaningfulChildren(document)).toEqual(
1743 <html>
1744 <head>
1617 - <link rel="stylesheet" href="initial" data-rprec="one" />
1618 - <link rel="stylesheet" href="foo" data-rprec="one" />
1619 - <link rel="stylesheet" href="qux" data-rprec="one" />
1745 + <link rel="stylesheet" href="initial" data-precedence="one" />
1746 + <link rel="stylesheet" href="foo" data-precedence="one" />
1747 + <link rel="stylesheet" href="qux" data-precedence="one" />
1748 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1749 <link
1750 rel="stylesheet"
1751 href="bar"
1623 - data-rprec="default"
1752 + data-precedence="default"
1753 data-foo="foo"
1754 crossorigin="anonymous"
1755 />
1627 - <link rel="stylesheet" href="baz" data-rprec="two" />
1756 + <link rel="stylesheet" href="baz" data-precedence="two" />
1757 <link rel="preload" href="foo" as="style" />
1758 </head>
1759 <body>
@@ -1650,26 +1779,31 @@ describe('ReactDOMFloat', () => {
1779 const quxlink = document.querySelector(
1780 'link[rel="stylesheet"][href="qux"]',
1781 );
1782 + const presetLink = document.querySelector(
1783 + 'link[rel="stylesheet"][href="preset"]',
1784 + );
1785 const event = document.createEvent('Events');
1786 event.initEvent('load', true, true);
1787 bazlink.dispatchEvent(event);
1788 quxlink.dispatchEvent(event);
1789 + presetLink.dispatchEvent(event);
1790 });
1791
1659 - expect(getVisibleChildren(document)).toEqual(
1792 + expect(getMeaningfulChildren(document)).toEqual(
1793 <html>
1794 <head>
1662 - <link rel="stylesheet" href="initial" data-rprec="one" />
1663 - <link rel="stylesheet" href="foo" data-rprec="one" />
1664 - <link rel="stylesheet" href="qux" data-rprec="one" />
1795 + <link rel="stylesheet" href="initial" data-precedence="one" />
1796 + <link rel="stylesheet" href="foo" data-precedence="one" />
1797 + <link rel="stylesheet" href="qux" data-precedence="one" />
1798 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1799 <link
1800 rel="stylesheet"
1801 href="bar"
1668 - data-rprec="default"
1802 + data-precedence="default"
1803 data-foo="foo"
1804 crossorigin="anonymous"
1805 />
1672 - <link rel="stylesheet" href="baz" data-rprec="two" />
1806 + <link rel="stylesheet" href="baz" data-precedence="two" />
1807 <link rel="preload" href="foo" as="style" />
1808 </head>
1809 <body>
@@ -1698,20 +1832,21 @@ describe('ReactDOMFloat', () => {
1832 resolveText('unblock');
1833 });
1834
1701 - expect(getVisibleChildren(document)).toEqual(
1835 + expect(getMeaningfulChildren(document)).toEqual(
1836 <html>
1837 <head>
1704 - <link rel="stylesheet" href="initial" data-rprec="one" />
1705 - <link rel="stylesheet" href="foo" data-rprec="one" />
1706 - <link rel="stylesheet" href="qux" data-rprec="one" />
1838 + <link rel="stylesheet" href="initial" data-precedence="one" />
1839 + <link rel="stylesheet" href="foo" data-precedence="one" />
1840 + <link rel="stylesheet" href="qux" data-precedence="one" />
1841 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1842 <link
1843 rel="stylesheet"
1844 href="bar"
1710 - data-rprec="default"
1845 + data-precedence="default"
1846 data-foo="foo"
1847 crossorigin="anonymous"
1848 />
1714 - <link rel="stylesheet" href="baz" data-rprec="two" />
1849 + <link rel="stylesheet" href="baz" data-precedence="two" />
1850 <link rel="preload" href="foo" as="style" />
1851 </head>
1852 <body>
@@ -1795,30 +1930,30 @@ describe('ReactDOMFloat', () => {
1930 // early. The reason precedences are still ordered correctly between child and parent is because
1931 // the precedence ordering is determined upon first discovernig a resource rather than on hoist and
1932 // so it follows render order
1798 - expect(getVisibleChildren(document)).toEqual(
1933 + expect(getMeaningfulChildren(document)).toEqual(
1934 <html>
1935 <head>
1801 - <link rel="stylesheet" href="1one" data-rprec="one" />
1802 - <link rel="stylesheet" href="3one" data-rprec="one" />
1803 - <link rel="stylesheet" href="2one" data-rprec="one" />
1804 - <link rel="stylesheet" href="4one" data-rprec="one" />
1805 -
1806 - <link rel="stylesheet" href="1two" data-rprec="two" />
1807 - <link rel="stylesheet" href="3two" data-rprec="two" />
1808 - <link rel="stylesheet" href="2two" data-rprec="two" />
1809 - <link rel="stylesheet" href="4two" data-rprec="two" />
1810 -
1811 - <link rel="stylesheet" href="1three" data-rprec="three" />
1812 - <link rel="stylesheet" href="3three" data-rprec="three" />
1813 - <link rel="stylesheet" href="2three" data-rprec="three" />
1814 - <link rel="stylesheet" href="4three" data-rprec="three" />
1815 -
1816 - <link rel="stylesheet" href="1four" data-rprec="four" />
1817 - <link rel="stylesheet" href="3four" data-rprec="four" />
1818 - <link rel="stylesheet" href="2four" data-rprec="four" />
1819 - <link rel="stylesheet" href="4four" data-rprec="four" />
1820 -
1821 - <link rel="stylesheet" href="3five" data-rprec="five" />
1936 + <link rel="stylesheet" href="1one" data-precedence="one" />
1937 + <link rel="stylesheet" href="3one" data-precedence="one" />
1938 + <link rel="stylesheet" href="2one" data-precedence="one" />
1939 + <link rel="stylesheet" href="4one" data-precedence="one" />
1940 +
1941 + <link rel="stylesheet" href="1two" data-precedence="two" />
1942 + <link rel="stylesheet" href="3two" data-precedence="two" />
1943 + <link rel="stylesheet" href="2two" data-precedence="two" />
1944 + <link rel="stylesheet" href="4two" data-precedence="two" />
1945 +
1946 + <link rel="stylesheet" href="1three" data-precedence="three" />
1947 + <link rel="stylesheet" href="3three" data-precedence="three" />
1948 + <link rel="stylesheet" href="2three" data-precedence="three" />
1949 + <link rel="stylesheet" href="4three" data-precedence="three" />
1950 +
1951 + <link rel="stylesheet" href="1four" data-precedence="four" />
1952 + <link rel="stylesheet" href="3four" data-precedence="four" />
1953 + <link rel="stylesheet" href="2four" data-precedence="four" />
1954 + <link rel="stylesheet" href="4four" data-precedence="four" />
1955 +
1956 + <link rel="stylesheet" href="3five" data-precedence="five" />
1957 </head>
1958 <body>
1959 <div>
@@ -1851,11 +1986,11 @@ describe('ReactDOMFloat', () => {
1986 pipe(writable);
1987 });
1988
1854 - expect(getVisibleChildren(document)).toEqual(
1989 + expect(getMeaningfulChildren(document)).toEqual(
1990 <html>
1991 <head>
1857 - <link rel="stylesheet" href="foo" data-rprec="one" />
1858 - <link rel="stylesheet" href="bar" data-rprec="two" />
1992 + <link rel="stylesheet" href="foo" data-precedence="one" />
1993 + <link rel="stylesheet" href="bar" data-precedence="two" />
1994 </head>
1995 <body>
1996 <div>Hello</div>
@@ -1877,11 +2012,11 @@ describe('ReactDOMFloat', () => {
2012 </html>,
2013 );
2014 expect(Scheduler).toFlushWithoutYielding();
1880 - expect(getVisibleChildren(document)).toEqual(
2015 + expect(getMeaningfulChildren(document)).toEqual(
2016 <html>
2017 <head>
1883 - <link rel="stylesheet" href="foo" data-rprec="one" />
1884 - <link rel="stylesheet" href="bar" data-rprec="two" />
2018 + <link rel="stylesheet" href="foo" data-precedence="one" />
2019 + <link rel="stylesheet" href="bar" data-precedence="two" />
2020 </head>
2021 <body>
2022 <div>Hello</div>
@@ -1899,12 +2034,12 @@ describe('ReactDOMFloat', () => {
2034 </html>,
2035 );
2036 expect(Scheduler).toFlushWithoutYielding();
1902 - expect(getVisibleChildren(document)).toEqual(
2037 + expect(getMeaningfulChildren(document)).toEqual(
2038 <html>
2039 <head>
1905 - <link rel="stylesheet" href="foo" data-rprec="one" />
1906 - <link rel="stylesheet" href="baz" data-rprec="one" />
1907 - <link rel="stylesheet" href="bar" data-rprec="two" />
2040 + <link rel="stylesheet" href="foo" data-precedence="one" />
2041 + <link rel="stylesheet" href="baz" data-precedence="one" />
2042 + <link rel="stylesheet" href="bar" data-precedence="two" />
2043 <link rel="preload" as="style" href="baz" />
2044 </head>
2045 <body>
@@ -1944,7 +2079,7 @@ describe('ReactDOMFloat', () => {
2079 </ErrorBoundary>,
2080 );
2081 expect(Scheduler).toFlushWithoutYielding();
1947 - expect(getVisibleChildren(document)).toEqual(
2082 + expect(getMeaningfulChildren(document)).toEqual(
2083 <html>
2084 <head>
2085 <link rel="preload" href="foo" as="style" />
@@ -1986,11 +2121,11 @@ describe('ReactDOMFloat', () => {
2121 pipe(writable);
2122 });
2123
1989 - expect(getVisibleChildren(document)).toEqual(
2124 + expect(getMeaningfulChildren(document)).toEqual(
2125 <html>
2126 <head>
1992 - <link rel="stylesheet" href="foo" data-rprec="one" />
1993 - <link rel="stylesheet" href="bar" data-rprec="two" />
2127 + <link rel="stylesheet" href="foo" data-precedence="one" />
2128 + <link rel="stylesheet" href="bar" data-precedence="two" />
2129 </head>
2130 <body>
2131 <div>Hello</div>
@@ -2002,11 +2137,11 @@ describe('ReactDOMFloat', () => {
2137 await act(() => {
2138 resolveText('foo');
2139 });
2005 - expect(getVisibleChildren(document)).toEqual(
2140 + expect(getMeaningfulChildren(document)).toEqual(
2141 <html>
2142 <head>
2008 - <link rel="stylesheet" href="foo" data-rprec="one" />
2009 - <link rel="stylesheet" href="bar" data-rprec="two" />
2143 + <link rel="stylesheet" href="foo" data-precedence="one" />
2144 + <link rel="stylesheet" href="bar" data-precedence="two" />
2145 </head>
2146 <body>
2147 <div>Hello</div>
@@ -2020,11 +2155,11 @@ describe('ReactDOMFloat', () => {
2155 await act(() => {
2156 resolveText('bar');
2157 });
2023 - expect(getVisibleChildren(document)).toEqual(
2158 + expect(getMeaningfulChildren(document)).toEqual(
2159 <html>
2160 <head>
2026 - <link rel="stylesheet" href="foo" data-rprec="one" />
2027 - <link rel="stylesheet" href="bar" data-rprec="two" />
2161 + <link rel="stylesheet" href="foo" data-precedence="one" />
2162 + <link rel="stylesheet" href="bar" data-precedence="two" />
2163 </head>
2164 <body>
2165 <div>Hello</div>
@@ -2077,7 +2212,7 @@ describe('ReactDOMFloat', () => {
2212 pipe(writable);
2213 });
2214
2080 - expect(getVisibleChildren(document)).toEqual(
2215 + expect(getMeaningfulChildren(document)).toEqual(
2216 <html>
2217 <head />
2218 <body>
@@ -2089,7 +2224,7 @@ describe('ReactDOMFloat', () => {
2224 await act(() => {
2225 resolveText('bar');
2226 });
2092 - expect(getVisibleChildren(document)).toEqual(
2227 + expect(getMeaningfulChildren(document)).toEqual(
2228 <html>
2229 <head />
2230 <body>
@@ -2101,7 +2236,7 @@ describe('ReactDOMFloat', () => {
2236 await act(() => {
2237 resolveText('baz');
2238 });
2104 - expect(getVisibleChildren(document)).toEqual(
2239 + expect(getMeaningfulChildren(document)).toEqual(
2240 <html>
2241 <head />
2242 <body>
@@ -2113,12 +2248,12 @@ describe('ReactDOMFloat', () => {
2248 await act(() => {
2249 resolveText('foo');
2250 });
2116 - expect(getVisibleChildren(document)).toEqual(
2251 + expect(getMeaningfulChildren(document)).toEqual(
2252 <html>
2253 <head>
2119 - <link rel="stylesheet" href="foo" data-rprec="default" />
2120 - <link rel="stylesheet" href="bar" data-rprec="default" />
2121 - <link rel="stylesheet" href="baz" data-rprec="default" />
2254 + <link rel="stylesheet" href="foo" data-precedence="default" />
2255 + <link rel="stylesheet" href="bar" data-precedence="default" />
2256 + <link rel="stylesheet" href="baz" data-precedence="default" />
2257 </head>
2258 <body>
2259 <div>loading foo...</div>
@@ -2138,12 +2273,12 @@ describe('ReactDOMFloat', () => {
2273 },
2274 );
2275 });
2141 - expect(getVisibleChildren(document)).toEqual(
2276 + expect(getMeaningfulChildren(document)).toEqual(
2277 <html>
2278 <head>
2144 - <link rel="stylesheet" href="foo" data-rprec="default" />
2145 - <link rel="stylesheet" href="bar" data-rprec="default" />
2146 - <link rel="stylesheet" href="baz" data-rprec="default" />
2279 + <link rel="stylesheet" href="foo" data-precedence="default" />
2280 + <link rel="stylesheet" href="bar" data-precedence="default" />
2281 + <link rel="stylesheet" href="baz" data-precedence="default" />
2282 </head>
2283 <body>
2284 <div>
@@ -2212,7 +2347,7 @@ describe('ReactDOMFloat', () => {
2347 pipe(writable);
2348 });
2349
2215 - expect(getVisibleChildren(document)).toEqual(
2350 + expect(getMeaningfulChildren(document)).toEqual(
2351 <html>
2352 <head />
2353 <body>
@@ -2225,7 +2360,7 @@ describe('ReactDOMFloat', () => {
2360 await act(() => {
2361 resolveText('baz');
2362 });
2228 - expect(getVisibleChildren(document)).toEqual(
2363 + expect(getMeaningfulChildren(document)).toEqual(
2364 <html>
2365 <head />
2366 <body>
@@ -2238,7 +2373,7 @@ describe('ReactDOMFloat', () => {
2373 await act(() => {
2374 resolveText('bar');
2375 });
2241 - expect(getVisibleChildren(document)).toEqual(
2376 + expect(getMeaningfulChildren(document)).toEqual(
2377 <html>
2378 <head />
2379 <body>
@@ -2254,7 +2389,7 @@ describe('ReactDOMFloat', () => {
2389 await act(() => {
2390 resolveText('foo');
2391 });
2257 - expect(getVisibleChildren(document)).toEqual(
2392 + expect(getMeaningfulChildren(document)).toEqual(
2393 <html>
2394 <head />
2395 <body>
@@ -2271,13 +2406,13 @@ describe('ReactDOMFloat', () => {
2406 await act(() => {
2407 resolveText('qux');
2408 });
2274 - expect(getVisibleChildren(document)).toEqual(
2409 + expect(getMeaningfulChildren(document)).toEqual(
2410 <html>
2411 <head>
2277 - <link rel="stylesheet" href="foo" data-rprec="default" />
2278 - <link rel="stylesheet" href="bar" data-rprec="default" />
2279 - <link rel="stylesheet" href="baz" data-rprec="default" />
2280 - <link rel="stylesheet" href="qux" data-rprec="default" />
2412 + <link rel="stylesheet" href="foo" data-precedence="default" />
2413 + <link rel="stylesheet" href="bar" data-precedence="default" />
2414 + <link rel="stylesheet" href="baz" data-precedence="default" />
2415 + <link rel="stylesheet" href="qux" data-precedence="default" />
2416 </head>
2417 <body>
2418 <div>loading...</div>
@@ -2299,13 +2434,13 @@ describe('ReactDOMFloat', () => {
2434 },
2435 );
2436 });
2302 - expect(getVisibleChildren(document)).toEqual(
2437 + expect(getMeaningfulChildren(document)).toEqual(
2438 <html>
2439 <head>
2305 - <link rel="stylesheet" href="foo" data-rprec="default" />
2306 - <link rel="stylesheet" href="bar" data-rprec="default" />
2307 - <link rel="stylesheet" href="baz" data-rprec="default" />
2308 - <link rel="stylesheet" href="qux" data-rprec="default" />
2440 + <link rel="stylesheet" href="foo" data-precedence="default" />
2441 + <link rel="stylesheet" href="bar" data-precedence="default" />
2442 + <link rel="stylesheet" href="baz" data-precedence="default" />
2443 + <link rel="stylesheet" href="qux" data-precedence="default" />
2444 </head>
2445 <body>
2446 <div>
@@ -2401,7 +2536,7 @@ describe('ReactDOMFloat', () => {
2536 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
2537 pipe(writable);
2538 });
2404 - expect(getVisibleChildren(document)).toEqual(
2539 + expect(getMeaningfulChildren(document)).toEqual(
2540 <html>
2541 <head>
2542 <link
@@ -2452,13 +2587,13 @@ describe('ReactDOMFloat', () => {
2587 resolveText('unblock');
2588 });
2589
2455 - expect(getVisibleChildren(document)).toEqual(
2590 + expect(getMeaningfulChildren(document)).toEqual(
2591 <html>
2592 <head>
2593 <link
2594 rel="stylesheet"
2595 href="foo"
2461 - data-rprec="default"
2596 + data-precedence="default"
2597 crossorigin="anonymous"
2598 media="all"
2599 integrity="somehash"
@@ -2535,7 +2670,7 @@ describe('ReactDOMFloat', () => {
2670 );
2671 pipe(writable);
2672 });
2538 - expect(getVisibleChildren(document)).toEqual(
2673 + expect(getMeaningfulChildren(document)).toEqual(
2674 <html>
2675 <head />
2676 <body>
@@ -2549,7 +2684,7 @@ describe('ReactDOMFloat', () => {
2684 resolveText('AAAA');
2685 resolveText('AA');
2686 });
2552 - expect(getVisibleChildren(document)).toEqual(
2687 + expect(getMeaningfulChildren(document)).toEqual(
2688 <html>
2689 <head />
2690 <body>
@@ -2572,11 +2707,11 @@ describe('ReactDOMFloat', () => {
2707 l.dispatchEvent(event);
2708 });
2709 });
2575 - expect(getVisibleChildren(document)).toEqual(
2710 + expect(getMeaningfulChildren(document)).toEqual(
2711 <html>
2712 <head>
2578 - <link rel="stylesheet" href="A" data-rprec="A" />
2579 - <link rel="stylesheet" href="AA" data-rprec="AA" />
2713 + <link rel="stylesheet" href="A" data-precedence="A" />
2714 + <link rel="stylesheet" href="AA" data-precedence="AA" />
2715 </head>
2716 <body>
2717 <div>
@@ -2602,13 +2737,13 @@ describe('ReactDOMFloat', () => {
2737 l.dispatchEvent(event);
2738 });
2739 });
2605 - expect(getVisibleChildren(document)).toEqual(
2740 + expect(getMeaningfulChildren(document)).toEqual(
2741 <html>
2742 <head>
2608 - <link rel="stylesheet" href="A" data-rprec="A" />
2609 - <link rel="stylesheet" href="AA" data-rprec="AA" />
2610 - <link rel="stylesheet" href="AAA" data-rprec="AAA" />
2611 - <link rel="stylesheet" href="AAAA" data-rprec="AAAA" />
2743 + <link rel="stylesheet" href="A" data-precedence="A" />
2744 + <link rel="stylesheet" href="AA" data-precedence="AA" />
2745 + <link rel="stylesheet" href="AAA" data-precedence="AAA" />
2746 + <link rel="stylesheet" href="AAAA" data-precedence="AAAA" />
2747 </head>
2748 <body>
2749 <div>
@@ -2658,7 +2793,7 @@ describe('ReactDOMFloat', () => {
2793 );
2794 pipe(writable);
2795 });
2661 - expect(getVisibleChildren(document)).toEqual(
2796 + expect(getMeaningfulChildren(document)).toEqual(
2797 <html>
2798 <head>
2799 <link rel="preload" as="font" href="foo" crossorigin="" />
@@ -3167,8 +3302,8 @@ describe('ReactDOMFloat', () => {
3302 async () => {
3303 const expectedMessage =
3304 'Warning: ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
3170 - ' specifying the Resource type. It found %s instead. Currently, the only valid resource type for preinit is "style".' +
3171 - ' The href for the preinit call where this warning originated is "%s".%s';
3305 + ' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' +
3306 + ' and "script". The href for the preinit call where this warning originated is "%s".%s';
3307 const expectedStack = componentStack(['Preinits', 'head', 'html']);
3308 function makeArgs(...substitutions) {
3309 return [expectedMessage, ...substitutions, expectedStack];
@@ -3263,6 +3398,8 @@ describe('ReactDOMFloat', () => {
3398 precedence="bar"
3399 data-something-extra="extra"
3400 />
3401 + <script src="sfoo" async={true} data-something-extra="extra" />
3402 + <script src="sbar" async={true} data-something-extra="extra" />
3403 hello
3404 </div>,
3405 );
@@ -3282,12 +3419,14 @@ describe('ReactDOMFloat', () => {
3419 precedence="baz"
3420 data-something-new="new"
3421 />
3422 + <script src="sfoo" async={true} data-something-new="new" />
3423 + <script src="sbaz" async={true} data-something-new="new" />
3424 hello
3425 </div>,
3426 );
3427 expect(() => {
3428 expect(Scheduler).toFlushWithoutYielding();
3290 - }).toErrorDev(
3429 + }).toErrorDev([
3430 'Warning: A style Resource with href "foo" recieved new props with different values from the props used' +
3431 ' when this Resource was first rendered. React will only use the props provided when' +
3432 ' this resource was first rendered until a new href is provided. Unlike conventional' +
@@ -3297,31 +3436,42 @@ describe('ReactDOMFloat', () => {
3436 '\n data-something-extra: missing or null in latest props, "extra" in original props' +
3437 '\n data-something-new: "new" in latest props, missing or null in original props' +
3438 '\n precedence: "fu" in latest props, "foo" in original props',
3300 - );
3301 - expect(getVisibleChildren(document)).toEqual(
3439 + 'Warning: A script Resource with src "sfoo" recieved new props with different values from the props used' +
3440 + ' when this Resource was first rendered. React will only use the props provided when' +
3441 + ' this resource was first rendered until a new src is provided. Unlike conventional' +
3442 + ' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
3443 + ' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
3444 + ' (src) must have props that agree with each other. The differences are described below.' +
3445 + '\n data-something-extra: missing or null in latest props, "extra" in original props' +
3446 + '\n data-something-new: "new" in latest props, missing or null in original props',
3447 + ]);
3448 + expect(getMeaningfulChildren(document)).toEqual(
3449 <html>
3450 <head>
3451 <link
3452 rel="stylesheet"
3453 href="foo"
3307 - data-rprec="foo"
3454 + data-precedence="foo"
3455 data-something-extra="extra"
3456 />
3457 <link
3458 rel="stylesheet"
3459 href="bar"
3313 - data-rprec="bar"
3460 + data-precedence="bar"
3461 data-something-extra="extra"
3462 />
3463 <link
3464 rel="stylesheet"
3465 href="baz"
3319 - data-rprec="baz"
3466 + data-precedence="baz"
3467 data-something-new="new"
3468 />
3469 <link rel="preload" as="style" href="foo" />
3470 <link rel="preload" as="style" href="bar" />
3471 + <script src="sfoo" async="" data-something-extra="extra" />
3472 + <script src="sbar" async="" data-something-extra="extra" />
3473 <link rel="preload" as="style" href="baz" />
3474 + <script src="sbaz" async="" data-something-new="new" />
3475 </head>
3476 <body>
3477 <div id="container">
@@ -3377,13 +3527,23 @@ describe('ReactDOMFloat', () => {
3527 );
3528 pipe(writable);
3529 });
3380 - expect(getVisibleChildren(document)).toEqual(
3530 + expect(getMeaningfulChildren(document)).toEqual(
3531 <html>
3532 <head>
3383 - <link rel="stylesheet" href="foo" data-rprec="foo" media="all" />
3384 - <link rel="stylesheet" href="bar" data-rprec="bar" />
3533 + <link
3534 + rel="stylesheet"
3535 + href="foo"
3536 + data-precedence="foo"
3537 + media="all"
3538 + />
3539 + <link rel="stylesheet" href="bar" data-precedence="bar" />
3540
3386 - <link rel="stylesheet" href="baz" data-rprec="baz" media="some" />
3541 + <link
3542 + rel="stylesheet"
3543 + href="baz"
3544 + data-precedence="baz"
3545 + media="some"
3546 + />
3547 </head>
3548 <body />
3549 </html>,
@@ -3392,32 +3552,38 @@ describe('ReactDOMFloat', () => {
3552 if (__DEV__) {
3553 expect(mockError).toHaveBeenCalledTimes(3);
3554 expect(mockError).toHaveBeenCalledWith(
3395 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3555 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3556 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3397 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3557 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3558 'style Resource',
3559 + 'href',
3560 'foo',
3561 'an earlier instance of this Resource',
3562 + 'href',
3563 '\n media: missing or null in latest props, "all" in original props',
3564 componentStack(['link', 'head', 'html']),
3565 );
3566 expect(mockError).toHaveBeenCalledWith(
3405 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3567 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3568 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3407 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3569 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3570 'style Resource',
3571 + 'href',
3572 'bar',
3573 'an earlier instance of this Resource',
3574 + 'href',
3575 '\n media: "all" in latest props, missing or null in original props',
3576 componentStack(['link', 'head', 'html']),
3577 );
3578 expect(mockError).toHaveBeenCalledWith(
3415 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3579 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3580 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3417 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3581 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3582 'style Resource',
3583 + 'href',
3584 'baz',
3585 'an earlier instance of this Resource',
3586 + 'href',
3587 '\n media: "all" in latest props, "some" in original props',
3588 componentStack(['link', 'head', 'html']),
3589 );
@@ -3475,20 +3641,20 @@ describe('ReactDOMFloat', () => {
3641 );
3642 pipe(writable);
3643 });
3478 - expect(getVisibleChildren(document)).toEqual(
3644 + expect(getMeaningfulChildren(document)).toEqual(
3645 <html>
3646 <head>
3647 <link
3648 rel="stylesheet"
3649 href="foo"
3484 - data-rprec="foo"
3650 + data-precedence="foo"
3651 data-foo="an original value"
3652 />
3487 - <link rel="stylesheet" href="bar" data-rprec="bar" />
3653 + <link rel="stylesheet" href="bar" data-precedence="bar" />
3654 <link
3655 rel="stylesheet"
3656 href="baz"
3491 - data-rprec="baz"
3657 + data-precedence="baz"
3658 data-foo="an original value"
3659 />
3660 </head>
@@ -3499,32 +3665,38 @@ describe('ReactDOMFloat', () => {
3665 if (__DEV__) {
3666 expect(mockError).toHaveBeenCalledTimes(3);
3667 expect(mockError).toHaveBeenCalledWith(
3502 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3668 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3669 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3504 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3670 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3671 'style Resource',
3672 + 'href',
3673 'foo',
3674 'an earlier instance of this Resource',
3675 + 'href',
3676 '\n precedence: "foonew" in latest props, "foo" in original props',
3677 componentStack(['link', 'head', 'html']),
3678 );
3679 expect(mockError).toHaveBeenCalledWith(
3512 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3680 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3681 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3514 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3682 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3683 'style Resource',
3684 + 'href',
3685 'bar',
3686 'an earlier instance of this Resource',
3687 + 'href',
3688 '\n data-foo: "a new value" in latest props, missing or null in original props',
3689 componentStack(['link', 'head', 'html']),
3690 );
3691 expect(mockError).toHaveBeenCalledWith(
3522 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3692 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3693 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3524 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3694 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3695 'style Resource',
3696 + 'href',
3697 'baz',
3698 'an earlier instance of this Resource',
3699 + 'href',
3700 '\n data-foo: "a new value" in latest props, "an original value" in original props',
3701 componentStack(['link', 'head', 'html']),
3702 );
@@ -3581,7 +3753,7 @@ describe('ReactDOMFloat', () => {
3753 });
3754 // precedence is removed from the stylesheets because it is considered a reserved prop for
3755 // stylesheets to opt into resource semantics.
3584 - expect(getVisibleChildren(document)).toEqual(
3756 + expect(getMeaningfulChildren(document)).toEqual(
3757 <html>
3758 <head>
3759 <link rel="preload" as="style" href="foo" />
@@ -3647,6 +3819,56 @@ describe('ReactDOMFloat', () => {
3819 }
3820 });
3821
3822 + // @gate enableFloat
3823 + it('warns when script Resources have new or different values for props', async () => {
3824 + const originalConsoleError = console.error;
3825 + const mockError = jest.fn();
3826 + console.error = (...args) => {
3827 + mockError(...args.map(normalizeCodeLocInfo));
3828 + };
3829 + try {
3830 + await actIntoEmptyDocument(() => {
3831 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3832 + <html>
3833 + <head>
3834 + <script src="foo" async={true} data-foo="a current value" />
3835 + <script src="foo" async={true} data-foo="a new value" />
3836 + </head>
3837 + </html>,
3838 + );
3839 + pipe(writable);
3840 + });
3841 + expect(getMeaningfulChildren(document)).toEqual(
3842 + <html>
3843 + <head>
3844 + <script src="foo" async="" data-foo="a current value" />
3845 + </head>
3846 + <body />
3847 + </html>,
3848 + );
3849 +
3850 + if (__DEV__) {
3851 + expect(mockError).toHaveBeenCalledTimes(1);
3852 + expect(mockError).toHaveBeenCalledWith(
3853 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3854 + ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3855 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3856 + 'script Resource',
3857 + 'src',
3858 + 'foo',
3859 + 'an earlier instance of this Resource',
3860 + 'src',
3861 + '\n data-foo: "a new value" in latest props, "a current value" in original props',
3862 + componentStack(['script', 'head', 'html']),
3863 + );
3864 + } else {
3865 + expect(mockError).not.toHaveBeenCalled();
3866 + }
3867 + } finally {
3868 + console.error = originalConsoleError;
3869 + }
3870 + });
3871 +
3872 // @gate enableFloat
3873 it('warns when preload Resources have new or different values for props', async () => {
3874 const originalConsoleError = console.error;
@@ -3690,7 +3912,7 @@ describe('ReactDOMFloat', () => {
3912 );
3913 pipe(writable);
3914 });
3693 - expect(getVisibleChildren(document)).toEqual(
3915 + expect(getMeaningfulChildren(document)).toEqual(
3916 <html>
3917 <head>
3918 <link
@@ -3713,12 +3935,14 @@ describe('ReactDOMFloat', () => {
3935 if (__DEV__) {
3936 expect(mockError).toHaveBeenCalledTimes(2);
3937 expect(mockError).toHaveBeenCalledWith(
3716 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
3938 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
3939 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3718 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
3940 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
3941 'preload Resource (as "style")',
3942 + 'href',
3943 'foo',
3944 'an earlier instance of this Resource',
3945 + 'href',
3946 '\n data-foo: "a new value" in latest props, "a current value" in original props',
3947 componentStack(['link', 'head', 'html']),
3948 );
@@ -3769,13 +3993,13 @@ describe('ReactDOMFloat', () => {
3993 );
3994 pipe(writable);
3995 });
3772 - expect(getVisibleChildren(document)).toEqual(
3996 + expect(getMeaningfulChildren(document)).toEqual(
3997 <html>
3998 <head>
3999 <link
4000 rel="stylesheet"
4001 href="foo"
3778 - data-rprec="foo"
4002 + data-precedence="foo"
4003 crossorigin="style value"
4004 />
4005 </head>
@@ -3786,12 +4010,14 @@ describe('ReactDOMFloat', () => {
4010 if (__DEV__) {
4011 expect(mockError).toHaveBeenCalledTimes(1);
4012 expect(mockError).toHaveBeenCalledWith(
3789 - 'Warning: A %s with href "%s" has props that disagree with those found on %s. Resources always use the props' +
4013 + 'Warning: A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
4014 ' that were provided the first time they are encountered so any differences will be ignored. Please' +
3791 - ' update Resources that share an href to have props that agree. The differences are described below.%s%s',
4015 + ' update Resources that share an %s to have props that agree. The differences are described below.%s%s',
4016 'style Resource',
4017 + 'href',
4018 'foo',
4019 'a preload Resource (as "style") with the same href',
4020 + 'href',
4021 '\n crossOrigin: "style value" in latest props, "preload value" in original props',
4022 componentStack(['link', 'head', 'html']),
4023 );
@@ -3839,18 +4065,22 @@ describe('ReactDOMFloat', () => {
4065 </div>,
4066 );
4067 expect(Scheduler).toFlushWithoutYielding();
3842 - expect(getVisibleChildren(document)).toEqual(
4068 + expect(getMeaningfulChildren(document)).toEqual(
4069 <html>
4070 <head>
3845 - <link rel="preload" as="style" href="preload" />
3846 - <link rel="preload" href={'with\nnewline'} as="style" />
3847 - <link rel="stylesheet" href="style" data-rprec="style" />
3848 - <link rel="stylesheet" href="with\slashes" data-rprec="style" />
4071 + <link rel="stylesheet" href="style" data-precedence="style" />
4072 + <link
4073 + rel="stylesheet"
4074 + href="with\slashes"
4075 + data-precedence="style"
4076 + />
4077 <link
4078 rel="stylesheet"
4079 href={'style"][rel="stylesheet'}
3852 - data-rprec="style"
4080 + data-precedence="style"
4081 />
4082 + <link rel="preload" as="style" href="preload" />
4083 + <link rel="preload" href={'with\nnewline'} as="style" />
4084 <link rel="preload" href={'preload"][rel="preload'} as="style" />
4085 <link rel="preload" href={'style"][rel="stylesheet'} as="style" />
4086 </head>
@@ -3899,18 +4129,22 @@ describe('ReactDOMFloat', () => {
4129 const root = ReactDOMClient.createRoot(container);
4130 root.render(<App />);
4131 expect(Scheduler).toFlushWithoutYielding();
3902 - expect(getVisibleChildren(document)).toEqual(
4132 + expect(getMeaningfulChildren(document)).toEqual(
4133 <html>
4134 <head>
3905 - <link rel="preload" as="style" href="preload" />
3906 - <link rel="preload" href={'with\nnewline'} as="style" />
3907 - <link rel="stylesheet" href="style" data-rprec="style" />
3908 - <link rel="stylesheet" href="with\slashes" data-rprec="style" />
4135 + <link rel="stylesheet" href="style" data-precedence="style" />
4136 + <link
4137 + rel="stylesheet"
4138 + href="with\slashes"
4139 + data-precedence="style"
4140 + />
4141 <link
4142 rel="stylesheet"
4143 href={'style"][rel="stylesheet'}
3912 - data-rprec="style"
4144 + data-precedence="style"
4145 />
4146 + <link rel="preload" as="style" href="preload" />
4147 + <link rel="preload" href={'with\nnewline'} as="style" />
4148 <link rel="preload" href={'preload"][rel="preload'} as="style" />
4149 </head>
4150 <body>
packages/react-dom/src/__tests__/react-dom-server-rendering-stub-test.js
+1 -1
@@ -50,7 +50,7 @@ describe('react-dom-server-rendering-stub', () => {
50 }
51 const html = ReactDOMFizzServer.renderToString(<App />);
52 expect(html).toEqual(
53 - '<link href="foo" rel="preload" as="style"/><link rel="stylesheet" href="bar" data-rprec="default"/><div>foo</div>',
53 + '<link rel="stylesheet" href="bar" data-precedence="default"/><link href="foo" rel="preload" as="style"/><div>foo</div>',
54 );
55 });
56
packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+1
@@ -21,6 +21,7 @@ function shim(...args: any): empty {
21 // Hydration (when unsupported)
22 export type SuspenseInstance = mixed;
23 export const supportsHydration = false;
24 +export const isHydratable = shim;
25 export const canHydrateInstance = shim;
26 export const canHydrateTextInstance = shim;
27 export const canHydrateSuspenseInstance = shim;
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+9 -1
@@ -35,7 +35,7 @@ import {
35 NoFlags,
36 DidCapture,
37 } from './ReactFiberFlags';
38 -import {enableHostSingletons} from 'shared/ReactFeatureFlags';
38 +import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
39
40 import {
41 createFiberFromHostInstanceForDeletion,
@@ -45,6 +45,7 @@ import {
45 shouldSetTextContent,
46 supportsHydration,
47 supportsSingletons,
48 + isHydratable,
49 canHydrateInstance,
50 canHydrateTextInstance,
51 canHydrateSuspenseInstance,
@@ -438,6 +439,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
439 if (!isHydrating) {
440 return;
441 }
442 + if (enableFloat && !isHydratable(fiber.type, fiber.pendingProps)) {
443 + // This fiber never hydrates from the DOM and always does an insert
444 + fiber.flags = (fiber.flags & ~Hydrating) | Placement;
445 + isHydrating = false;
446 + hydrationParentFiber = fiber;
447 + return;
448 + }
449 let nextInstance = nextHydratableInstance;
450 if (!nextInstance) {
451 if (shouldClientRenderOnMismatch(fiber)) {
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+9 -1
@@ -35,7 +35,7 @@ import {
35 NoFlags,
36 DidCapture,
37 } from './ReactFiberFlags';
38 -import {enableHostSingletons} from 'shared/ReactFeatureFlags';
38 +import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
39
40 import {
41 createFiberFromHostInstanceForDeletion,
@@ -45,6 +45,7 @@ import {
45 shouldSetTextContent,
46 supportsHydration,
47 supportsSingletons,
48 + isHydratable,
49 canHydrateInstance,
50 canHydrateTextInstance,
51 canHydrateSuspenseInstance,
@@ -438,6 +439,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
439 if (!isHydrating) {
440 return;
441 }
442 + if (enableFloat && !isHydratable(fiber.type, fiber.pendingProps)) {
443 + // This fiber never hydrates from the DOM and always does an insert
444 + fiber.flags = (fiber.flags & ~Hydrating) | Placement;
445 + isHydrating = false;
446 + hydrationParentFiber = fiber;
447 + return;
448 + }
449 let nextInstance = nextHydratableInstance;
450 if (!nextInstance) {
451 if (shouldClientRenderOnMismatch(fiber)) {
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -130,6 +130,7 @@ export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
130 // Hydration
131 // (optional)
132 // -------------------
133 +export const isHydratable = $$$hostConfig.isHydratable;
134 export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
135 export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
136 export const canHydrateSuspenseInstance =
scripts/rollup/generate-inline-fizz-runtime.js
+1 -1
@@ -72,7 +72,7 @@ async function main() {
72 // couldn't figure it out. Good enough for now. This only affects the inline
73 // Fizz runtime, and should break immediately if there were a mistake, so I'm
74 // not too worried about it.
75 - outputCode = outputCode.replaceAll(
75 + outputCode = outputCode.replace(
76 /window\.(\$[A-z0-9_]*)/g,
77 (_, variableName) => variableName
78 );