@samitouri / QOS-React-2 / commits / 6396b66411

Model Float on Hoistables semantics (#26106)

## Hoistables In the original implementation of Float, all hoisted elements were treated like Resources. They had deduplication semantics and hydrated based on a key. This made certain kinds of hoists very challenging such as sequences of meta tags for `og:image:...` metadata. The reason is each tag along is not dedupable based on only it's intrinsic properties. two identical tags may need to be included and hoisted together with preceding meta tags that describe a semantic object with a linear set of html nodes. It was clear that the concept of Browser Resources (stylesheets / scripts / preloads) did not extend universally to all hositable tags (title, meta, other links, etc...) Additionally while Resources benefit from deduping they suffer an inability to update because while we may have multiple rendered elements that refer to a single Resource it isn't unambiguous which element owns the props on the underlying resource. We could try merging props, but that is still really hard to reason about for authors. Instead we restrict Resource semantics to freezing the props at the time the Resource is first constructed and warn if you attempt to render the same Resource with different props via another rendered element or by updating an existing element for that Resource. This lack of updating restriction is however way more extreme than necessary for instances that get hoisted but otherwise do not dedupe; where there is a well defined DOM instance for each rendered element. We should be able to update props on these instances. Hoistable is a generalization of what Float tries to model for hoisting. Instead of assuming every hoistable element is a Resource we now have two distinct categories, hoistable elements and hoistable resources. As one might guess the former has semantics that match regular Host Components except the placement of the node is usually in the <head>. The latter continues to behave how the original implementation of HostResource behaved with the first iteration of Float ### Hoistable Element On the server hoistable elements render just like regular tags except the output is stored in special queues that can be emitted in the stream earlier than they otherwise would be if rendered in place. This also allow for instance the ability to render a hoistable before even rendering the <html> tag because the queues for hoistable elements won't flush until after we have flushed the preamble (`<DOCTYPE html><html><head>`). On the client, hoistable elements largely operate like HostComponents. The most notable difference is in the hydration strategy. If we are hydrating and encounter a hoistable element we will look for all tags in the document that could potentially be a match and we check whether the attributes match the props for this particular instance. We also do this in the commit phase rather than the render phase. The reason hydration can be done for HostComponents in render is the instance will be removed from the document if hydration fails so mutating it in render is safe. For hoistables the nodes are not in a hydration boundary (Root or SuspenseBoundary at time of writing) and thus if hydration fails and we may have an instance marked as bound to some Fiber when that Fiber never commits. Moving the hydration matching to commit ensures we will always succeed in pairing the hoisted DOM instance with a Fiber that has committed. ### Hoistable Resource On the server and client the semantics of Resources are largely the same they just don't apply to title, meta, and most link tags anymore. Resources hoist and dedupe via an `href` key and are ref counted. In a future update we will add a garbage collector so we can clean up Resources that no longer have any references ## `<style>` support In earlier implementations there was no support for <style> tags. This PR adds support for treating `<style href="..." precedence="...">...</style>` as a Resource analagous to `<link rel="stylesheet" href="..." precedence="..." />` It may seem odd at first to require an href to get Resource semantics for a style tag. The rationale is that these are for inlining of actual external stylesheets as an optimization and for URI like scoping of inline styles for css-in-js libraries. The href indicates that the key space for `<style>` and `<link rel="stylesheet" />` Resources is shared. and the precedence is there to allow for interleaving of both kinds of Style resources. This is an advanced feature that we do not expect most app developers to use directly but will be quite handy for various styling libraries and for folks who want to inline as much as possible once Fizz supports this feature. ## refactor notes * HostResource Fiber type is renamed HostHoistable to reflect the generalization of the concept * The Resource object representation is modified to reduce hidden class checks and to use less memory overall * The thing that distinguishes a resource from an element is whether the Fiber has a memoizedState. If it does, it will use resource semantics, otherwise element semantics * The time complexity of matching hositable elements for hydration should be improved

Josh Story committed Feb 9, 2023 at 22:59 UTC 6396b664118442f3c2eae7bf13732fcb27bda98f
45 files changed +7195 -8300
packages/react-devtools-shared/src/backend/renderer.js
+9 -9
@@ -223,7 +223,7 @@ export function getInternalReactConstants(version: string): {
223 HostComponent: 5,
224 HostPortal: 4,
225 HostRoot: 3,
226 - HostResource: 26, // In reality, 18.2+. But doesn't hurt to include it here
226 + HostHoistable: 26, // In reality, 18.2+. But doesn't hurt to include it here
227 HostSingleton: 27, // Same as above
228 HostText: 6,
229 IncompleteClassComponent: 17,
@@ -257,7 +257,7 @@ export function getInternalReactConstants(version: string): {
257 HostComponent: 5,
258 HostPortal: 4,
259 HostRoot: 3,
260 - HostResource: -1, // Doesn't exist yet
260 + HostHoistable: -1, // Doesn't exist yet
261 HostSingleton: -1, // Doesn't exist yet
262 HostText: 6,
263 IncompleteClassComponent: 17,
@@ -290,7 +290,7 @@ export function getInternalReactConstants(version: string): {
290 HostComponent: 5,
291 HostPortal: 4,
292 HostRoot: 3,
293 - HostResource: -1, // Doesn't exist yet
293 + HostHoistable: -1, // Doesn't exist yet
294 HostSingleton: -1, // Doesn't exist yet
295 HostText: 6,
296 IncompleteClassComponent: 17,
@@ -323,7 +323,7 @@ export function getInternalReactConstants(version: string): {
323 HostComponent: 7,
324 HostPortal: 6,
325 HostRoot: 5,
326 - HostResource: -1, // Doesn't exist yet
326 + HostHoistable: -1, // Doesn't exist yet
327 HostSingleton: -1, // Doesn't exist yet
328 HostText: 8,
329 IncompleteClassComponent: -1, // Doesn't exist yet
@@ -356,7 +356,7 @@ export function getInternalReactConstants(version: string): {
356 HostComponent: 5,
357 HostPortal: 4,
358 HostRoot: 3,
359 - HostResource: -1, // Doesn't exist yet
359 + HostHoistable: -1, // Doesn't exist yet
360 HostSingleton: -1, // Doesn't exist yet
361 HostText: 6,
362 IncompleteClassComponent: -1, // Doesn't exist yet
@@ -397,7 +397,7 @@ export function getInternalReactConstants(version: string): {
397 IndeterminateComponent,
398 ForwardRef,
399 HostRoot,
400 - HostResource,
400 + HostHoistable,
401 HostSingleton,
402 HostComponent,
403 HostPortal,
@@ -465,7 +465,7 @@ export function getInternalReactConstants(version: string): {
465 return null;
466 case HostComponent:
467 case HostSingleton:
468 - case HostResource:
468 + case HostHoistable:
469 return type;
470 case HostPortal:
471 case HostText:
@@ -591,7 +591,7 @@ export function attach(
591 Fragment,
592 FunctionComponent,
593 HostRoot,
594 - HostResource,
594 + HostHoistable,
595 HostSingleton,
596 HostPortal,
597 HostComponent,
@@ -1032,7 +1032,7 @@ export function attach(
1032 case HostRoot:
1033 return ElementTypeRoot;
1034 case HostComponent:
1035 - case HostResource:
1035 + case HostHoistable:
1036 case HostSingleton:
1037 return ElementTypeHostComponent;
1038 case HostPortal:
packages/react-devtools-shared/src/backend/types.js
+1 -1
@@ -40,7 +40,7 @@ export type WorkTagMap = {
40 HostComponent: WorkTag,
41 HostPortal: WorkTag,
42 HostRoot: WorkTag,
43 - HostResource: WorkTag,
43 + HostHoistable: WorkTag,
44 HostSingleton: WorkTag,
45 HostText: WorkTag,
46 IncompleteClassComponent: WorkTag,
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js
+1 -1
@@ -36,7 +36,7 @@ export default function InspectedElementStateTree({
36 }: Props): React.Node {
37 const {state, type} = inspectedElement;
38
39 - // HostSingleton and HostResource may have state that we don't want to expose to users
39 + // HostSingleton and HostHoistable may have state that we don't want to expose to users
40 const isHostComponent = type === ElementTypeHostComponent;
41
42 const entries = state != null ? Object.entries(state) : null;
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+7 -9
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {FloatRoot, RootResources} from './ReactDOMFloatClient';
10 +import type {HoistableRoot, RootResources} from './ReactDOMFloatClient';
11 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 import type {ReactScopeInstance} from 'shared/ReactTypes';
13 import type {
@@ -24,7 +24,7 @@ import type {
24
25 import {
26 HostComponent,
27 - HostResource,
27 + HostHoistable,
28 HostSingleton,
29 HostText,
30 HostRoot,
@@ -178,7 +178,7 @@ export function getInstanceFromNode(node: Node): Fiber | null {
178 tag === HostComponent ||
179 tag === HostText ||
180 tag === SuspenseComponent ||
181 - (enableFloat ? tag === HostResource : false) ||
181 + (enableFloat ? tag === HostHoistable : false) ||
182 (enableHostSingletons ? tag === HostSingleton : false) ||
183 tag === HostRoot
184 ) {
@@ -198,7 +198,7 @@ export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
198 const tag = inst.tag;
199 if (
200 tag === HostComponent ||
201 - (enableFloat ? tag === HostResource : false) ||
201 + (enableFloat ? tag === HostHoistable : false) ||
202 (enableHostSingletons ? tag === HostSingleton : false) ||
203 tag === HostText
204 ) {
@@ -277,14 +277,12 @@ export function doesTargetHaveEventHandle(
277 return eventHandles.has(eventHandle);
278 }
279
280 -export function getResourcesFromRoot(root: FloatRoot): RootResources {
280 +export function getResourcesFromRoot(root: HoistableRoot): RootResources {
281 let resources = (root: any)[internalRootNodeResourcesKey];
282 if (!resources) {
283 resources = (root: any)[internalRootNodeResourcesKey] = {
284 - styles: new Map(),
285 - scripts: new Map(),
286 - head: new Map(),
287 - lastStructuredMeta: new Map(),
284 + hoistableStyles: new Map(),
285 + hoistableScripts: new Map(),
286 };
287 }
288 return resources;
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+626 -1034
@@ -9,23 +9,24 @@
9
10 import type {Instance, Container} from './ReactDOMHostConfig';
11
12 +import {isAttributeNameSafe} from '../shared/DOMProperty';
13 +import {precacheFiberNode} from './ReactDOMComponentTree';
14 +
15 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
16 const {Dispatcher} = ReactDOMSharedInternals;
17 import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
18 import {
16 - warnOnMissingHrefAndRel,
17 - validatePreloadResourceDifference,
18 - validateURLKeyedUpdatedProps,
19 - validateStyleResourceDifference,
20 - validateScriptResourceDifference,
21 - validateLinkPropsForStyleResource,
22 - validateLinkPropsForPreloadResource,
19 validatePreloadArguments,
20 validatePreinitArguments,
21 } from '../shared/ReactDOMResourceValidation';
22 import {createElement, setInitialProperties} from './ReactDOMComponent';
23 +import {
24 + checkAttributeStringCoercion,
25 + checkPropStringCoercion,
26 +} from 'shared/CheckStringCoercion';
27 import {
28 getResourcesFromRoot,
29 + isMarkedResource,
30 markNodeAsResource,
31 } from './ReactDOMComponentTree';
32 import {HTML_NAMESPACE, SVG_NAMESPACE} from '../shared/DOMNamespaces';
@@ -35,128 +36,48 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo
36 // In the future this may need to change, especially when modules / scripts are supported
37 type ResourceType = 'style' | 'font' | 'script';
38
38 -type PreloadProps = {
39 - rel: 'preload',
40 - href: string,
41 - [string]: mixed,
39 +type HoistableTagType = 'link' | 'meta' | 'title' | 'script' | 'style';
40 +type TResource<T: 'stylesheet' | 'style' | 'script' | 'void'> = {
41 + type: T,
42 + instance: null | Instance,
43 + count: number,
44 };
43 -type PreloadResource = {
44 - type: 'preload',
45 - href: string,
46 - ownerDocument: Document,
47 - props: PreloadProps,
48 - instance: Element,
45 +type StylesheetResource = TResource<'stylesheet'>;
46 +type StyleTagResource = TResource<'style'>;
47 +type StyleResource = StyleTagResource | StylesheetResource;
48 +type ScriptResource = TResource<'script'>;
49 +type VoidResource = TResource<'void'>;
50 +type Resource = StyleResource | ScriptResource | VoidResource;
51 +
52 +type StyleTagProps = {
53 + 'data-href': string,
54 + 'data-precedence': string,
55 + [string]: mixed,
56 };
50 -
51 -type StyleProps = {
57 +type StylesheetProps = {
58 rel: 'stylesheet',
59 href: string,
60 'data-precedence': string,
61 [string]: mixed,
62 };
57 -type StyleResource = {
58 - type: 'style',
59 -
60 - // Ref count for resource
61 - count: number,
63
63 - // Resource Descriptors
64 - href: string,
65 - precedence: string,
66 - props: StyleProps,
67 -
68 - // Related Resources
69 - hint: ?PreloadResource,
70 -
71 - // Insertion
72 - preloaded: boolean,
73 - loaded: boolean,
74 - error: mixed,
75 - instance: ?Element,
76 - root: FloatRoot,
77 -};
64 type ScriptProps = {
65 src: string,
66 + async: true,
67 [string]: mixed,
68 };
82 -type ScriptResource = {
83 - type: 'script',
84 - src: string,
85 - props: ScriptProps,
86 -
87 - instance: ?Element,
88 - root: FloatRoot,
89 -};
90 -
91 -type TitleProps = {
92 - [string]: mixed,
93 -};
94 -type TitleResource = {
95 - type: 'title',
96 - props: TitleProps,
97 -
98 - count: number,
99 - instance: ?Element,
100 - root: Document,
101 -};
102 -
103 -type MetaProps = {
104 - [string]: mixed,
105 -};
106 -type MetaResource = {
107 - type: 'meta',
108 - matcher: string,
109 - property: ?string,
110 - parentResource: ?MetaResource,
111 - props: MetaProps,
112 -
113 - count: number,
114 - instance: ?Element,
115 - root: Document,
116 -};
69
118 -type LinkProps = {
70 +type PreloadProps = {
71 + rel: 'preload',
72 href: string,
120 - rel: string,
73 [string]: mixed,
74 };
123 -type LinkResource = {
124 - type: 'link',
125 - props: LinkProps,
126 -
127 - count: number,
128 - instance: ?Element,
129 - root: Document,
130 -};
131 -
132 -type BaseResource = {
133 - type: 'base',
134 - matcher: string,
135 - props: Props,
136 -
137 - count: number,
138 - instance: ?Element,
139 - root: Document,
140 -};
141 -
142 -type Props = {[string]: mixed};
143 -
144 -type HeadResource = TitleResource | MetaResource | LinkResource | BaseResource;
145 -type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
75
76 export type RootResources = {
148 - styles: Map<string, StyleResource>,
149 - scripts: Map<string, ScriptResource>,
150 - head: Map<string, HeadResource>,
151 - lastStructuredMeta: Map<string, MetaResource>,
77 + hoistableStyles: Map<string, StyleResource>,
78 + hoistableScripts: Map<string, ScriptResource>,
79 };
80
154 -// Brief on purpose due to insertion by script when streaming late boundaries
155 -// s = Status
156 -// l = loaded
157 -// e = errored
158 -type StyleResourceLoadingState = Promise<mixed> & {s?: 'l' | 'e'};
159 -
81 // It is valid to preload even when we aren't actively rendering. For cases where Float functions are
82 // called when there is no rendering we track the last used document. It is not safe to insert
83 // arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
@@ -184,13 +105,13 @@ export function cleanupAfterRenderResources() {
105 // from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
106 export const ReactDOMClientDispatcher = {preload, preinit};
107
187 -export type FloatRoot = Document | ShadowRoot;
108 +export type HoistableRoot = Document | ShadowRoot;
109
110 // global maps of Resources
190 -const preloadResources: Map<string, PreloadResource> = new Map();
111 +const preloadPropsMap: Map<string, PreloadProps> = new Map();
112
113 // getRootNode is missing from IE and old jsdom versions
193 -function getRootNode(container: Container): FloatRoot {
114 +function getRootNode(container: Container): HoistableRoot {
115 // $FlowFixMe[method-unbinding]
116 return typeof container.getRootNode === 'function'
117 ? /* $FlowFixMe[incompatible-return] Flow types this as returning a `Node`,
@@ -199,33 +120,11 @@ function getRootNode(container: Container): FloatRoot {
120 : container.ownerDocument;
121 }
122
202 -function getCurrentResourceRoot(): null | FloatRoot {
123 +function getCurrentResourceRoot(): null | HoistableRoot {
124 const currentContainer = getCurrentRootHostContainer();
125 return currentContainer ? getRootNode(currentContainer) : null;
126 }
127
207 -// This resource type constraint can be loosened. It really is everything except PreloadResource
208 -// because that is the only one that does not have an optional instance type. Expand as needed.
209 -function resetInstance(resource: ScriptResource | HeadResource) {
210 - resource.instance = undefined;
211 -}
212 -
213 -export function clearRootResources(rootContainer: Container): void {
214 - const rootNode = getRootNode(rootContainer);
215 - const resources = getResourcesFromRoot(rootNode);
216 -
217 - // We can't actually delete the resource cache because this function is called
218 - // during commit after we have rendered. Instead we detatch any instances from
219 - // the Resource object if they are going to be cleared
220 -
221 - // Styles stay put
222 - // Scripts get reset
223 - resources.scripts.forEach(resetInstance);
224 - // Head Resources get reset
225 - resources.head.forEach(resetInstance);
226 - // lastStructuredMeta stays put
227 -}
228 -
128 // Preloads are somewhat special. Even if we don't have the Document
129 // used by the root that is rendering a component trying to insert a preload
130 // we can still seed the file cache by doing the preload on any document we have
@@ -245,10 +144,15 @@ function getDocumentForPreloads(): ?Document {
144 }
145 }
146
248 -function getDocumentFromRoot(root: FloatRoot): Document {
147 +function getDocumentFromRoot(root: HoistableRoot): Document {
148 return root.ownerDocument || root;
149 }
150
151 +export function getHoistableRoot(container: Container): HoistableRoot {
152 + // Flow thinks getRootNode returns Node but we know it is actualy either a Document or ShadowRoot
153 + return ((container.getRootNode(): any): Document | ShadowRoot);
154 +}
155 +
156 // --------------------------------------
157 // ReactDOM.Preload
158 // --------------------------------------
@@ -267,22 +171,33 @@ function preload(href: string, options: PreloadOptions) {
171 ownerDocument
172 ) {
173 const as = options.as;
270 - const resource = preloadResources.get(href);
271 - if (resource) {
272 - if (__DEV__) {
273 - const originallyImplicit =
274 - (resource: any)._dev_implicit_construction === true;
275 - const latestProps = preloadPropsFromPreloadOptions(href, as, options);
276 - validatePreloadResourceDifference(
277 - resource.props,
278 - originallyImplicit,
279 - latestProps,
280 - false,
174 + const limitedEscapedHref =
175 + escapeSelectorAttributeValueInsideDoubleQuotes(href);
176 + const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
177 + let key = preloadKey;
178 + switch (as) {
179 + case 'style':
180 + key = getStyleKey(href);
181 + break;
182 + case 'script':
183 + key = getScriptKey(href);
184 + break;
185 + }
186 + if (!preloadPropsMap.has(key)) {
187 + const preloadProps = preloadPropsFromPreloadOptions(href, as, options);
188 + preloadPropsMap.set(key, preloadProps);
189 +
190 + if (null === ownerDocument.querySelector(preloadKey)) {
191 + const preloadInstance = createElement(
192 + 'link',
193 + preloadProps,
194 + ownerDocument,
195 + HTML_NAMESPACE,
196 );
197 + setInitialProperties(preloadInstance, 'link', preloadProps);
198 + markNodeAsResource(preloadInstance);
199 + (ownerDocument.head: any).appendChild(preloadInstance);
200 }
283 - } else {
284 - const resourceProps = preloadPropsFromPreloadOptions(href, as, options);
285 - createPreloadResource(ownerDocument, href, resourceProps);
201 }
202 }
203 }
@@ -326,20 +241,45 @@ function preinit(href: string, options: PreinitOptions) {
241 const resourceRoot = getCurrentResourceRoot();
242 const as = options.as;
243 if (!resourceRoot) {
329 - // We are going to emit a preload as a best effort fallback since this preinit
330 - // was called outside of a render. Given the passive nature of this fallback
331 - // we do not warn in dev when props disagree if there happens to already be a
332 - // matching preload with this href
333 - const preloadDocument = getDocumentForPreloads();
334 - if (preloadDocument) {
335 - const preloadResource = preloadResources.get(href);
336 - if (!preloadResource) {
337 - const preloadProps = preloadPropsFromPreinitOptions(
338 - href,
339 - as,
340 - options,
341 - );
342 - createPreloadResource(preloadDocument, href, preloadProps);
244 + if (as === 'style' || as === 'script') {
245 + // We are going to emit a preload as a best effort fallback since this preinit
246 + // was called outside of a render. Given the passive nature of this fallback
247 + // we do not warn in dev when props disagree if there happens to already be a
248 + // matching preload with this href
249 + const preloadDocument = getDocumentForPreloads();
250 + if (preloadDocument) {
251 + const limitedEscapedHref =
252 + escapeSelectorAttributeValueInsideDoubleQuotes(href);
253 + const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
254 + let key = preloadKey;
255 + switch (as) {
256 + case 'style':
257 + key = getStyleKey(href);
258 + break;
259 + case 'script':
260 + key = getScriptKey(href);
261 + break;
262 + }
263 + if (!preloadPropsMap.has(key)) {
264 + const preloadProps = preloadPropsFromPreinitOptions(
265 + href,
266 + as,
267 + options,
268 + );
269 + preloadPropsMap.set(key, preloadProps);
270 +
271 + if (null === preloadDocument.querySelector(preloadKey)) {
272 + const preloadInstance = createElement(
273 + 'link',
274 + preloadProps,
275 + preloadDocument,
276 + HTML_NAMESPACE,
277 + );
278 + setInitialProperties(preloadInstance, 'link', preloadProps);
279 + markNodeAsResource(preloadInstance);
280 + (preloadDocument.head: any).appendChild(preloadInstance);
281 + }
282 + }
283 }
284 }
285 return;
@@ -347,54 +287,98 @@ function preinit(href: string, options: PreinitOptions) {
287
288 switch (as) {
289 case 'style': {
350 - const styleResources = getResourcesFromRoot(resourceRoot).styles;
290 + const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
291 +
292 + const key = getStyleKey(href);
293 const precedence = options.precedence || 'default';
352 - let resource = styleResources.get(href);
294 +
295 + // Check if this resource already exists
296 + let resource = styles.get(key);
297 if (resource) {
354 - if (__DEV__) {
355 - const latestProps = stylePropsFromPreinitOptions(
356 - href,
357 - precedence,
358 - options,
359 - );
360 - validateStyleResourceDifference(resource.props, latestProps);
361 - }
362 - } else {
363 - const resourceProps = stylePropsFromPreinitOptions(
298 + // We can early return. The resource exists and there is nothing
299 + // more to do
300 + return;
301 + }
302 +
303 + // Attempt to hydrate instance from DOM
304 + let instance: null | Instance = resourceRoot.querySelector(
305 + getStylesheetSelectorFromKey(key),
306 + );
307 + if (!instance) {
308 + // Construct a new instance and insert it
309 + const stylesheetProps = stylesheetPropsFromPreinitOptions(
310 href,
311 precedence,
312 options,
313 );
368 - resource = createStyleResource(
369 - styleResources,
314 + const preloadProps = preloadPropsMap.get(key);
315 + if (preloadProps) {
316 + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
317 + }
318 + instance = createElement(
319 + 'link',
320 + stylesheetProps,
321 resourceRoot,
371 - href,
372 - precedence,
373 - resourceProps,
322 + HTML_NAMESPACE,
323 );
324 + markNodeAsResource(instance);
325 + setInitialProperties(instance, 'link', stylesheetProps);
326 + insertStylesheet(instance, precedence, resourceRoot);
327 }
376 - acquireResource(resource);
328 +
329 + // Construct a Resource and cache it
330 + resource = {
331 + type: 'stylesheet',
332 + instance,
333 + count: 1,
334 + };
335 + styles.set(key, resource);
336 return;
337 }
338 case 'script': {
339 const src = href;
381 - const scriptResources = getResourcesFromRoot(resourceRoot).scripts;
382 - let resource = scriptResources.get(src);
340 + const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
341 +
342 + const key = getScriptKey(src);
343 +
344 + // Check if this resource already exists
345 + let resource = scripts.get(key);
346 if (resource) {
384 - if (__DEV__) {
385 - const latestProps = scriptPropsFromPreinitOptions(src, options);
386 - validateScriptResourceDifference(resource.props, latestProps);
347 + // We can early return. The resource exists and there is nothing
348 + // more to do
349 + return;
350 + }
351 +
352 + // Attempt to hydrate instance from DOM
353 + let instance: null | Instance = resourceRoot.querySelector(
354 + getScriptSelectorFromKey(key),
355 + );
356 + if (!instance) {
357 + // Construct a new instance and insert it
358 + const scriptProps = scriptPropsFromPreinitOptions(src, options);
359 + // Adopt certain preload props
360 + const preloadProps = preloadPropsMap.get(key);
361 + if (preloadProps) {
362 + adoptPreloadPropsForScript(scriptProps, preloadProps);
363 }
388 - } else {
389 - const resourceProps = scriptPropsFromPreinitOptions(src, options);
390 - resource = createScriptResource(
391 - scriptResources,
364 + instance = createElement(
365 + 'script',
366 + scriptProps,
367 resourceRoot,
393 - src,
394 - resourceProps,
368 + HTML_NAMESPACE,
369 );
370 + markNodeAsResource(instance);
371 + setInitialProperties(instance, 'link', scriptProps);
372 + (getDocumentFromRoot(resourceRoot).head: any).appendChild(instance);
373 }
397 - acquireResource(resource);
374 +
375 + // Construct a Resource and cache it
376 + resource = {
377 + type: 'script',
378 + instance,
379 + count: 1,
380 + };
381 + scripts.set(key, resource);
382 return;
383 }
384 }
@@ -415,11 +399,11 @@ function preloadPropsFromPreinitOptions(
399 };
400 }
401
418 -function stylePropsFromPreinitOptions(
402 +function stylesheetPropsFromPreinitOptions(
403 href: string,
404 precedence: string,
405 options: PreinitOptions,
422 -): StyleProps {
406 +): StylesheetProps {
407 return {
408 rel: 'stylesheet',
409 href,
@@ -444,32 +428,24 @@ function scriptPropsFromPreinitOptions(
428 // Resources from render
429 // --------------------------------------
430
447 -type StyleQualifyingProps = {
448 - rel: 'stylesheet',
431 +type StyleTagQualifyingProps = {
432 href: string,
433 precedence: string,
434 [string]: mixed,
435 };
453 -type PreloadQualifyingProps = {
454 - rel: 'preload',
436 +
437 +type StylesheetQualifyingProps = {
438 + rel: 'stylesheet',
439 href: string,
456 - [string]: mixed,
457 -};
458 -type ScriptQualifyingProps = {
459 - src: string,
460 - async: true,
440 + precedence: string,
441 [string]: mixed,
442 };
443
464 -function getTitleKey(child: string | number): string {
465 - return 'title:' + child;
466 -}
467 -
444 // This function is called in begin work and we should always have a currentDocument set
445 export function getResource(
446 type: string,
471 - pendingProps: Props,
472 - currentProps: null | Props,
447 + currentProps: any,
448 + pendingProps: any,
449 ): null | Resource {
450 const resourceRoot = getCurrentResourceRoot();
451 if (!resourceRoot) {
@@ -478,515 +454,169 @@ export function getResource(
454 );
455 }
456 switch (type) {
481 - case 'base': {
482 - const headRoot: Document = getDocumentFromRoot(resourceRoot);
483 - const headResources = getResourcesFromRoot(headRoot).head;
484 - const {target, href} = pendingProps;
485 - let matcher = 'base';
486 - matcher +=
487 - typeof href === 'string'
488 - ? `[href="${escapeSelectorAttributeValueInsideDoubleQuotes(href)}"]`
489 - : ':not([href])';
490 - matcher +=
491 - typeof target === 'string'
492 - ? `[target="${escapeSelectorAttributeValueInsideDoubleQuotes(
493 - target,
494 - )}"]`
495 - : ':not([target])';
496 - let resource = headResources.get(matcher);
497 - if (!resource) {
498 - resource = {
499 - type: 'base',
500 - matcher,
501 - props: Object.assign({}, pendingProps),
502 - count: 0,
503 - instance: null,
504 - root: headRoot,
505 - };
506 - headResources.set(matcher, resource);
507 - }
508 - return resource;
457 + case 'meta':
458 + case 'title': {
459 + return null;
460 }
510 - case 'meta': {
511 - let matcher, propertyString, parentResource;
512 - const {charSet, content, httpEquiv, name, itemProp, property} =
513 - pendingProps;
514 - const headRoot: Document = getDocumentFromRoot(resourceRoot);
515 - const {head: headResources, lastStructuredMeta} =
516 - getResourcesFromRoot(headRoot);
517 - if (typeof charSet === 'string') {
518 - matcher = 'meta[charset]';
519 - } else if (typeof content === 'string') {
520 - if (typeof httpEquiv === 'string') {
521 - matcher = `meta[http-equiv="${escapeSelectorAttributeValueInsideDoubleQuotes(
522 - httpEquiv,
523 - )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
524 - content,
525 - )}"]`;
526 - } else if (typeof property === 'string') {
527 - propertyString = property;
528 - matcher = `meta[property="${escapeSelectorAttributeValueInsideDoubleQuotes(
529 - property,
530 - )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
531 - content,
532 - )}"]`;
533 -
534 - const parentPropertyPath = property.split(':').slice(0, -1).join(':');
535 - parentResource = lastStructuredMeta.get(parentPropertyPath);
536 - if (parentResource) {
537 - // When using parentResource the matcher is not functional for locating
538 - // the instance in the DOM but it still serves as a unique key.
539 - matcher = parentResource.matcher + matcher;
540 - }
541 - } else if (typeof name === 'string') {
542 - matcher = `meta[name="${escapeSelectorAttributeValueInsideDoubleQuotes(
543 - name,
544 - )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
545 - content,
546 - )}"]`;
547 - } else if (typeof itemProp === 'string') {
548 - matcher = `meta[itemprop="${escapeSelectorAttributeValueInsideDoubleQuotes(
549 - itemProp,
550 - )}"][content="${escapeSelectorAttributeValueInsideDoubleQuotes(
551 - content,
552 - )}"]`;
553 - }
554 - }
555 - if (matcher) {
556 - let resource = headResources.get(matcher);
461 + case 'style': {
462 + if (
463 + typeof pendingProps.precedence === 'string' &&
464 + typeof pendingProps.href === 'string'
465 + ) {
466 + const key = getStyleKey(pendingProps.href);
467 + const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
468 + let resource = styles.get(key);
469 if (!resource) {
470 resource = {
559 - type: 'meta',
560 - matcher,
561 - property: propertyString,
562 - parentResource,
563 - props: Object.assign({}, pendingProps),
564 - count: 0,
471 + type: 'style',
472 instance: null,
566 - root: headRoot,
473 + count: 0,
474 };
568 - headResources.set(matcher, resource);
569 - }
570 - if (typeof resource.property === 'string') {
571 - // We cast because flow doesn't know that this resource must be a Meta resource
572 - lastStructuredMeta.set(resource.property, (resource: any));
475 + styles.set(key, resource);
476 }
477 return resource;
478 }
576 - return null;
479 + return {
480 + type: 'void',
481 + instance: null,
482 + count: 0,
483 + };
484 }
578 - case 'title': {
579 - const children = pendingProps.children;
580 - let child;
581 - if (Array.isArray(children)) {
582 - child = children.length === 1 ? children[0] : null;
583 - } else {
584 - child = children;
585 - }
485 + case 'link': {
486 if (
587 - typeof child !== 'function' &&
588 - typeof child !== 'symbol' &&
589 - child !== null &&
590 - child !== undefined
487 + pendingProps.rel === 'stylesheet' &&
488 + typeof pendingProps.href === 'string' &&
489 + typeof pendingProps.precedence === 'string'
490 ) {
592 - // eslint-disable-next-line react-internal/safe-string-coercion
593 - const childString = '' + (child: any);
594 - const headRoot: Document = getDocumentFromRoot(resourceRoot);
595 - const headResources = getResourcesFromRoot(headRoot).head;
596 - const key = getTitleKey(childString);
597 - let resource = headResources.get(key);
491 + const qualifiedProps: StylesheetQualifyingProps = pendingProps;
492 + const key = getStyleKey(qualifiedProps.href);
493 +
494 + const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
495 +
496 + let resource = styles.get(key);
497 if (!resource) {
599 - const titleProps = titlePropsFromRawProps(childString, pendingProps);
498 + // We asserted this above but Flow can't figure out that the type satisfies
499 + const ownerDocument = getDocumentFromRoot(resourceRoot);
500 resource = {
601 - type: 'title',
602 - props: titleProps,
603 - count: 0,
501 + type: 'stylesheet',
502 instance: null,
605 - root: headRoot,
503 + count: 0,
504 };
607 - headResources.set(key, resource);
505 + styles.set(key, resource);
506 + if (!preloadPropsMap.has(key)) {
507 + preloadStylesheet(
508 + ownerDocument,
509 + key,
510 + preloadPropsFromStylesheet(qualifiedProps),
511 + );
512 + }
513 }
514 return resource;
515 }
516 return null;
517 }
613 - case 'link': {
614 - const {rel} = pendingProps;
615 - switch (rel) {
616 - case 'stylesheet': {
617 - const styleResources = getResourcesFromRoot(resourceRoot).styles;
618 - let didWarn;
619 - if (__DEV__) {
620 - if (currentProps) {
621 - didWarn = validateURLKeyedUpdatedProps(
622 - pendingProps,
623 - currentProps,
624 - 'style',
625 - 'href',
626 - );
627 - }
628 - if (!didWarn) {
629 - didWarn = validateLinkPropsForStyleResource(pendingProps);
630 - }
631 - }
632 - const {precedence, href} = pendingProps;
633 - if (typeof href === 'string' && typeof precedence === 'string') {
634 - // We've asserted all the specific types for StyleQualifyingProps
635 - const styleRawProps: StyleQualifyingProps = (pendingProps: any);
636 -
637 - // We construct or get an existing resource for the style itself and return it
638 - let resource = styleResources.get(href);
639 - if (resource) {
640 - if (__DEV__) {
641 - if (!didWarn) {
642 - const latestProps = stylePropsFromRawProps(styleRawProps);
643 - if ((resource: any)._dev_preload_props) {
644 - adoptPreloadPropsForStyle(
645 - latestProps,
646 - (resource: any)._dev_preload_props,
647 - );
648 - }
649 - validateStyleResourceDifference(resource.props, latestProps);
650 - }
651 - }
652 - } else {
653 - const resourceProps = stylePropsFromRawProps(styleRawProps);
654 - resource = createStyleResource(
655 - styleResources,
656 - resourceRoot,
657 - href,
658 - precedence,
659 - resourceProps,
660 - );
661 - immediatelyPreloadStyleResource(resource);
662 - }
663 - return resource;
664 - }
665 - return null;
666 - }
667 - case 'preload': {
668 - if (__DEV__) {
669 - validateLinkPropsForPreloadResource(pendingProps);
670 - }
671 - const {href} = pendingProps;
672 - if (typeof href === 'string') {
673 - // We've asserted all the specific types for PreloadQualifyingProps
674 - const preloadRawProps: PreloadQualifyingProps = (pendingProps: any);
675 - let resource = preloadResources.get(href);
676 - if (resource) {
677 - if (__DEV__) {
678 - const originallyImplicit =
679 - (resource: any)._dev_implicit_construction === true;
680 - const latestProps = preloadPropsFromRawProps(preloadRawProps);
681 - validatePreloadResourceDifference(
682 - resource.props,
683 - originallyImplicit,
684 - latestProps,
685 - false,
686 - );
687 - }
688 - } else {
689 - const resourceProps = preloadPropsFromRawProps(preloadRawProps);
690 - resource = createPreloadResource(
691 - getDocumentFromRoot(resourceRoot),
692 - href,
693 - resourceProps,
694 - );
695 - }
696 - return resource;
697 - }
698 - return null;
699 - }
700 - default: {
701 - const {href, sizes, media} = pendingProps;
702 - if (typeof rel === 'string' && typeof href === 'string') {
703 - const sizeKey =
704 - '::sizes:' + (typeof sizes === 'string' ? sizes : '');
705 - const mediaKey =
706 - '::media:' + (typeof media === 'string' ? media : '');
707 - const key = 'rel:' + rel + '::href:' + href + sizeKey + mediaKey;
708 - const headRoot = getDocumentFromRoot(resourceRoot);
709 - const headResources = getResourcesFromRoot(headRoot).head;
710 - let resource = headResources.get(key);
711 - if (!resource) {
712 - resource = {
713 - type: 'link',
714 - props: Object.assign({}, pendingProps),
715 - count: 0,
716 - instance: null,
717 - root: headRoot,
718 - };
719 - headResources.set(key, resource);
720 - }
721 - return resource;
722 - }
723 - if (__DEV__) {
724 - warnOnMissingHrefAndRel(pendingProps, currentProps);
725 - }
726 - return null;
727 - }
728 - }
729 - }
518 case 'script': {
731 - const scriptResources = getResourcesFromRoot(resourceRoot).scripts;
732 - let didWarn;
733 - if (__DEV__) {
734 - if (currentProps) {
735 - didWarn = validateURLKeyedUpdatedProps(
736 - pendingProps,
737 - currentProps,
738 - 'script',
739 - 'src',
740 - );
741 - }
742 - }
743 - const {src, async} = pendingProps;
744 - if (async && typeof src === 'string') {
745 - const scriptRawProps: ScriptQualifyingProps = (pendingProps: any);
746 - let resource = scriptResources.get(src);
747 - if (resource) {
748 - if (__DEV__) {
749 - if (!didWarn) {
750 - const latestProps = scriptPropsFromRawProps(scriptRawProps);
751 - if ((resource: any)._dev_preload_props) {
752 - adoptPreloadPropsForScript(
753 - latestProps,
754 - (resource: any)._dev_preload_props,
755 - );
756 - }
757 - validateScriptResourceDifference(resource.props, latestProps);
758 - }
759 - }
760 - } else {
761 - const resourceProps = scriptPropsFromRawProps(scriptRawProps);
762 - resource = createScriptResource(
763 - scriptResources,
764 - resourceRoot,
765 - src,
766 - resourceProps,
767 - );
519 + if (typeof pendingProps.src === 'string' && pendingProps.async === true) {
520 + const scriptProps: ScriptProps = pendingProps;
521 + const key = getScriptKey(scriptProps.src);
522 + const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
523 +
524 + let resource = scripts.get(key);
525 + if (!resource) {
526 + resource = {
527 + type: 'script',
528 + instance: null,
529 + count: 0,
530 + };
531 + scripts.set(key, resource);
532 }
533 return resource;
534 }
771 - return null;
535 + return {
536 + type: 'void',
537 + instance: null,
538 + count: 0,
539 + };
540 }
541 default: {
542 throw new Error(
775 - `getResource encountered a resource type it did not expect: "${type}". this is a bug in React.`,
543 + `getResource encountered a type it did not expect: "${type}". this is a bug in React.`,
544 );
545 }
546 }
547 }
548
781 -function preloadPropsFromRawProps(
782 - rawBorrowedProps: PreloadQualifyingProps,
783 -): PreloadProps {
784 - // $FlowFixMe[prop-missing] - recommended fix is to use object spread operator
785 - return Object.assign({}, rawBorrowedProps);
786 -}
787 -
788 -function titlePropsFromRawProps(
789 - child: string | number,
790 - rawProps: Props,
791 -): TitleProps {
792 - const props: TitleProps = Object.assign({}, rawProps);
793 - props.children = child;
794 - return props;
549 +function styleTagPropsFromRawProps(
550 + rawProps: StyleTagQualifyingProps,
551 +): StyleTagProps {
552 + return {
553 + ...rawProps,
554 + 'data-href': rawProps.href,
555 + 'data-precedence': rawProps.precedence,
556 + href: null,
557 + precedence: null,
558 + };
559 }
560
797 -function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
798 - // $FlowFixMe[prop-missing] - recommended fix is to use object spread operator
799 - const props: StyleProps = Object.assign({}, rawProps);
800 - props['data-precedence'] = rawProps.precedence;
801 - props.precedence = null;
802 -
803 - return props;
561 +function getStyleKey(href: string) {
562 + const limitedEscapedHref =
563 + escapeSelectorAttributeValueInsideDoubleQuotes(href);
564 + return `href="${limitedEscapedHref}"`;
565 }
566
806 -function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
807 - // $FlowFixMe[prop-missing] - recommended fix is to use object spread operator
808 - const props: ScriptProps = Object.assign({}, rawProps);
809 - return props;
567 +function getStyleTagSelectorFromKey(key: string) {
568 + return `style[data-${key}]`;
569 }
570
812 -// --------------------------------------
813 -// Resource Reconciliation
814 -// --------------------------------------
815 -
816 -export function acquireResource(resource: Resource): Instance {
817 - switch (resource.type) {
818 - case 'base':
819 - case 'title':
820 - case 'link':
821 - case 'meta': {
822 - return acquireHeadResource(resource);
823 - }
824 - case 'style': {
825 - return acquireStyleResource(resource);
826 - }
827 - case 'script': {
828 - return acquireScriptResource(resource);
829 - }
830 - case 'preload': {
831 - return resource.instance;
832 - }
833 - default: {
834 - throw new Error(
835 - `acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
836 - );
837 - }
838 - }
571 +function getStylesheetSelectorFromKey(key: string) {
572 + return `link[rel="stylesheet"][${key}]`;
573 }
574
841 -export function releaseResource(resource: Resource): void {
842 - switch (resource.type) {
843 - case 'link':
844 - case 'title':
845 - case 'meta': {
846 - return releaseHeadResource(resource);
847 - }
848 - case 'style': {
849 - resource.count--;
850 - return;
851 - }
852 - }
575 +function getPreloadStylesheetSelectorFromKey(key: string) {
576 + return `link[rel="preload"][as="style"][${key}]`;
577 }
578
855 -function releaseHeadResource(resource: HeadResource): void {
856 - if (--resource.count === 0) {
857 - // the instance will have existed since we acquired it
858 - const instance: Instance = (resource.instance: any);
859 - const parent = instance.parentNode;
860 - if (parent) {
861 - parent.removeChild(instance);
862 - }
863 - resource.instance = null;
864 - }
579 +function stylesheetPropsFromRawProps(
580 + rawProps: StylesheetQualifyingProps,
581 +): StylesheetProps {
582 + return {
583 + ...rawProps,
584 + 'data-precedence': rawProps.precedence,
585 + precedence: null,
586 + };
587 }
588
867 -function createResourceInstance(
868 - type: string,
869 - props: Object,
589 +function preloadStylesheet(
590 ownerDocument: Document,
871 -): Instance {
872 - const element = createElement(type, props, ownerDocument, HTML_NAMESPACE);
873 - setInitialProperties(element, type, props);
874 - markNodeAsResource(element);
875 - return element;
876 -}
877 -
878 -function createStyleResource(
879 - styleResources: Map<string, StyleResource>,
880 - root: FloatRoot,
881 - href: string,
882 - precedence: string,
883 - props: StyleProps,
884 -): StyleResource {
885 - if (__DEV__) {
886 - if (styleResources.has(href)) {
887 - console.error(
888 - 'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
591 + key: string,
592 + preloadProps: PreloadProps,
593 +) {
594 + preloadPropsMap.set(key, preloadProps);
595 +
596 + if (!ownerDocument.querySelector(getStylesheetSelectorFromKey(key))) {
597 + // There is no matching stylesheet instance in the Document.
598 + // We will insert a preload now to kick off loading because
599 + // we expect this stylesheet to commit
600 + if (
601 + null ===
602 + ownerDocument.querySelector(getPreloadStylesheetSelectorFromKey(key))
603 + ) {
604 + const preloadInstance = createElement(
605 + 'link',
606 + preloadProps,
607 + ownerDocument,
608 + HTML_NAMESPACE,
609 );
610 + setInitialProperties(preloadInstance, 'link', preloadProps);
611 + markNodeAsResource(preloadInstance);
612 + (ownerDocument.head: any).appendChild(preloadInstance);
613 }
614 }
892 -
893 - const limitedEscapedHref =
894 - escapeSelectorAttributeValueInsideDoubleQuotes(href);
895 - const existingEl = root.querySelector(
896 - `link[rel="stylesheet"][href="${limitedEscapedHref}"]`,
897 - );
898 - const resource = {
899 - type: 'style',
900 - count: 0,
901 - href,
902 - precedence,
903 - props,
904 - hint: null,
905 - preloaded: false,
906 - loaded: false,
907 - error: false,
908 - root,
909 - instance: null,
910 - };
911 - styleResources.set(href, resource);
912 -
913 - if (existingEl) {
914 - // If we have an existing element in the DOM we don't need to preload this resource nor can we
915 - // adopt props from any preload that might exist already for this resource. We do need to try
916 - // to reify the Resource loading state the best we can.
917 - const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
918 - if (loadingState) {
919 - switch (loadingState.s) {
920 - case 'l': {
921 - resource.loaded = true;
922 - break;
923 - }
924 - case 'e': {
925 - resource.error = true;
926 - break;
927 - }
928 - default: {
929 - attachLoadListeners(existingEl, resource);
930 - }
931 - }
932 - } else {
933 - // This is unfortunately just an assumption. The rationale here is that stylesheets without
934 - // a loading state must have been flushed in the shell and would have blocked until loading
935 - // or error. we can't know afterwards which happened for all types of stylesheets (cross origin)
936 - // for instance) and the techniques for determining if a sheet has loaded that we do have still
937 - // fail if the sheet loaded zero rules. At the moment we are going to just opt to assume the
938 - // sheet is loaded if it was flushed in the shell
939 - resource.loaded = true;
940 - }
941 - } else {
942 - const hint = preloadResources.get(href);
943 - if (hint) {
944 - // $FlowFixMe[incompatible-type]: found when upgrading Flow
945 - resource.hint = hint;
946 - // If a preload for this style Resource already exists there are certain props we want to adopt
947 - // on the style Resource, primarily focussed on making sure the style network pathways utilize
948 - // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
949 - // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
950 - const preloadProps = hint.props;
951 - adoptPreloadPropsForStyle(resource.props, hint.props);
952 - if (__DEV__) {
953 - (resource: any)._dev_preload_props = preloadProps;
954 - }
955 - }
956 - }
957 -
958 - return resource;
615 }
616
961 -function adoptPreloadPropsForStyle(
962 - styleProps: StyleProps,
963 - preloadProps: PreloadProps,
964 -): void {
965 - if (styleProps.crossOrigin == null)
966 - styleProps.crossOrigin = preloadProps.crossOrigin;
967 - if (styleProps.referrerPolicy == null)
968 - styleProps.referrerPolicy = preloadProps.referrerPolicy;
969 - if (styleProps.title == null) styleProps.title = preloadProps.title;
970 -}
971 -
972 -function immediatelyPreloadStyleResource(resource: StyleResource) {
973 - // This function must be called synchronously after creating a styleResource otherwise it may
974 - // violate assumptions around the existence of a preload. The reason it is extracted out is we
975 - // don't always want to preload a style, in particular when we are going to synchronously insert
976 - // that style. We confirm the style resource has no preload already and then construct it. If
977 - // we wait and call this later it is possible a preload will already exist for this href
978 - if (resource.loaded === false && resource.hint === null) {
979 - const {href, props} = resource;
980 - const preloadProps = preloadPropsFromStyleProps(props);
981 - resource.hint = createPreloadResource(
982 - getDocumentFromRoot(resource.root),
983 - href,
984 - preloadProps,
985 - );
986 - }
987 -}
988 -
989 -function preloadPropsFromStyleProps(props: StyleProps): PreloadProps {
617 +function preloadPropsFromStylesheet(
618 + props: StylesheetQualifyingProps,
619 +): PreloadProps {
620 return {
621 rel: 'preload',
622 as: 'style',
@@ -999,378 +629,160 @@ function preloadPropsFromStyleProps(props: StyleProps): PreloadProps {
629 };
630 }
631
1002 -function createScriptResource(
1003 - scriptResources: Map<string, ScriptResource>,
1004 - root: FloatRoot,
1005 - src: string,
1006 - props: ScriptProps,
1007 -): ScriptResource {
1008 - if (__DEV__) {
1009 - if (scriptResources.has(src)) {
1010 - console.error(
1011 - 'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
1012 - );
1013 - }
1014 - }
1015 -
632 +function getScriptKey(src: string): string {
633 const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src);
1017 - const existingEl = root.querySelector(
1018 - `script[async][src="${limitedEscapedSrc}"]`,
1019 - );
1020 - const resource = {
1021 - type: 'script',
1022 - src,
1023 - props,
1024 - root,
1025 - instance: existingEl || null,
1026 - };
1027 - scriptResources.set(src, resource);
1028 -
1029 - if (!existingEl) {
1030 - const hint = preloadResources.get(src);
1031 - if (hint) {
1032 - // If a preload for this style Resource already exists there are certain props we want to adopt
1033 - // on the style Resource, primarily focussed on making sure the style network pathways utilize
1034 - // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
1035 - // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
1036 - const preloadProps = hint.props;
1037 - adoptPreloadPropsForScript(props, hint.props);
1038 - if (__DEV__) {
1039 - (resource: any)._dev_preload_props = preloadProps;
1040 - }
1041 - }
1042 - } else {
1043 - markNodeAsResource(existingEl);
1044 - }
1045 -
1046 - return resource;
634 + return `[src="${limitedEscapedSrc}"]`;
635 }
636
1049 -function adoptPreloadPropsForScript(
1050 - scriptProps: ScriptProps,
1051 - preloadProps: PreloadProps,
1052 -): void {
1053 - if (scriptProps.crossOrigin == null)
1054 - scriptProps.crossOrigin = preloadProps.crossOrigin;
1055 - if (scriptProps.referrerPolicy == null)
1056 - scriptProps.referrerPolicy = preloadProps.referrerPolicy;
1057 - if (scriptProps.integrity == null)
1058 - scriptProps.referrerPolicy = preloadProps.integrity;
637 +function getScriptSelectorFromKey(key: string): string {
638 + return 'script[async]' + key;
639 }
640
1061 -function createPreloadResource(
1062 - ownerDocument: Document,
1063 - href: string,
1064 - props: PreloadProps,
1065 -): PreloadResource {
1066 - const limitedEscapedHref =
1067 - escapeSelectorAttributeValueInsideDoubleQuotes(href);
1068 - let element: null | Instance | HTMLElement = ownerDocument.querySelector(
1069 - `link[rel="preload"][href="${limitedEscapedHref}"]`,
1070 - );
1071 - if (!element) {
1072 - element = createResourceInstance('link', props, ownerDocument);
1073 - insertResourceInstanceBefore(ownerDocument, element, null);
1074 - } else {
1075 - markNodeAsResource(element);
1076 - }
1077 - return {
1078 - type: 'preload',
1079 - href: href,
1080 - ownerDocument,
1081 - props,
1082 - instance: element,
1083 - };
1084 -}
641 +// --------------------------------------
642 +// Hoistable Resource Reconciliation
643 +// --------------------------------------
644
1086 -function acquireHeadResource(resource: HeadResource): Instance {
645 +export function acquireResource(
646 + hoistableRoot: HoistableRoot,
647 + resource: Resource,
648 + props: any,
649 +): null | Instance {
650 resource.count++;
1088 - let instance = resource.instance;
1089 - if (!instance) {
1090 - const {props, root, type} = resource;
1091 - switch (type) {
1092 - case 'title': {
1093 - const titles = root.querySelectorAll('title');
1094 - for (let i = 0; i < titles.length; i++) {
1095 - if (titles[i].textContent === props.children) {
1096 - instance = resource.instance = titles[i];
1097 - markNodeAsResource(instance);
1098 - return instance;
1099 - }
1100 - }
1101 - instance = resource.instance = createResourceInstance(
1102 - type,
1103 - props,
1104 - root,
1105 - );
1106 - const firstTitle = titles[0];
1107 - insertResourceInstanceBefore(
1108 - root,
1109 - instance,
1110 - firstTitle && firstTitle.namespaceURI !== SVG_NAMESPACE
1111 - ? firstTitle
1112 - : null,
651 + if (resource.instance === null) {
652 + switch (resource.type) {
653 + case 'style': {
654 + const qualifiedProps: StyleTagQualifyingProps = props;
655 + const key = getStyleKey(qualifiedProps.href);
656 +
657 + // Attempt to hydrate instance from DOM
658 + let instance: null | Instance = hoistableRoot.querySelector(
659 + getStyleTagSelectorFromKey(key),
660 );
1114 - break;
1115 - }
1116 - case 'meta': {
1117 - let insertBefore = null;
1118 -
1119 - const metaResource: MetaResource = (resource: any);
1120 - const {matcher, property, parentResource} = metaResource;
1121 -
1122 - if (parentResource && typeof property === 'string') {
1123 - // This resoruce is a structured meta type with a parent.
1124 - // Instead of using the matcher we just traverse forward
1125 - // siblings of the parent instance until we find a match
1126 - // or exhaust.
1127 - const parent = parentResource.instance;
1128 - if (parent) {
1129 - let node = null;
1130 - let nextNode = (insertBefore = parent.nextSibling);
1131 - while ((node = nextNode)) {
1132 - nextNode = node.nextSibling;
1133 - if (node.nodeName === 'META') {
1134 - const meta: Element = (node: any);
1135 - const propertyAttr = meta.getAttribute('property');
1136 - if (typeof propertyAttr !== 'string') {
1137 - continue;
1138 - } else if (
1139 - propertyAttr === property &&
1140 - meta.getAttribute('content') === props.content
1141 - ) {
1142 - resource.instance = meta;
1143 - markNodeAsResource(meta);
1144 - return meta;
1145 - } else if (property.startsWith(propertyAttr + ':')) {
1146 - // This meta starts a new instance of a parent structure for this meta type
1147 - // We need to halt our search here because even if we find a later match it
1148 - // is for a different parent element
1149 - break;
1150 - }
1151 - }
1152 - }
1153 - }
1154 - } else if ((instance = root.querySelector(matcher))) {
661 + if (instance) {
662 resource.instance = instance;
1156 - markNodeAsResource(instance);
663 return instance;
664 }
1159 - instance = resource.instance = createResourceInstance(
1160 - type,
1161 - props,
1162 - root,
665 +
666 + const styleProps = styleTagPropsFromRawProps(props);
667 + instance = createElement(
668 + 'style',
669 + styleProps,
670 + hoistableRoot,
671 + HTML_NAMESPACE,
672 );
1164 - insertResourceInstanceBefore(root, instance, insertBefore);
1165 - break;
673 +
674 + markNodeAsResource(instance);
675 + setInitialProperties(instance, 'style', styleProps);
676 + insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
677 + resource.instance = instance;
678 +
679 + return instance;
680 }
1167 - case 'link': {
1168 - const linkProps: LinkProps = (props: any);
1169 - const limitedEscapedRel =
1170 - escapeSelectorAttributeValueInsideDoubleQuotes(linkProps.rel);
1171 - const limitedEscapedHref =
1172 - escapeSelectorAttributeValueInsideDoubleQuotes(linkProps.href);
1173 - let selector = `link[rel="${limitedEscapedRel}"][href="${limitedEscapedHref}"]`;
1174 - if (typeof linkProps.sizes === 'string') {
1175 - const limitedEscapedSizes =
1176 - escapeSelectorAttributeValueInsideDoubleQuotes(linkProps.sizes);
1177 - selector += `[sizes="${limitedEscapedSizes}"]`;
1178 - }
1179 - if (typeof linkProps.media === 'string') {
1180 - const limitedEscapedMedia =
1181 - escapeSelectorAttributeValueInsideDoubleQuotes(linkProps.media);
1182 - selector += `[media="${limitedEscapedMedia}"]`;
1183 - }
1184 - const existingEl = root.querySelector(selector);
1185 - if (existingEl) {
1186 - instance = resource.instance = existingEl;
1187 - markNodeAsResource(instance);
681 + case 'stylesheet': {
682 + // This typing is enforce by `getResource`. If we change the logic
683 + // there for what qualifies as a stylesheet resource we need to ensure
684 + // this cast still makes sense;
685 + const qualifiedProps: StylesheetQualifyingProps = props;
686 + const key = getStyleKey(qualifiedProps.href);
687 +
688 + // Attempt to hydrate instance from DOM
689 + let instance: null | Instance = hoistableRoot.querySelector(
690 + getStylesheetSelectorFromKey(key),
691 + );
692 + if (instance) {
693 + resource.instance = instance;
694 return instance;
695 }
1190 - instance = resource.instance = createResourceInstance(
1191 - type,
1192 - props,
1193 - root,
696 +
697 + const stylesheetProps = stylesheetPropsFromRawProps(props);
698 + const preloadProps = preloadPropsMap.get(key);
699 + if (preloadProps) {
700 + adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
701 + }
702 +
703 + // Construct and insert a new instance
704 + instance = createElement(
705 + 'link',
706 + stylesheetProps,
707 + hoistableRoot,
708 + HTML_NAMESPACE,
709 + );
710 + markNodeAsResource(instance);
711 + const linkInstance: HTMLLinkElement = (instance: any);
712 + (linkInstance: any)._p = new Promise((resolve, reject) => {
713 + linkInstance.onload = resolve;
714 + linkInstance.onerror = reject;
715 + }).then(
716 + () => ((linkInstance: any)._p.s = 'l'),
717 + () => ((linkInstance: any)._p.s = 'e'),
718 );
1195 - insertResourceInstanceBefore(root, instance, null);
719 + setInitialProperties(instance, 'link', stylesheetProps);
720 + insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
721 + resource.instance = instance;
722 +
723 return instance;
724 }
1198 - case 'base': {
1199 - const baseResource: BaseResource = (resource: any);
1200 - const {matcher} = baseResource;
1201 - const base = root.querySelector(matcher);
1202 - if (base) {
1203 - instance = resource.instance = base;
1204 - markNodeAsResource(instance);
1205 - } else {
1206 - instance = resource.instance = createResourceInstance(
1207 - type,
1208 - props,
1209 - root,
1210 - );
1211 - insertResourceInstanceBefore(
1212 - root,
1213 - instance,
1214 - root.querySelector('base'),
1215 - );
725 + case 'script': {
726 + // This typing is enforce by `getResource`. If we change the logic
727 + // there for what qualifies as a stylesheet resource we need to ensure
728 + // this cast still makes sense;
729 + const borrowedScriptProps: ScriptProps = props;
730 + const key = getScriptKey(borrowedScriptProps.src);
731 +
732 + // Attempt to hydrate instance from DOM
733 + let instance: null | Instance = hoistableRoot.querySelector(
734 + getScriptSelectorFromKey(key),
735 + );
736 + if (instance) {
737 + resource.instance = instance;
738 + return instance;
739 }
740 +
741 + let scriptProps = borrowedScriptProps;
742 + const preloadProps = preloadPropsMap.get(key);
743 + if (preloadProps) {
744 + scriptProps = {...borrowedScriptProps};
745 + adoptPreloadPropsForScript(scriptProps, preloadProps);
746 + }
747 +
748 + // Construct and insert a new instance
749 + instance = createElement(
750 + 'script',
751 + scriptProps,
752 + hoistableRoot,
753 + HTML_NAMESPACE,
754 + );
755 + markNodeAsResource(instance);
756 + setInitialProperties(instance, 'link', scriptProps);
757 + (getDocumentFromRoot(hoistableRoot).head: any).appendChild(instance);
758 + resource.instance = instance;
759 +
760 return instance;
761 }
762 + case 'void': {
763 + return null;
764 + }
765 default: {
766 throw new Error(
1221 - `acquireHeadResource encountered a resource type it did not expect: "${type}". This is a bug in React.`,
767 + `acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
768 );
769 }
770 }
771 }
1226 - return instance;
1227 -}
1228 -
1229 -function acquireStyleResource(resource: StyleResource): Instance {
1230 - let instance = resource.instance;
1231 - if (!instance) {
1232 - const {props, root, precedence} = resource;
1233 - const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
1234 - props.href,
1235 - );
1236 - const existingEl = root.querySelector(
1237 - `link[rel="stylesheet"][data-precedence][href="${limitedEscapedHref}"]`,
1238 - );
1239 - if (existingEl) {
1240 - instance = resource.instance = existingEl;
1241 - markNodeAsResource(instance);
1242 - resource.preloaded = true;
1243 - const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
1244 - if (loadingState) {
1245 - // if an existingEl is found there should always be a loadingState because if
1246 - // the resource was flushed in the head it should have already been found when
1247 - // the resource was first created. Still defensively we gate this
1248 - switch (loadingState.s) {
1249 - case 'l': {
1250 - resource.loaded = true;
1251 - resource.error = false;
1252 - break;
1253 - }
1254 - case 'e': {
1255 - resource.error = true;
1256 - break;
1257 - }
1258 - default: {
1259 - attachLoadListeners(existingEl, resource);
1260 - }
1261 - }
1262 - } else {
1263 - resource.loaded = true;
1264 - }
1265 - } else {
1266 - instance = resource.instance = createResourceInstance(
1267 - 'link',
1268 - resource.props,
1269 - getDocumentFromRoot(root),
1270 - );
1271 -
1272 - attachLoadListeners(instance, resource);
1273 - insertStyleInstance(instance, precedence, root);
1274 - }
1275 - }
1276 - resource.count++;
1277 - return instance;
1278 -}
1279 -
1280 -function acquireScriptResource(resource: ScriptResource): Instance {
1281 - let instance = resource.instance;
1282 - if (!instance) {
1283 - const {props, root} = resource;
1284 - const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(
1285 - props.src,
1286 - );
1287 - const existingEl = root.querySelector(
1288 - `script[async][src="${limitedEscapedSrc}"]`,
1289 - );
1290 - if (existingEl) {
1291 - instance = resource.instance = existingEl;
1292 - markNodeAsResource(instance);
1293 - } else {
1294 - instance = resource.instance = createResourceInstance(
1295 - 'script',
1296 - resource.props,
1297 - getDocumentFromRoot(root),
1298 - );
1299 -
1300 - insertResourceInstanceBefore(getDocumentFromRoot(root), instance, null);
1301 - }
1302 - }
1303 - return instance;
1304 -}
1305 -
1306 -function attachLoadListeners(instance: Instance, resource: StyleResource) {
1307 - const listeners: {
1308 - [string]: () => mixed,
1309 - } = {};
1310 - listeners.load = onResourceLoad.bind(
1311 - null,
1312 - instance,
1313 - resource,
1314 - listeners,
1315 - loadAndErrorEventListenerOptions,
1316 - );
1317 - listeners.error = onResourceError.bind(
1318 - null,
1319 - instance,
1320 - resource,
1321 - listeners,
1322 - loadAndErrorEventListenerOptions,
1323 - );
1324 -
1325 - instance.addEventListener(
1326 - 'load',
1327 - listeners.load,
1328 - loadAndErrorEventListenerOptions,
1329 - );
1330 - instance.addEventListener(
1331 - 'error',
1332 - listeners.error,
1333 - loadAndErrorEventListenerOptions,
1334 - );
772 + return resource.instance;
773 }
774
1337 -const loadAndErrorEventListenerOptions = {
1338 - passive: true,
1339 -};
1340 -
1341 -function onResourceLoad(
1342 - instance: Instance,
1343 - resource: StyleResource,
1344 - listeners: {[string]: () => mixed},
1345 - listenerOptions: typeof loadAndErrorEventListenerOptions,
1346 -) {
1347 - resource.loaded = true;
1348 - resource.error = false;
1349 - for (const event in listeners) {
1350 - instance.removeEventListener(event, listeners[event], listenerOptions);
1351 - }
1352 -}
1353 -
1354 -function onResourceError(
1355 - instance: Instance,
1356 - resource: StyleResource,
1357 - listeners: {[string]: () => mixed},
1358 - listenerOptions: typeof loadAndErrorEventListenerOptions,
1359 -) {
1360 - resource.loaded = false;
1361 - resource.error = true;
1362 - for (const event in listeners) {
1363 - instance.removeEventListener(event, listeners[event], listenerOptions);
1364 - }
775 +export function releaseResource(resource: Resource): void {
776 + resource.count--;
777 }
778
1367 -function insertStyleInstance(
779 +function insertStylesheet(
780 instance: Instance,
781 precedence: string,
1370 - root: FloatRoot,
782 + root: HoistableRoot,
783 ): void {
784 const nodes = root.querySelectorAll(
1373 - 'link[rel="stylesheet"][data-precedence]',
785 + 'link[rel="stylesheet"][data-precedence],style[data-precedence]',
786 );
787 const last = nodes.length ? nodes[nodes.length - 1] : null;
788 let prior = last;
@@ -1390,40 +802,220 @@ function insertStyleInstance(
802 ((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
803 } else {
804 const parent =
1393 - root.nodeType === DOCUMENT_NODE ? ((root: any): Document).head : root;
1394 - if (parent) {
1395 - parent.insertBefore(instance, parent.firstChild);
1396 - } else {
1397 - throw new Error(
1398 - 'While attempting to insert a Resource, React expected the Document to contain' +
1399 - ' a head element but it was not found.',
1400 - );
1401 - }
805 + root.nodeType === DOCUMENT_NODE
806 + ? ((((root: any): Document).head: any): Element)
807 + : ((root: any): ShadowRoot);
808 + parent.insertBefore(instance, parent.firstChild);
809 }
810 }
811
1405 -function insertResourceInstanceBefore(
1406 - ownerDocument: Document,
1407 - instance: Instance,
1408 - before: ?Node,
812 +function adoptPreloadPropsForStylesheet(
813 + stylesheetProps: StylesheetProps,
814 + preloadProps: PreloadProps,
815 ): void {
1410 - if (__DEV__) {
1411 - if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
1412 - console.error(
1413 - 'insertResourceInstanceBefore was called with a stylesheet. Stylesheets must be' +
1414 - ' inserted with insertStyleInstance instead. This is a bug in React.',
1415 - );
1416 - }
816 + if (stylesheetProps.crossOrigin == null)
817 + stylesheetProps.crossOrigin = preloadProps.crossOrigin;
818 + if (stylesheetProps.referrerPolicy == null)
819 + stylesheetProps.referrerPolicy = preloadProps.referrerPolicy;
820 + if (stylesheetProps.title == null) stylesheetProps.title = preloadProps.title;
821 +}
822 +
823 +function adoptPreloadPropsForScript(
824 + scriptProps: ScriptProps,
825 + preloadProps: PreloadProps,
826 +): void {
827 + if (scriptProps.crossOrigin == null)
828 + scriptProps.crossOrigin = preloadProps.crossOrigin;
829 + if (scriptProps.referrerPolicy == null)
830 + scriptProps.referrerPolicy = preloadProps.referrerPolicy;
831 + if (scriptProps.integrity == null)
832 + scriptProps.referrerPolicy = preloadProps.integrity;
833 +}
834 +
835 +// --------------------------------------
836 +// Hoistable Element Reconciliation
837 +// --------------------------------------
838 +
839 +export function hydrateHoistable(
840 + hoistableRoot: HoistableRoot,
841 + type: HoistableTagType,
842 + props: any,
843 + internalInstanceHandle: Object,
844 +): Instance {
845 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
846 + const nodes = ownerDocument.getElementsByTagName(type);
847 +
848 + const children = props.children;
849 + let child, childString;
850 + if (Array.isArray(children)) {
851 + child = children.length === 1 ? children[0] : null;
852 + } else {
853 + child = children;
854 }
1418 - const parent = (before && before.parentNode) || ownerDocument.head;
1419 - if (parent) {
1420 - parent.insertBefore(instance, before);
855 + if (
856 + typeof child !== 'function' &&
857 + typeof child !== 'symbol' &&
858 + child !== null &&
859 + child !== undefined
860 + ) {
861 + if (__DEV__) {
862 + checkPropStringCoercion(child, 'children');
863 + }
864 + childString = '' + (child: any);
865 } else {
1422 - throw new Error(
1423 - 'While attempting to insert a Resource, React expected the Document to contain' +
1424 - ' a head element but it was not found.',
1425 - );
866 + childString = '';
867 + }
868 + nodeLoop: for (let i = 0; i < nodes.length; i++) {
869 + const node = nodes[i];
870 + if (
871 + isMarkedResource(node) ||
872 + node.namespaceURI === SVG_NAMESPACE ||
873 + node.textContent !== childString
874 + ) {
875 + continue;
876 + }
877 + let checkedAttributes = 0;
878 + for (const propName in props) {
879 + const propValue = props[propName];
880 + if (!props.hasOwnProperty(propName)) {
881 + continue;
882 + }
883 + switch (propName) {
884 + // Reserved props will never have an attribute partner
885 + case 'children':
886 + case 'defaultValue':
887 + case 'dangerouslySetInnerHTML':
888 + case 'defaultChecked':
889 + case 'innerHTML':
890 + case 'suppressContentEditableWarning':
891 + case 'suppressHydrationWarning':
892 + case 'style':
893 + // we advance to the next prop
894 + continue;
895 +
896 + // Name remapped props used by hoistable tag types
897 + case 'className': {
898 + if (__DEV__) {
899 + checkAttributeStringCoercion(propValue, propName);
900 + }
901 + if (node.getAttribute('class') !== '' + propValue) continue nodeLoop;
902 + break;
903 + }
904 + case 'httpEquiv': {
905 + if (__DEV__) {
906 + checkAttributeStringCoercion(propValue, propName);
907 + }
908 + if (node.getAttribute('http-equiv') !== '' + propValue)
909 + continue nodeLoop;
910 + break;
911 + }
912 +
913 + // Booleanish props used by hoistable tag types
914 + case 'contentEditable':
915 + case 'draggable':
916 + case 'spellCheck': {
917 + if (__DEV__) {
918 + checkAttributeStringCoercion(propValue, propName);
919 + }
920 + if (node.getAttribute(propName) !== '' + propValue) continue nodeLoop;
921 + break;
922 + }
923 +
924 + // Boolean props used by hoistable tag types
925 + case 'async':
926 + case 'defer':
927 + case 'disabled':
928 + case 'hidden':
929 + case 'noModule':
930 + case 'scoped':
931 + case 'itemScope':
932 + if (propValue !== node.hasAttribute(propName)) continue nodeLoop;
933 + break;
934 +
935 + // The following properties are left out because they do not apply to
936 + // the current set of hoistable types. They may have special handling
937 + // requirements if they end up applying to a hoistable type in the future
938 + // case 'acceptCharset':
939 + // case 'value':
940 + // case 'allowFullScreen':
941 + // case 'autoFocus':
942 + // case 'autoPlay':
943 + // case 'controls':
944 + // case 'default':
945 + // case 'disablePictureInPicture':
946 + // case 'disableRemotePlayback':
947 + // case 'formNoValidate':
948 + // case 'loop':
949 + // case 'noValidate':
950 + // case 'open':
951 + // case 'playsInline':
952 + // case 'readOnly':
953 + // case 'required':
954 + // case 'reversed':
955 + // case 'seamless':
956 + // case 'multiple':
957 + // case 'selected':
958 + // case 'capture':
959 + // case 'download':
960 + // case 'cols':
961 + // case 'rows':
962 + // case 'size':
963 + // case 'span':
964 + // case 'rowSpan':
965 + // case 'start':
966 +
967 + default:
968 + if (isAttributeNameSafe(propName)) {
969 + const attributeName = propName;
970 + if (propValue == null && node.hasAttribute(attributeName))
971 + continue nodeLoop;
972 + if (__DEV__) {
973 + checkAttributeStringCoercion(propValue, attributeName);
974 + }
975 + if (node.getAttribute(attributeName) !== '' + (propValue: any))
976 + continue nodeLoop;
977 + }
978 + }
979 + checkedAttributes++;
980 + }
981 +
982 + if (node.attributes.length !== checkedAttributes) {
983 + // We didn't match ever attribute so we abandon this node
984 + continue nodeLoop;
985 + }
986 +
987 + // We found a matching instance. We can return early after marking it
988 + markNodeAsResource(node);
989 + return node;
990 }
991 +
992 + // There is no matching instance to hydrate, we create it now
993 + const instance = createElement(type, props, ownerDocument, HTML_NAMESPACE);
994 + setInitialProperties(instance, type, props);
995 + precacheFiberNode(internalInstanceHandle, instance);
996 + markNodeAsResource(instance);
997 +
998 + (ownerDocument.head: any).insertBefore(
999 + instance,
1000 + type === 'title' ? ownerDocument.querySelector('head > title') : null,
1001 + );
1002 + return instance;
1003 +}
1004 +
1005 +export function mountHoistable(
1006 + hoistableRoot: HoistableRoot,
1007 + type: HoistableTagType,
1008 + instance: Instance,
1009 +): void {
1010 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
1011 + (ownerDocument.head: any).insertBefore(
1012 + instance,
1013 + type === 'title' ? ownerDocument.querySelector('head > title') : null,
1014 + );
1015 +}
1016 +
1017 +export function unmountHoistable(instance: Instance): void {
1018 + (instance.parentNode: any).removeChild(instance);
1019 }
1020
1021 // When passing user input into querySelector(All) the embedded string must not alter
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+121 -75
@@ -27,6 +27,7 @@ import {
27 isContainerMarkedAsRoot,
28 detachDeletedInstance,
29 isMarkedResource,
30 + markNodeAsResource,
31 } from './ReactDOMComponentTree';
32 export {detachDeletedInstance};
33 import {hasRole} from './DOMAccessibilityRoles';
@@ -54,7 +55,11 @@ import {
55 setEnabled as ReactBrowserEventEmitterSetEnabled,
56 getEventPriority,
57 } from '../events/ReactDOMEventListener';
57 -import {getChildNamespace, SVG_NAMESPACE} from '../shared/DOMNamespaces';
58 +import {
59 + getChildNamespace,
60 + SVG_NAMESPACE,
61 + HTML_NAMESPACE,
62 +} from '../shared/DOMNamespaces';
63 import {
64 ELEMENT_NODE,
65 TEXT_NODE,
@@ -75,7 +80,7 @@ import {
80 } from 'shared/ReactFeatureFlags';
81 import {
82 HostComponent,
78 - HostResource,
83 + HostHoistable,
84 HostText,
85 HostSingleton,
86 } from 'react-reconciler/src/ReactWorkTags';
@@ -89,7 +94,6 @@ import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
94 import {
95 prepareToRenderResources,
96 cleanupAfterRenderResources,
92 - clearRootResources,
97 } from './ReactDOMFloatClient';
98 import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
99
@@ -133,6 +137,7 @@ export type Container =
137 | interface extends DocumentFragment {_reactRootContainer?: FiberRoot};
138 export type Instance = Element;
139 export type TextInstance = Text;
140 +export type {HoistableRoot} from './ReactDOMFloatClient';
141 export interface SuspenseInstance extends Comment {
142 _reactRetry?: () => void;
143 }
@@ -263,6 +268,25 @@ export function resetAfterCommit(containerInfo: Container): void {
268 selectionInformation = null;
269 }
270
271 +export function createHoistableInstance(
272 + type: string,
273 + props: Props,
274 + rootContainerInstance: Container,
275 + internalInstanceHandle: Object,
276 +): Instance {
277 + const domElement: Instance = createElement(
278 + type,
279 + props,
280 + rootContainerInstance,
281 + HTML_NAMESPACE,
282 + );
283 + precacheFiberNode(internalInstanceHandle, domElement);
284 + updateFiberProps(domElement, props);
285 + setInitialProperties(domElement, type, props);
286 + markNodeAsResource(domElement);
287 + return domElement;
288 +}
289 +
290 export function createInstance(
291 type: string,
292 props: Props,
@@ -716,17 +740,10 @@ export function clearContainer(container: Container): void {
740 if (enableHostSingletons) {
741 const nodeType = container.nodeType;
742 if (nodeType === DOCUMENT_NODE) {
719 - clearRootResources(container);
743 clearContainerSparingly(container);
744 } else if (nodeType === ELEMENT_NODE) {
745 switch (container.nodeName) {
723 - case 'HEAD': {
724 - // If we are clearing document.head as a container we are essentially clearing everything
725 - // that was hoisted to the head and should forget the instances that will no longer be in the DOM
726 - clearRootResources(container);
727 - // fall through to clear child contents
728 - }
729 - // eslint-disable-next-line-no-fallthrough
746 + case 'HEAD':
747 case 'HTML':
748 case 'BODY':
749 clearContainerSparingly(container);
@@ -933,7 +950,6 @@ function getNextHydratable(node: ?Node) {
950 // developer on how to fix.
951 case 'TITLE':
952 case 'META':
936 - case 'BASE':
953 case 'HTML':
954 case 'HEAD':
955 case 'BODY': {
@@ -975,8 +991,7 @@ function getNextHydratable(node: ?Node) {
991 const element: Element = (node: any);
992 switch (element.tagName) {
993 case 'TITLE':
978 - case 'META':
979 - case 'BASE': {
994 + case 'META': {
995 continue;
996 }
997 case 'LINK': {
@@ -1445,7 +1460,7 @@ export function matchAccessibilityRole(node: Instance, role: string): boolean {
1460
1461 export function getTextContent(fiber: Fiber): string | null {
1462 switch (fiber.tag) {
1448 - case HostResource:
1463 + case HostHoistable:
1464 case HostSingleton:
1465 case HostComponent:
1466 let textContent = '';
@@ -1563,7 +1578,7 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
1578
1579 export const supportsResources = true;
1580
1566 -export function isHostResourceType(
1581 +export function isHostHoistableType(
1582 type: string,
1583 props: RawProps,
1584 hostContext: HostContext,
@@ -1581,100 +1596,127 @@ export function isHostResourceType(
1596 namespace = hostContextProd;
1597 }
1598 switch (type) {
1584 - case 'base':
1585 - case 'meta': {
1586 - return true;
1587 - }
1599 + case 'meta':
1600 case 'title': {
1601 return namespace !== SVG_NAMESPACE;
1602 }
1591 - case 'link': {
1592 - const {onLoad, onError} = props;
1593 - if (onLoad || onError) {
1603 + case 'style': {
1604 + if (
1605 + typeof props.precedence !== 'string' ||
1606 + typeof props.href !== 'string' ||
1607 + props.href === '' ||
1608 + namespace === SVG_NAMESPACE
1609 + ) {
1610 if (__DEV__) {
1611 if (outsideHostContainerContext) {
1612 console.error(
1597 - 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
1598 - ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1599 - ' somewhere in the <body>.',
1600 - );
1601 - } else if (namespace === SVG_NAMESPACE) {
1602 - console.error(
1603 - 'Cannot render a <link> with onLoad or onError listeners as a descendent of <svg>.' +
1604 - ' Try removing onLoad={...} and onError={...} or moving it above the <svg> ancestor.',
1613 + 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key.' +
1614 + ' React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that' +
1615 + ' does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' +
1616 + ' Note that hoisting <style> tags is considered an advanced feature that most will not use directly.' +
1617 + ' Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style>' +
1618 + ' to the <style> tag.',
1619 );
1620 }
1621 }
1622 return false;
1623 }
1624 + return true;
1625 + }
1626 + case 'link': {
1627 + if (
1628 + typeof props.rel !== 'string' ||
1629 + typeof props.href !== 'string' ||
1630 + props.href === '' ||
1631 + props.onLoad ||
1632 + props.onError ||
1633 + namespace === SVG_NAMESPACE
1634 + ) {
1635 + if (__DEV__) {
1636 + if (
1637 + props.rel === 'stylesheet' &&
1638 + typeof props.precedence === 'string'
1639 + ) {
1640 + validateLinkPropsForStyleResource(props);
1641 + }
1642 + if (outsideHostContainerContext) {
1643 + if (
1644 + typeof props.rel !== 'string' ||
1645 + typeof props.href !== 'string' ||
1646 + props.href === ''
1647 + ) {
1648 + console.error(
1649 + 'Cannot render a <link> outside the main document without a `rel` and `href` prop.' +
1650 + ' Try adding a `rel` and/or `href` prop to this <link> or moving the link into the <head> tag',
1651 + );
1652 + } else if (props.onError || props.onLoad) {
1653 + console.error(
1654 + 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
1655 + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1656 + ' somewhere in the <body>.',
1657 + );
1658 + }
1659 + }
1660 + }
1661 + return false;
1662 + }
1663 switch (props.rel) {
1664 case 'stylesheet': {
1612 - const {href, precedence, disabled} = props;
1665 + const {precedence, disabled} = props;
1666 if (__DEV__) {
1614 - validateLinkPropsForStyleResource(props);
1667 if (typeof precedence !== 'string') {
1668 if (outsideHostContainerContext) {
1669 console.error(
1670 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
1671 ' Consider adding precedence="default" or moving it into the root <head> tag.',
1672 );
1621 - } else if (namespace === SVG_NAMESPACE) {
1622 - console.error(
1623 - 'Cannot render a <link rel="stylesheet" /> as a descendent of an <svg> element without knowing its precedence.' +
1624 - ' Consider adding precedence="default" or moving it above the <svg> ancestor.',
1625 - );
1673 }
1674 }
1675 }
1629 - return (
1630 - typeof href === 'string' &&
1631 - typeof precedence === 'string' &&
1632 - disabled == null
1633 - );
1676 + return typeof precedence === 'string' && disabled == null;
1677 }
1678 default: {
1636 - const {rel, href} = props;
1637 - return typeof href === 'string' && typeof rel === 'string';
1679 + return true;
1680 }
1681 }
1682 }
1683 case 'script': {
1642 - // We don't validate because it is valid to use async with onLoad/onError unlike combining
1643 - // precedence with these for style resources
1644 - const {src, async, onLoad, onError} = props;
1645 - if (__DEV__) {
1646 - if (async !== true) {
1647 - if (outsideHostContainerContext) {
1648 - console.error(
1649 - 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
1650 - ' Try adding async="" or moving it into the root <head> tag.',
1651 - );
1652 - } else if (namespace === SVG_NAMESPACE) {
1653 - console.error(
1654 - 'Cannot render a sync or defer <script> as a descendent of an <svg> element.' +
1655 - ' Try adding async="" or moving it above the ancestor <svg> element.',
1656 - );
1657 - }
1658 - } else if (onLoad || onError) {
1684 + if (
1685 + props.async !== true ||
1686 + props.onLoad ||
1687 + props.onError ||
1688 + typeof props.src !== 'string' ||
1689 + !props.src ||
1690 + namespace === SVG_NAMESPACE
1691 + ) {
1692 + if (__DEV__) {
1693 if (outsideHostContainerContext) {
1660 - console.error(
1661 - 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
1662 - ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1663 - ' somewhere in the <body>.',
1664 - );
1665 - } else if (namespace === SVG_NAMESPACE) {
1666 - console.error(
1667 - 'Cannot render a <script> with onLoad or onError listeners as a descendent of an <svg> element.' +
1668 - ' Try removing onLoad={...} and onError={...} or moving it above the ancestor <svg> element.',
1669 - );
1694 + if (props.async !== true) {
1695 + console.error(
1696 + 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
1697 + ' Try adding async="" or moving it into the root <head> tag.',
1698 + );
1699 + } else if (props.onLoad || props.onError) {
1700 + console.error(
1701 + 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
1702 + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1703 + ' somewhere in the <body>.',
1704 + );
1705 + } else {
1706 + console.error(
1707 + 'Cannot render a <script> outside the main document without `async={true}` and a non-empty `src` prop.' +
1708 + ' Ensure there is a valid `src` and either make the script async or move it into the root <head> tag or' +
1709 + ' somewhere in the <body>.',
1710 + );
1711 + }
1712 }
1713 }
1714 + return false;
1715 }
1673 - return (async: any) && typeof src === 'string' && !onLoad && !onError;
1716 + return true;
1717 }
1718 case 'noscript':
1676 - case 'template':
1677 - case 'style': {
1719 + case 'template': {
1720 if (__DEV__) {
1721 if (outsideHostContainerContext) {
1722 console.error(
@@ -1702,9 +1744,13 @@ export function resetRendererAfterRender() {
1744 }
1745
1746 export {
1747 + getHoistableRoot,
1748 getResource,
1749 acquireResource,
1750 releaseResource,
1751 + hydrateHoistable,
1752 + mountHoistable,
1753 + unmountHoistable,
1754 } from './ReactDOMFloatClient';
1755
1756 // -------------------
packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
+5 -5
@@ -31,7 +31,7 @@ import {
31 HostRoot,
32 HostPortal,
33 HostComponent,
34 - HostResource,
34 + HostHoistable,
35 HostSingleton,
36 HostText,
37 ScopeComponent,
@@ -623,7 +623,7 @@ export function dispatchEventForPluginEventSystem(
623 if (
624 parentTag === HostComponent ||
625 parentTag === HostText ||
626 - (enableFloat ? parentTag === HostResource : false) ||
626 + (enableFloat ? parentTag === HostHoistable : false) ||
627 (enableHostSingletons ? parentTag === HostSingleton : false)
628 ) {
629 node = ancestorInst = parentNode;
@@ -681,7 +681,7 @@ export function accumulateSinglePhaseListeners(
681 // Handle listeners that are on HostComponents (i.e. <div>)
682 if (
683 (tag === HostComponent ||
684 - (enableFloat ? tag === HostResource : false) ||
684 + (enableFloat ? tag === HostHoistable : false) ||
685 (enableHostSingletons ? tag === HostSingleton : false)) &&
686 stateNode !== null
687 ) {
@@ -795,7 +795,7 @@ export function accumulateTwoPhaseListeners(
795 // Handle listeners that are on HostComponents (i.e. <div>)
796 if (
797 (tag === HostComponent ||
798 - (enableFloat ? tag === HostResource : false) ||
798 + (enableFloat ? tag === HostHoistable : false) ||
799 (enableHostSingletons ? tag === HostSingleton : false)) &&
800 stateNode !== null
801 ) {
@@ -902,7 +902,7 @@ function accumulateEnterLeaveListenersForEvent(
902 }
903 if (
904 (tag === HostComponent ||
905 - (enableFloat ? tag === HostResource : false) ||
905 + (enableFloat ? tag === HostHoistable : false) ||
906 (enableHostSingletons ? tag === HostSingleton : false)) &&
907 stateNode !== null
908 ) {
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js deleted
-982
@@ -1,982 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import {
11 - validatePreloadResourceDifference,
12 - validateStyleResourceDifference,
13 - validateStyleAndHintProps,
14 - validateScriptResourceDifference,
15 - validateScriptAndHintProps,
16 - validateLinkPropsForStyleResource,
17 - validateLinkPropsForPreloadResource,
18 - validatePreloadArguments,
19 - validatePreinitArguments,
20 -} from '../shared/ReactDOMResourceValidation';
21 -
22 -type Props = {[string]: mixed};
23 -
24 -type ResourceType = 'style' | 'font' | 'script';
25 -
26 -type PreloadProps = {
27 - rel: 'preload',
28 - as: ResourceType,
29 - href: string,
30 - [string]: mixed,
31 -};
32 -type PreloadResource = {
33 - type: 'preload',
34 - as: ResourceType,
35 - href: string,
36 - props: PreloadProps,
37 - flushed: boolean,
38 -};
39 -
40 -type StyleProps = {
41 - rel: 'stylesheet',
42 - href: string,
43 - 'data-precedence': string,
44 - [string]: mixed,
45 -};
46 -type StyleResource = {
47 - type: 'style',
48 - href: string,
49 - precedence: string,
50 - props: StyleProps,
51 -
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 -
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 -type TitleProps = {
72 - [string]: mixed,
73 -};
74 -type TitleResource = {
75 - type: 'title',
76 - props: TitleProps,
77 -
78 - flushed: boolean,
79 -};
80 -
81 -type MetaProps = {
82 - [string]: mixed,
83 -};
84 -type MetaResource = {
85 - type: 'meta',
86 - key: string,
87 - props: MetaProps,
88 -
89 - flushed: boolean,
90 -};
91 -
92 -type LinkProps = {
93 - href: string,
94 - rel: string,
95 - [string]: mixed,
96 -};
97 -type LinkResource = {
98 - type: 'link',
99 - props: LinkProps,
100 -
101 - flushed: boolean,
102 -};
103 -
104 -type BaseResource = {
105 - type: 'base',
106 - props: Props,
107 -
108 - flushed: boolean,
109 -};
110 -
111 -export type Resource = PreloadResource | StyleResource | ScriptResource;
112 -export type HeadResource =
113 - | TitleResource
114 - | MetaResource
115 - | LinkResource
116 - | BaseResource;
117 -
118 -export type Resources = {
119 - // Request local cache
120 - preloadsMap: Map<string, PreloadResource>,
121 - stylesMap: Map<string, StyleResource>,
122 - scriptsMap: Map<string, ScriptResource>,
123 - headsMap: Map<string, HeadResource>,
124 -
125 - // Flushing queues for Resource dependencies
126 - charset: null | MetaResource,
127 - bases: Set<BaseResource>,
128 - preconnects: Set<LinkResource>,
129 - fontPreloads: Set<PreloadResource>,
130 - // usedImagePreloads: Set<PreloadResource>,
131 - precedences: Map<string, Set<StyleResource>>,
132 - usedStylePreloads: Set<PreloadResource>,
133 - scripts: Set<ScriptResource>,
134 - usedScriptPreloads: Set<PreloadResource>,
135 - explicitStylePreloads: Set<PreloadResource>,
136 - // explicitImagePreloads: Set<PreloadResource>,
137 - explicitScriptPreloads: Set<PreloadResource>,
138 - headResources: Set<HeadResource>,
139 -
140 - // cache for tracking structured meta tags
141 - structuredMetaKeys: Map<string, MetaResource>,
142 -
143 - // Module-global-like reference for current boundary resources
144 - boundaryResources: ?BoundaryResources,
145 - ...
146 -};
147 -
148 -// @TODO add bootstrap script to implicit preloads
149 -export function createResources(): Resources {
150 - return {
151 - // persistent
152 - preloadsMap: new Map(),
153 - stylesMap: new Map(),
154 - scriptsMap: new Map(),
155 - headsMap: new Map(),
156 -
157 - // cleared on flush
158 - charset: null,
159 - bases: new Set(),
160 - preconnects: new Set(),
161 - fontPreloads: new Set(),
162 - // usedImagePreloads: new Set(),
163 - precedences: new Map(),
164 - usedStylePreloads: new Set(),
165 - scripts: new Set(),
166 - usedScriptPreloads: new Set(),
167 - explicitStylePreloads: new Set(),
168 - // explicitImagePreloads: new Set(),
169 - explicitScriptPreloads: new Set(),
170 - headResources: new Set(),
171 -
172 - // cache for tracking structured meta tags
173 - structuredMetaKeys: new Map(),
174 -
175 - // like a module global for currently rendering boundary
176 - boundaryResources: null,
177 - };
178 -}
179 -
180 -export type BoundaryResources = Set<StyleResource>;
181 -
182 -export function createBoundaryResources(): BoundaryResources {
183 - return new Set();
184 -}
185 -
186 -let currentResources: null | Resources = null;
187 -const currentResourcesStack = [];
188 -
189 -export function prepareToRenderResources(resources: Resources) {
190 - currentResourcesStack.push(currentResources);
191 - currentResources = resources;
192 -}
193 -
194 -export function finishRenderingResources() {
195 - currentResources = currentResourcesStack.pop();
196 -}
197 -
198 -export function setCurrentlyRenderingBoundaryResourcesTarget(
199 - resources: Resources,
200 - boundaryResources: null | BoundaryResources,
201 -) {
202 - resources.boundaryResources = boundaryResources;
203 -}
204 -
205 -export const ReactDOMServerFloatDispatcher = {
206 - preload,
207 - preinit,
208 -};
209 -
210 -type PreloadAs = ResourceType;
211 -type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
212 -function preload(href: string, options: PreloadOptions) {
213 - if (!currentResources) {
214 - // While we expect that preload calls are primarily going to be observed
215 - // during render because effects and events don't run on the server it is
216 - // still possible that these get called in module scope. This is valid on
217 - // the client since there is still a document to interact with but on the
218 - // server we need a request to associate the call to. Because of this we
219 - // simply return and do not warn.
220 - return;
221 - }
222 - const resources = currentResources;
223 - if (__DEV__) {
224 - validatePreloadArguments(href, options);
225 - }
226 - if (
227 - typeof href === 'string' &&
228 - href &&
229 - typeof options === 'object' &&
230 - options !== null
231 - ) {
232 - const as = options.as;
233 - let resource = resources.preloadsMap.get(href);
234 - if (resource) {
235 - if (__DEV__) {
236 - const originallyImplicit =
237 - (resource: any)._dev_implicit_construction === true;
238 - const latestProps = preloadPropsFromPreloadOptions(href, as, options);
239 - validatePreloadResourceDifference(
240 - resource.props,
241 - originallyImplicit,
242 - latestProps,
243 - false,
244 - );
245 - }
246 - } else {
247 - resource = createPreloadResource(
248 - resources,
249 - href,
250 - as,
251 - preloadPropsFromPreloadOptions(href, as, options),
252 - );
253 - }
254 - switch (as) {
255 - case 'font': {
256 - resources.fontPreloads.add(resource);
257 - break;
258 - }
259 - case 'style': {
260 - resources.explicitStylePreloads.add(resource);
261 - break;
262 - }
263 - case 'script': {
264 - resources.explicitScriptPreloads.add(resource);
265 - break;
266 - }
267 - }
268 - }
269 -}
270 -
271 -type PreinitAs = 'style' | 'script';
272 -type PreinitOptions = {
273 - as: PreinitAs,
274 - precedence?: string,
275 - crossOrigin?: string,
276 - integrity?: string,
277 -};
278 -function preinit(href: string, options: PreinitOptions): void {
279 - if (!currentResources) {
280 - // While we expect that preinit calls are primarily going to be observed
281 - // during render because effects and events don't run on the server it is
282 - // still possible that these get called in module scope. This is valid on
283 - // the client since there is still a document to interact with but on the
284 - // server we need a request to associate the call to. Because of this we
285 - // simply return and do not warn.
286 - return;
287 - }
288 - preinitImpl(currentResources, href, options);
289 -}
290 -
291 -// On the server, preinit may be called outside of render when sending an
292 -// external SSR runtime as part of the initial resources payload. Since this
293 -// is an internal React call, we do not need to use the resources stack.
294 -export function preinitImpl(
295 - resources: Resources,
296 - href: string,
297 - options: PreinitOptions,
298 -): void {
299 - if (__DEV__) {
300 - validatePreinitArguments(href, options);
301 - }
302 - if (
303 - typeof href === 'string' &&
304 - href &&
305 - typeof options === 'object' &&
306 - options !== null
307 - ) {
308 - const as = options.as;
309 - switch (as) {
310 - case 'style': {
311 - let resource = resources.stylesMap.get(href);
312 - if (resource) {
313 - if (__DEV__) {
314 - const latestProps = stylePropsFromPreinitOptions(
315 - href,
316 - resource.precedence,
317 - options,
318 - );
319 - validateStyleResourceDifference(resource.props, latestProps);
320 - }
321 - } else {
322 - const precedence = options.precedence || 'default';
323 - const resourceProps = stylePropsFromPreinitOptions(
324 - href,
325 - precedence,
326 - options,
327 - );
328 - resource = createStyleResource(
329 - resources,
330 - href,
331 - precedence,
332 - resourceProps,
333 - );
334 - }
335 - resource.set.add(resource);
336 - resources.explicitStylePreloads.add(resource.hint);
337 -
338 - return;
339 - }
340 - case 'script': {
341 - const src = href;
342 - let resource = resources.scriptsMap.get(src);
343 - if (resource) {
344 - if (__DEV__) {
345 - const latestProps = scriptPropsFromPreinitOptions(src, options);
346 - validateScriptResourceDifference(resource.props, latestProps);
347 - }
348 - } else {
349 - const scriptProps = scriptPropsFromPreinitOptions(src, options);
350 - resource = createScriptResource(resources, src, scriptProps);
351 - resources.scripts.add(resource);
352 - }
353 - return;
354 - }
355 - }
356 - }
357 -}
358 -
359 -function preloadPropsFromPreloadOptions(
360 - href: string,
361 - as: ResourceType,
362 - options: PreloadOptions,
363 -): PreloadProps {
364 - return {
365 - href,
366 - rel: 'preload',
367 - as,
368 - crossOrigin: as === 'font' ? '' : options.crossOrigin,
369 - integrity: options.integrity,
370 - };
371 -}
372 -
373 -function preloadPropsFromRawProps(
374 - href: string,
375 - as: ResourceType,
376 - rawProps: Props,
377 -): PreloadProps {
378 - const props: PreloadProps = Object.assign({}, rawProps);
379 - props.href = href;
380 - props.rel = 'preload';
381 - props.as = as;
382 - if (as === 'font') {
383 - // Font preloads always need CORS anonymous mode so we set it here
384 - // regardless of the props provided. This should warn elsewhere in
385 - // dev
386 - props.crossOrigin = '';
387 - }
388 - return props;
389 -}
390 -
391 -function preloadAsStylePropsFromProps(
392 - href: string,
393 - props: Props | StyleProps,
394 -): PreloadProps {
395 - return {
396 - rel: 'preload',
397 - as: 'style',
398 - href: href,
399 - crossOrigin: props.crossOrigin,
400 - integrity: props.integrity,
401 - media: props.media,
402 - hrefLang: props.hrefLang,
403 - referrerPolicy: props.referrerPolicy,
404 - };
405 -}
406 -
407 -function preloadAsScriptPropsFromProps(
408 - href: string,
409 - props: Props | ScriptProps,
410 -): PreloadProps {
411 - return {
412 - rel: 'preload',
413 - as: 'script',
414 - href,
415 - crossOrigin: props.crossOrigin,
416 - integrity: props.integrity,
417 - referrerPolicy: props.referrerPolicy,
418 - };
419 -}
420 -
421 -function createPreloadResource(
422 - resources: Resources,
423 - href: string,
424 - as: ResourceType,
425 - props: PreloadProps,
426 -): PreloadResource {
427 - const {preloadsMap} = resources;
428 - if (__DEV__) {
429 - if (preloadsMap.has(href)) {
430 - console.error(
431 - 'createPreloadResource was called when a preload Resource matching the same href already exists. This is a bug in React.',
432 - );
433 - }
434 - }
435 -
436 - const resource = {
437 - type: 'preload',
438 - as,
439 - href,
440 - flushed: false,
441 - props,
442 - };
443 - preloadsMap.set(href, resource);
444 - return resource;
445 -}
446 -
447 -function stylePropsFromRawProps(
448 - href: string,
449 - precedence: string,
450 - rawProps: Props,
451 -): StyleProps {
452 - const props: StyleProps = Object.assign({}, rawProps);
453 - props.href = href;
454 - props.rel = 'stylesheet';
455 - props['data-precedence'] = precedence;
456 - delete props.precedence;
457 -
458 - return props;
459 -}
460 -
461 -function stylePropsFromPreinitOptions(
462 - href: string,
463 - precedence: string,
464 - options: PreinitOptions,
465 -): StyleProps {
466 - return {
467 - rel: 'stylesheet',
468 - href,
469 - 'data-precedence': precedence,
470 - crossOrigin: options.crossOrigin,
471 - };
472 -}
473 -
474 -function createStyleResource(
475 - resources: Resources,
476 - href: string,
477 - precedence: string,
478 - props: StyleProps,
479 -): StyleResource {
480 - if (__DEV__) {
481 - if (resources.stylesMap.has(href)) {
482 - console.error(
483 - 'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
484 - );
485 - }
486 - }
487 - const {stylesMap, preloadsMap, precedences} = resources;
488 -
489 - // If this is the first time we've seen this precedence we encode it's position in our set even though
490 - // we don't add the resource to this set yet
491 - let precedenceSet = precedences.get(precedence);
492 - if (!precedenceSet) {
493 - precedenceSet = new Set();
494 - precedences.set(precedence, precedenceSet);
495 - }
496 -
497 - let hint = preloadsMap.get(href);
498 - if (hint) {
499 - // If a preload for this style Resource already exists there are certain props we want to adopt
500 - // on the style Resource, primarily focussed on making sure the style network pathways utilize
501 - // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
502 - // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
503 - adoptPreloadPropsForStyleProps(props, hint.props);
504 -
505 - if (__DEV__) {
506 - validateStyleAndHintProps(
507 - hint.props,
508 - props,
509 - (hint: any)._dev_implicit_construction,
510 - );
511 - }
512 - } else {
513 - const preloadResourceProps = preloadAsStylePropsFromProps(href, props);
514 - hint = createPreloadResource(
515 - resources,
516 - href,
517 - 'style',
518 - preloadResourceProps,
519 - );
520 - if (__DEV__) {
521 - (hint: any)._dev_implicit_construction = true;
522 - }
523 - resources.explicitStylePreloads.add(hint);
524 - }
525 -
526 - const resource = {
527 - type: 'style',
528 - href,
529 - precedence,
530 - flushed: false,
531 - inShell: false,
532 - props,
533 - hint,
534 - set: precedenceSet,
535 - };
536 - stylesMap.set(href, resource);
537 -
538 - return resource;
539 -}
540 -
541 -function adoptPreloadPropsForStyleProps(
542 - resourceProps: StyleProps,
543 - preloadProps: PreloadProps,
544 -): void {
545 - if (resourceProps.crossOrigin == null)
546 - resourceProps.crossOrigin = preloadProps.crossOrigin;
547 - if (resourceProps.referrerPolicy == null)
548 - resourceProps.referrerPolicy = preloadProps.referrerPolicy;
549 - if (resourceProps.title == null) resourceProps.title = preloadProps.title;
550 -}
551 -
552 -function scriptPropsFromPreinitOptions(
553 - src: string,
554 - options: PreinitOptions,
555 -): ScriptProps {
556 - return {
557 - src,
558 - async: true,
559 - crossOrigin: options.crossOrigin,
560 - integrity: options.integrity,
561 - };
562 -}
563 -
564 -function scriptPropsFromRawProps(src: string, rawProps: Props): ScriptProps {
565 - const props = Object.assign({}, rawProps);
566 - props.src = src;
567 - return props;
568 -}
569 -
570 -function createScriptResource(
571 - resources: Resources,
572 - src: string,
573 - props: ScriptProps,
574 -): ScriptResource {
575 - if (__DEV__) {
576 - if (resources.scriptsMap.has(src)) {
577 - console.error(
578 - 'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
579 - );
580 - }
581 - }
582 - const {scriptsMap, preloadsMap} = resources;
583 -
584 - let hint = preloadsMap.get(src);
585 - if (hint) {
586 - // If a preload for this style Resource already exists there are certain props we want to adopt
587 - // on the style Resource, primarily focussed on making sure the style network pathways utilize
588 - // the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
589 - // and a stylesheet the stylesheet will make a new request even if the preload had already loaded
590 - adoptPreloadPropsForScriptProps(props, hint.props);
591 -
592 - if (__DEV__) {
593 - validateScriptAndHintProps(
594 - hint.props,
595 - props,
596 - (hint: any)._dev_implicit_construction,
597 - );
598 - }
599 - } else {
600 - const preloadResourceProps = preloadAsScriptPropsFromProps(src, props);
601 - hint = createPreloadResource(
602 - resources,
603 - src,
604 - 'script',
605 - preloadResourceProps,
606 - );
607 - if (__DEV__) {
608 - (hint: any)._dev_implicit_construction = true;
609 - }
610 - resources.explicitScriptPreloads.add(hint);
611 - }
612 -
613 - const resource = {
614 - type: 'script',
615 - src,
616 - flushed: false,
617 - props,
618 - hint,
619 - };
620 - scriptsMap.set(src, resource);
621 -
622 - return resource;
623 -}
624 -
625 -function adoptPreloadPropsForScriptProps(
626 - resourceProps: ScriptProps,
627 - preloadProps: PreloadProps,
628 -): void {
629 - if (resourceProps.crossOrigin == null)
630 - resourceProps.crossOrigin = preloadProps.crossOrigin;
631 - if (resourceProps.referrerPolicy == null)
632 - resourceProps.referrerPolicy = preloadProps.referrerPolicy;
633 - if (resourceProps.integrity == null)
634 - resourceProps.integrity = preloadProps.integrity;
635 -}
636 -
637 -function titlePropsFromRawProps(
638 - child: string | number,
639 - rawProps: Props,
640 -): TitleProps {
641 - const props = Object.assign({}, rawProps);
642 - props.children = child;
643 - return props;
644 -}
645 -
646 -export function resourcesFromElement(type: string, props: Props): boolean {
647 - if (!currentResources) {
648 - throw new Error(
649 - '"currentResources" was expected to exist. This is a bug in React.',
650 - );
651 - }
652 - const resources = currentResources;
653 - switch (type) {
654 - case 'title': {
655 - const children = props.children;
656 - let child;
657 - if (Array.isArray(children)) {
658 - child = children.length === 1 ? children[0] : null;
659 - } else {
660 - child = children;
661 - }
662 - if (
663 - typeof child !== 'function' &&
664 - typeof child !== 'symbol' &&
665 - child !== null &&
666 - child !== undefined
667 - ) {
668 - // eslint-disable-next-line react-internal/safe-string-coercion
669 - const childString = '' + (child: any);
670 - const key = 'title::' + childString;
671 - let resource = resources.headsMap.get(key);
672 - if (!resource) {
673 - resource = {
674 - type: 'title',
675 - props: titlePropsFromRawProps(childString, props),
676 - flushed: false,
677 - };
678 - resources.headsMap.set(key, resource);
679 - resources.headResources.add(resource);
680 - }
681 - }
682 - return true;
683 - }
684 - case 'meta': {
685 - let key, propertyPath;
686 - if (typeof props.charSet === 'string') {
687 - key = 'charSet';
688 - } else if (typeof props.content === 'string') {
689 - const contentKey = '::' + props.content;
690 - if (typeof props.httpEquiv === 'string') {
691 - key = 'httpEquiv::' + props.httpEquiv + contentKey;
692 - } else if (typeof props.name === 'string') {
693 - key = 'name::' + props.name + contentKey;
694 - } else if (typeof props.itemProp === 'string') {
695 - key = 'itemProp::' + props.itemProp + contentKey;
696 - } else if (typeof props.property === 'string') {
697 - const {property} = props;
698 - key = 'property::' + property + contentKey;
699 - propertyPath = property;
700 - const parentPath = property.split(':').slice(0, -1).join(':');
701 - const parentResource = resources.structuredMetaKeys.get(parentPath);
702 - if (parentResource) {
703 - key = parentResource.key + '::child::' + key;
704 - }
705 - }
706 - }
707 - if (key) {
708 - if (!resources.headsMap.has(key)) {
709 - const resource = {
710 - type: 'meta',
711 - key,
712 - props: Object.assign({}, props),
713 - flushed: false,
714 - };
715 - resources.headsMap.set(key, resource);
716 - if (key === 'charSet') {
717 - resources.charset = resource;
718 - } else {
719 - if (propertyPath) {
720 - resources.structuredMetaKeys.set(propertyPath, resource);
721 - }
722 - resources.headResources.add(resource);
723 - }
724 - }
725 - }
726 - return true;
727 - }
728 - case 'base': {
729 - const {target, href} = props;
730 - // We mirror the key construction on the client since we will likely unify
731 - // this code in the future to better guarantee key semantics are identical
732 - // in both environments
733 - let key = 'base';
734 - key += typeof href === 'string' ? `[href="${href}"]` : ':not([href])';
735 - key +=
736 - typeof target === 'string' ? `[target="${target}"]` : ':not([target])';
737 - if (!resources.headsMap.has(key)) {
738 - const resource = {
739 - type: 'base',
740 - props: Object.assign({}, props),
741 - flushed: false,
742 - };
743 - resources.headsMap.set(key, resource);
744 - resources.bases.add(resource);
745 - }
746 - return true;
747 - }
748 - }
749 - return false;
750 -}
751 -
752 -// Construct a resource from link props.
753 -export function resourcesFromLink(props: Props): boolean {
754 - if (!currentResources) {
755 - throw new Error(
756 - '"currentResources" was expected to exist. This is a bug in React.',
757 - );
758 - }
759 - const resources = currentResources;
760 -
761 - const {rel, href} = props;
762 - if (!href || typeof href !== 'string' || !rel || typeof rel !== 'string') {
763 - return false;
764 - }
765 -
766 - let key = '';
767 - switch (rel) {
768 - case 'stylesheet': {
769 - const {onLoad, onError, precedence, disabled} = props;
770 - if (
771 - typeof precedence !== 'string' ||
772 - onLoad ||
773 - onError ||
774 - disabled != null
775 - ) {
776 - // This stylesheet is either not opted into Resource semantics or has conflicting properties which
777 - // disqualify it for such. We can still create a preload resource to help it load faster on the
778 - // client
779 - if (__DEV__) {
780 - validateLinkPropsForStyleResource(props);
781 - }
782 - let preloadResource = resources.preloadsMap.get(href);
783 - if (!preloadResource) {
784 - preloadResource = createPreloadResource(
785 - resources,
786 - href,
787 - 'style',
788 - preloadAsStylePropsFromProps(href, props),
789 - );
790 - if (__DEV__) {
791 - (preloadResource: any)._dev_implicit_construction = true;
792 - }
793 - resources.usedStylePreloads.add(preloadResource);
794 - }
795 - return false;
796 - } else {
797 - // We are able to convert this link element to a resource exclusively. We construct the relevant Resource
798 - // and return true indicating that this link was fully consumed.
799 - let resource = resources.stylesMap.get(href);
800 -
801 - if (resource) {
802 - if (__DEV__) {
803 - const resourceProps = stylePropsFromRawProps(
804 - href,
805 - precedence,
806 - props,
807 - );
808 - adoptPreloadPropsForStyleProps(resourceProps, resource.hint.props);
809 - validateStyleResourceDifference(resource.props, resourceProps);
810 - }
811 - } else {
812 - const resourceProps = stylePropsFromRawProps(href, precedence, props);
813 - resource = createStyleResource(
814 - // $FlowFixMe[incompatible-call] found when upgrading Flow
815 - currentResources,
816 - href,
817 - precedence,
818 - resourceProps,
819 - );
820 - resources.usedStylePreloads.add(resource.hint);
821 - }
822 - if (resources.boundaryResources) {
823 - resources.boundaryResources.add(resource);
824 - } else {
825 - resource.set.add(resource);
826 - }
827 - return true;
828 - }
829 - }
830 - case 'preload': {
831 - const {as} = props;
832 - switch (as) {
833 - case 'script':
834 - case 'style':
835 - case 'font': {
836 - if (__DEV__) {
837 - validateLinkPropsForPreloadResource(props);
838 - }
839 - let resource = resources.preloadsMap.get(href);
840 - if (resource) {
841 - if (__DEV__) {
842 - const originallyImplicit =
843 - (resource: any)._dev_implicit_construction === true;
844 - const latestProps = preloadPropsFromRawProps(href, as, props);
845 - validatePreloadResourceDifference(
846 - resource.props,
847 - originallyImplicit,
848 - latestProps,
849 - false,
850 - );
851 - }
852 - } else {
853 - resource = createPreloadResource(
854 - resources,
855 - href,
856 - as,
857 - preloadPropsFromRawProps(href, as, props),
858 - );
859 - switch (as) {
860 - case 'script': {
861 - resources.explicitScriptPreloads.add(resource);
862 - break;
863 - }
864 - case 'style': {
865 - resources.explicitStylePreloads.add(resource);
866 - break;
867 - }
868 - case 'font': {
869 - resources.fontPreloads.add(resource);
870 - break;
871 - }
872 - }
873 - }
874 - return true;
875 - }
876 - }
877 - break;
878 - }
879 - }
880 - if (props.onLoad || props.onError) {
881 - // When a link has these props we can't treat it is a Resource but if we rendered it on the
882 - // server it would look like a Resource in the rendered html (the onLoad/onError aren't emitted)
883 - // Instead we expect the client to insert them rather than hydrate them which also guarantees
884 - // that the onLoad and onError won't fire before the event handlers are attached
885 - return true;
886 - }
887 -
888 - const sizes = typeof props.sizes === 'string' ? props.sizes : '';
889 - const media = typeof props.media === 'string' ? props.media : '';
890 - key =
891 - 'rel:' + rel + '::href:' + href + '::sizes:' + sizes + '::media:' + media;
892 - let resource = resources.headsMap.get(key);
893 - if (!resource) {
894 - resource = {
895 - type: 'link',
896 - props: Object.assign({}, props),
897 - flushed: false,
898 - };
899 - resources.headsMap.set(key, resource);
900 - switch (rel) {
901 - case 'preconnect':
902 - case 'dns-prefetch': {
903 - resources.preconnects.add(resource);
904 - break;
905 - }
906 - default: {
907 - resources.headResources.add(resource);
908 - }
909 - }
910 - }
911 - return true;
912 -}
913 -
914 -// Construct a resource from link props.
915 -export function resourcesFromScript(props: Props): boolean {
916 - if (!currentResources) {
917 - throw new Error(
918 - '"currentResources" was expected to exist. This is a bug in React.',
919 - );
920 - }
921 - const resources = currentResources;
922 - const {src, async, onLoad, onError} = props;
923 - if (!src || typeof src !== 'string') {
924 - return false;
925 - }
926 -
927 - if (async) {
928 - if (onLoad || onError) {
929 - if (__DEV__) {
930 - // validate
931 - }
932 - let preloadResource = resources.preloadsMap.get(src);
933 - if (!preloadResource) {
934 - preloadResource = createPreloadResource(
935 - resources,
936 - src,
937 - 'script',
938 - preloadAsScriptPropsFromProps(src, props),
939 - );
940 - if (__DEV__) {
941 - (preloadResource: any)._dev_implicit_construction = true;
942 - }
943 - resources.usedScriptPreloads.add(preloadResource);
944 - }
945 - } else {
946 - let resource = resources.scriptsMap.get(src);
947 - if (resource) {
948 - if (__DEV__) {
949 - const latestProps = scriptPropsFromRawProps(src, props);
950 - adoptPreloadPropsForScriptProps(latestProps, resource.hint.props);
951 - validateScriptResourceDifference(resource.props, latestProps);
952 - }
953 - } else {
954 - const resourceProps = scriptPropsFromRawProps(src, props);
955 - resource = createScriptResource(resources, src, resourceProps);
956 - resources.scripts.add(resource);
957 - }
958 - }
959 - return true;
960 - }
961 -
962 - return false;
963 -}
964 -
965 -export function hoistResources(
966 - resources: Resources,
967 - source: BoundaryResources,
968 -): void {
969 - const currentBoundaryResources = resources.boundaryResources;
970 - if (currentBoundaryResources) {
971 - source.forEach(resource => currentBoundaryResources.add(resource));
972 - source.clear();
973 - }
974 -}
975 -
976 -export function hoistResourcesToRoot(
977 - resources: Resources,
978 - boundaryResources: BoundaryResources,
979 -): void {
980 - boundaryResources.forEach(resource => resource.set.add(resource));
981 - boundaryResources.clear();
982 -}
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+1851 -451
@@ -8,8 +8,6 @@
8 */
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 -import type {Resources, BoundaryResources} from './ReactDOMFloatServer';
12 -export type {Resources, BoundaryResources};
11
12 import {
13 checkHtmlStringCoercion,
@@ -62,23 +60,6 @@ import hasOwnProperty from 'shared/hasOwnProperty';
60 import sanitizeURL from '../shared/sanitizeURL';
61 import isArray from 'shared/isArray';
62
65 -import {
66 - preinitImpl,
67 - prepareToRenderResources,
68 - finishRenderingResources,
69 - resourcesFromElement,
70 - resourcesFromLink,
71 - resourcesFromScript,
72 - ReactDOMServerFloatDispatcher,
73 -} from './ReactDOMFloatServer';
74 -export {
75 - createResources,
76 - createBoundaryResources,
77 - setCurrentlyRenderingBoundaryResourcesTarget,
78 - hoistResources,
79 - hoistResourcesToRoot,
80 -} from './ReactDOMFloatServer';
81 -
63 import {
64 clientRenderBoundary as clientRenderFunction,
65 completeBoundary as completeBoundaryFunction,
@@ -86,15 +67,36 @@ import {
67 completeSegment as completeSegmentFunction,
68 } from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings';
69
70 +import {
71 + getValueDescriptorExpectingObjectForWarning,
72 + getValueDescriptorExpectingEnumForWarning,
73 + describeDifferencesForStylesheets,
74 + describeDifferencesForStylesheetOverPreinit,
75 + describeDifferencesForScripts,
76 + describeDifferencesForScriptOverPreinit,
77 + describeDifferencesForPreinits,
78 + describeDifferencesForPreinitOverStylesheet,
79 + describeDifferencesForPreinitOverScript,
80 + describeDifferencesForPreloads,
81 + describeDifferencesForPreloadOverImplicitPreload,
82 +} from '../shared/ReactDOMResourceValidation';
83 +
84 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
85 const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
86
87 const ReactDOMServerDispatcher = enableFloat
93 - ? ReactDOMServerFloatDispatcher
88 + ? {
89 + preload,
90 + preinit,
91 + }
92 : {};
93
94 +let currentResources: null | Resources = null;
95 +const currentResourcesStack = [];
96 +
97 export function prepareToRender(resources: Resources): mixed {
97 - prepareToRenderResources(resources);
98 + currentResourcesStack.push(currentResources);
99 + currentResources = resources;
100
101 const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
102 ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
@@ -102,7 +104,7 @@ export function prepareToRender(resources: Resources): mixed {
104 }
105
106 export function cleanupAfterRender(previousDispatcher: mixed) {
105 - finishRenderingResources();
107 + currentResources = currentResourcesStack.pop();
108 ReactDOMCurrentDispatcher.current = previousDispatcher;
109 }
110
@@ -114,6 +116,13 @@ export type StreamingFormat = 0 | 1;
116 const ScriptStreamingFormat: StreamingFormat = 0;
117 const DataStreamingFormat: StreamingFormat = 1;
118
119 +export type InstructionState = number;
120 +const NothingSent /* */ = 0b0000;
121 +const SentCompleteSegmentFunction /* */ = 0b0001;
122 +const SentCompleteBoundaryFunction /* */ = 0b0010;
123 +const SentClientRenderFunction /* */ = 0b0100;
124 +const SentStyleInsertionFunction /* */ = 0b1000;
125 +
126 // Per response, global state that is not contextual to the rendering subtree.
127 export type ResponseState = {
128 bootstrapChunks: Array<Chunk | PrecomputedChunk>,
@@ -123,15 +132,27 @@ export type ResponseState = {
132 idPrefix: string,
133 nextSuspenseID: number,
134 streamingFormat: StreamingFormat,
135 +
136 // state for script streaming format, unused if using external runtime / data
137 startInlineScript: PrecomputedChunk,
128 - sentCompleteSegmentFunction: boolean,
129 - sentCompleteBoundaryFunction: boolean,
130 - sentClientRenderFunction: boolean,
131 - sentStyleInsertionFunction: boolean,
138 + instructions: InstructionState,
139 +
140 // state for data streaming format
141 externalRuntimeConfig: BootstrapScriptDescriptor | null,
142 +
143 + // preamble and postamble chunks and state
144 + htmlChunks: null | Array<Chunk | PrecomputedChunk>,
145 + headChunks: null | Array<Chunk | PrecomputedChunk>,
146 + hasBody: boolean,
147 +
148 + // Hoistable chunks
149 + charsetChunks: Array<Chunk | PrecomputedChunk>,
150 + preconnectChunks: Array<Chunk | PrecomputedChunk>,
151 + preloadChunks: Array<Chunk | PrecomputedChunk>,
152 + hoistableChunks: Array<Chunk | PrecomputedChunk>,
153 +
154 // We allow the legacy renderer to extend this object.
155 +
156 ...
157 };
158
@@ -268,11 +289,15 @@ export function createResponseState(
289 nextSuspenseID: 0,
290 streamingFormat,
291 startInlineScript: inlineScriptWithNonce,
271 - sentCompleteSegmentFunction: false,
272 - sentCompleteBoundaryFunction: false,
273 - sentClientRenderFunction: false,
274 - sentStyleInsertionFunction: false,
292 + instructions: NothingSent,
293 externalRuntimeConfig: externalRuntimeDesc,
294 + htmlChunks: null,
295 + headChunks: null,
296 + hasBody: false,
297 + charsetChunks: [],
298 + preconnectChunks: [],
299 + preloadChunks: [],
300 + hoistableChunks: [],
301 };
302 }
303
@@ -280,17 +305,20 @@ export function createResponseState(
305 // modes. We only include the variants as they matter for the sake of our purposes.
306 // We don't actually provide the namespace therefore we use constants instead of the string.
307 const ROOT_HTML_MODE = 0; // Used for the root most element tag.
283 -export const HTML_MODE = 1;
284 -const SVG_MODE = 2;
285 -const MATHML_MODE = 3;
286 -const HTML_TABLE_MODE = 4;
287 -const HTML_TABLE_BODY_MODE = 5;
288 -const HTML_TABLE_ROW_MODE = 6;
289 -const HTML_COLGROUP_MODE = 7;
308 +// We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it
309 +// still makes sense
310 +const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.
311 +export const HTML_MODE = 2;
312 +const SVG_MODE = 3;
313 +const MATHML_MODE = 4;
314 +const HTML_TABLE_MODE = 5;
315 +const HTML_TABLE_BODY_MODE = 6;
316 +const HTML_TABLE_ROW_MODE = 7;
317 +const HTML_COLGROUP_MODE = 8;
318 // We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
319 // still makes sense
320
293 -type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
321 +type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
322
323 // Lets us keep track of contextual state and pick it back up after suspending.
324 export type FormatContext = {
@@ -392,12 +420,16 @@ export function getChildFormatContext(
420 );
421 }
422 if (parentContext.insertionMode === ROOT_HTML_MODE) {
395 - // We've emitted the root and is now in plain HTML mode.
396 - return createFormatContext(
397 - HTML_MODE,
398 - null,
399 - parentContext.noscriptTagInScope,
400 - );
423 + if (type === 'html') {
424 + // We've emitted the root and is now in <html> mode.
425 + return createFormatContext(HTML_HTML_MODE, null, false);
426 + } else {
427 + // We've emitted the root and is now in plain HTML mode.
428 + return createFormatContext(HTML_MODE, null, false);
429 + }
430 + } else if (parentContext.insertionMode === HTML_HTML_MODE) {
431 + // We've emitted the document element and is now in plain HTML mode.
432 + return createFormatContext(HTML_MODE, null, false);
433 }
434 return parentContext;
435 }
@@ -487,9 +519,8 @@ const styleAttributeStart = stringToPrecomputedChunk(' style="');
519 const styleAssign = stringToPrecomputedChunk(':');
520 const styleSeparator = stringToPrecomputedChunk(';');
521
490 -function pushStyle(
522 +function pushStyleAttribute(
523 target: Array<Chunk | PrecomputedChunk>,
492 - responseState: ResponseState,
524 style: Object,
525 ): void {
526 if (typeof style !== 'object') {
@@ -577,13 +608,12 @@ const attributeEmptyString = stringToPrecomputedChunk('=""');
608
609 function pushAttribute(
610 target: Array<Chunk | PrecomputedChunk>,
580 - responseState: ResponseState,
611 name: string,
612 value: string | boolean | number | Function | Object, // not null or undefined
613 ): void {
614 switch (name) {
615 case 'style': {
586 - pushStyle(target, responseState, value);
616 + pushStyleAttribute(target, value);
617 return;
618 }
619 case 'defaultValue':
@@ -804,7 +834,6 @@ function checkSelectProp(props: any, propName: string) {
834 function pushStartSelect(
835 target: Array<Chunk | PrecomputedChunk>,
836 props: Object,
807 - responseState: ResponseState,
837 ): ReactNodeList {
838 if (__DEV__) {
839 checkControlledValueProps('select', props);
@@ -852,7 +881,7 @@ function pushStartSelect(
881 // These are set on the Context instead and applied to the nested options.
882 break;
883 default:
855 - pushAttribute(target, responseState, propKey, propValue);
884 + pushAttribute(target, propKey, propValue);
885 break;
886 }
887 }
@@ -894,7 +923,6 @@ const selectedMarkerAttribute = stringToPrecomputedChunk(' selected=""');
923 function pushStartOption(
924 target: Array<Chunk | PrecomputedChunk>,
925 props: Object,
897 - responseState: ResponseState,
926 formatContext: FormatContext,
927 ): ReactNodeList {
928 const selectedValue = formatContext.selectedValue;
@@ -938,7 +966,7 @@ function pushStartOption(
966 // We intentionally fallthrough to also set the attribute on the node.
967 // eslint-disable-next-line-no-fallthrough
968 default:
941 - pushAttribute(target, responseState, propKey, propValue);
969 + pushAttribute(target, propKey, propValue);
970 break;
971 }
972 }
@@ -997,7 +1025,6 @@ function pushStartOption(
1025 function pushInput(
1026 target: Array<Chunk | PrecomputedChunk>,
1027 props: Object,
1000 - responseState: ResponseState,
1028 ): ReactNodeList {
1029 if (__DEV__) {
1030 checkControlledValueProps('input', props);
@@ -1072,21 +1099,21 @@ function pushInput(
1099 value = propValue;
1100 break;
1101 default:
1075 - pushAttribute(target, responseState, propKey, propValue);
1102 + pushAttribute(target, propKey, propValue);
1103 break;
1104 }
1105 }
1106 }
1107
1108 if (checked !== null) {
1082 - pushAttribute(target, responseState, 'checked', checked);
1109 + pushAttribute(target, 'checked', checked);
1110 } else if (defaultChecked !== null) {
1084 - pushAttribute(target, responseState, 'checked', defaultChecked);
1111 + pushAttribute(target, 'checked', defaultChecked);
1112 }
1113 if (value !== null) {
1087 - pushAttribute(target, responseState, 'value', value);
1114 + pushAttribute(target, 'value', value);
1115 } else if (defaultValue !== null) {
1089 - pushAttribute(target, responseState, 'value', defaultValue);
1116 + pushAttribute(target, 'value', defaultValue);
1117 }
1118
1119 target.push(endOfStartTagSelfClosing);
@@ -1096,7 +1123,6 @@ function pushInput(
1123 function pushStartTextArea(
1124 target: Array<Chunk | PrecomputedChunk>,
1125 props: Object,
1099 - responseState: ResponseState,
1126 ): ReactNodeList {
1127 if (__DEV__) {
1128 checkControlledValueProps('textarea', props);
@@ -1143,7 +1169,7 @@ function pushStartTextArea(
1169 );
1170 // eslint-disable-next-line-no-fallthrough
1171 default:
1146 - pushAttribute(target, responseState, propKey, propValue);
1172 + pushAttribute(target, propKey, propValue);
1173 break;
1174 }
1175 }
@@ -1214,85 +1240,364 @@ function pushStartTextArea(
1240 return null;
1241 }
1242
1217 -function pushBase(
1243 +function pushMeta(
1244 target: Array<Chunk | PrecomputedChunk>,
1245 props: Object,
1246 responseState: ResponseState,
1247 textEmbedded: boolean,
1248 + insertionMode: InsertionMode,
1249 noscriptTagInScope: boolean,
1223 -): ReactNodeList {
1224 - if (
1225 - enableFloat &&
1226 - !noscriptTagInScope &&
1227 - resourcesFromElement('base', props)
1228 - ) {
1229 - if (textEmbedded) {
1230 - // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1231 - // to be safe and assume text will follow by inserting a textSeparator
1232 - target.push(textSeparator);
1250 +): null {
1251 + if (enableFloat) {
1252 + if (insertionMode === SVG_MODE || noscriptTagInScope) {
1253 + return pushSelfClosing(target, props, 'meta');
1254 + } else {
1255 + if (textEmbedded) {
1256 + // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1257 + // to be safe and assume text will follow by inserting a textSeparator
1258 + target.push(textSeparator);
1259 + }
1260 +
1261 + if (typeof props.charSet === 'string') {
1262 + return pushSelfClosing(responseState.charsetChunks, props, 'meta');
1263 + } else {
1264 + return pushSelfClosing(responseState.hoistableChunks, props, 'meta');
1265 + }
1266 }
1234 - // We have converted this link exclusively to a resource and no longer
1235 - // need to emit it
1236 - return null;
1267 + } else {
1268 + return pushSelfClosing(target, props, 'meta');
1269 }
1238 -
1239 - return pushSelfClosing(target, props, 'base', responseState);
1270 }
1271
1242 -function pushMeta(
1272 +function pushLink(
1273 target: Array<Chunk | PrecomputedChunk>,
1274 props: Object,
1275 responseState: ResponseState,
1276 + resources: Resources,
1277 textEmbedded: boolean,
1278 + insertionMode: InsertionMode,
1279 noscriptTagInScope: boolean,
1248 -): ReactNodeList {
1249 - if (
1250 - enableFloat &&
1251 - !noscriptTagInScope &&
1252 - resourcesFromElement('meta', props)
1253 - ) {
1254 - if (textEmbedded) {
1255 - // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1256 - // to be safe and assume text will follow by inserting a textSeparator
1257 - target.push(textSeparator);
1280 +): null {
1281 + if (enableFloat) {
1282 + const rel = props.rel;
1283 + const href = props.href;
1284 + const precedence = props.precedence;
1285 + if (
1286 + insertionMode === SVG_MODE ||
1287 + noscriptTagInScope ||
1288 + typeof rel !== 'string' ||
1289 + typeof href !== 'string' ||
1290 + href === ''
1291 + ) {
1292 + if (__DEV__) {
1293 + if (rel === 'stylesheet' && typeof props.precedence === 'string') {
1294 + if (typeof href !== 'string' || !href) {
1295 + console.error(
1296 + 'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and expected the `href` prop to be a non-empty string but ecountered %s instead. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop ensure there is a non-empty string `href` prop as well, otherwise remove the `precedence` prop.',
1297 + getValueDescriptorExpectingObjectForWarning(href),
1298 + );
1299 + }
1300 + }
1301 + }
1302 + pushLinkImpl(target, props);
1303 + return null;
1304 + }
1305 +
1306 + if (props.rel === 'stylesheet') {
1307 + // This <link> may hoistable as a Stylesheet Resource, otherwise it will emit in place
1308 + const key = getResourceKey('style', href);
1309 + if (
1310 + typeof precedence !== 'string' ||
1311 + props.disabled != null ||
1312 + props.onLoad ||
1313 + props.onError
1314 + ) {
1315 + // This stylesheet is either not opted into Resource semantics or has conflicting properties which
1316 + // disqualify it for such. We can still create a preload resource to help it load faster on the
1317 + // client
1318 + if (__DEV__) {
1319 + if (typeof precedence === 'string') {
1320 + if (props.disabled != null) {
1321 + console.error(
1322 + 'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and a `disabled` prop. The presence of the `disabled` prop indicates an intent to manage the stylesheet active state from your from your Component code and React will not hoist or deduplicate this stylesheet. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop remove the `disabled` prop, otherwise remove the `precedence` prop.',
1323 + );
1324 + } else if (props.onLoad || props.onError) {
1325 + const propDescription =
1326 + props.onLoad && props.onError
1327 + ? '`onLoad` and `onError` props'
1328 + : props.onLoad
1329 + ? '`onLoad` prop'
1330 + : '`onError` prop';
1331 + console.error(
1332 + 'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and %s. The presence of loading and error handlers indicates an intent to manage the stylesheet loading state from your from your Component code and React will not hoist or deduplicate this stylesheet. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop remove the %s, otherwise remove the `precedence` prop.',
1333 + propDescription,
1334 + propDescription,
1335 + );
1336 + }
1337 + }
1338 + }
1339 + let resource = resources.preloadsMap.get(key);
1340 + if (!resource) {
1341 + resource = {
1342 + type: 'preload',
1343 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
1344 + state: NoState,
1345 + props: preloadAsStylePropsFromProps(href, props),
1346 + };
1347 + resources.preloadsMap.set(key, resource);
1348 + if (__DEV__) {
1349 + markAsImplicitResourceDEV(resource, props, resource.props);
1350 + }
1351 + }
1352 + pushLinkImpl(resource.chunks, resource.props);
1353 + resources.usedStylesheets.add(resource);
1354 + return pushLinkImpl(target, props);
1355 + } else {
1356 + // This stylesheet refers to a Resource and we create a new one if necessary
1357 + let resource = resources.stylesMap.get(key);
1358 + if (__DEV__) {
1359 + const devResource = getAsResourceDEV(resource);
1360 + if (devResource) {
1361 + switch (devResource.__provenance) {
1362 + case 'rendered': {
1363 + const differenceDescription = describeDifferencesForStylesheets(
1364 + // Diff the props from the JSX element, not the derived resource props
1365 + props,
1366 + devResource.__originalProps,
1367 + );
1368 + if (differenceDescription) {
1369 + console.error(
1370 + 'React encountered a <link rel="stylesheet" href="%s" .../> with a `precedence` prop that has props that conflict' +
1371 + ' with another hoistable stylesheet with the same `href`. When using `precedence` with <link rel="stylsheet" .../>' +
1372 + ' the props from the first encountered instance will be used and props from later instances will be ignored.' +
1373 + ' Update the props on either <link rel="stylesheet" .../> instance so they agree.%s',
1374 + href,
1375 + differenceDescription,
1376 + );
1377 + }
1378 + break;
1379 + }
1380 + case 'preinit': {
1381 + const differenceDescription =
1382 + describeDifferencesForStylesheetOverPreinit(
1383 + // Diff the props from the JSX element, not the derived resource props
1384 + props,
1385 + devResource.__propsEquivalent,
1386 + );
1387 + if (differenceDescription) {
1388 + console.error(
1389 + 'React encountered a <link rel="stylesheet" precedence="%s" href="%s" .../> with props that conflict' +
1390 + ' with the options provided to `ReactDOM.preinit("%s", { as: "style", ... })`. React will use the first props or preinitialization' +
1391 + ' options encountered when rendering a hoistable stylesheet with a particular `href` and will ignore any newer props or' +
1392 + ' options. The first instance of this stylesheet resource was created using the `ReactDOM.preinit()` function.' +
1393 + ' Please note, `ReactDOM.preinit()` is modeled off of module import assertions capabilities and does not support' +
1394 + ' arbitrary props. If you need to have props not included with the preinit options you will need to rely on rendering' +
1395 + ' <link> tags only.%s',
1396 + precedence,
1397 + href,
1398 + href,
1399 + differenceDescription,
1400 + );
1401 + }
1402 + break;
1403 + }
1404 + }
1405 + }
1406 + }
1407 + if (!resource) {
1408 + const resourceProps = stylesheetPropsFromRawProps(props);
1409 + const preloadResource = resources.preloadsMap.get(key);
1410 + if (preloadResource) {
1411 + // If we already had a preload we don't want that resource to flush directly.
1412 + // We let the newly created resource govern flushing.
1413 + preloadResource.state |= Blocked;
1414 + adoptPreloadPropsForStylesheetProps(
1415 + resourceProps,
1416 + preloadResource.props,
1417 + );
1418 + }
1419 + resource = {
1420 + type: 'stylesheet',
1421 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
1422 + state: resources.boundaryResources ? Blocked : NoState,
1423 + props: resourceProps,
1424 + };
1425 + resources.stylesMap.set(key, resource);
1426 + if (__DEV__) {
1427 + markAsRenderedResourceDEV(resource, props);
1428 + }
1429 + let precedenceSet = resources.precedences.get(precedence);
1430 + if (!precedenceSet) {
1431 + precedenceSet = new Set();
1432 + resources.precedences.set(precedence, precedenceSet);
1433 + }
1434 + precedenceSet.add(resource);
1435 + }
1436 + if (resources.boundaryResources) {
1437 + resources.boundaryResources.add(resource);
1438 + }
1439 + if (textEmbedded) {
1440 + // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1441 + // to be safe and assume text will follow by inserting a textSeparator
1442 + target.push(textSeparator);
1443 + }
1444 + return null;
1445 + }
1446 + } else if (props.onLoad || props.onError) {
1447 + // When using load handlers we cannot hoist and need to emit links in place
1448 + return pushLinkImpl(target, props);
1449 + } else {
1450 + // We can hoist this link so we may need to emit a text separator.
1451 + // @TODO refactor text separators so we don't have to defensively add
1452 + // them when we don't end up emitting a tag as a result of pushStartInstance
1453 + if (textEmbedded) {
1454 + // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1455 + // to be safe and assume text will follow by inserting a textSeparator
1456 + target.push(textSeparator);
1457 + }
1458 +
1459 + switch (props.rel) {
1460 + case 'preconnect':
1461 + case 'dns-prefetch':
1462 + return pushLinkImpl(responseState.preconnectChunks, props);
1463 + case 'preload':
1464 + return pushLinkImpl(responseState.preloadChunks, props);
1465 + default:
1466 + return pushLinkImpl(responseState.hoistableChunks, props);
1467 + }
1468 + }
1469 + } else {
1470 + return pushLinkImpl(target, props);
1471 + }
1472 +}
1473 +
1474 +function pushLinkImpl(
1475 + target: Array<Chunk | PrecomputedChunk>,
1476 + props: Object,
1477 +): null {
1478 + target.push(startChunkForTag('link'));
1479 +
1480 + for (const propKey in props) {
1481 + if (hasOwnProperty.call(props, propKey)) {
1482 + const propValue = props[propKey];
1483 + if (propValue == null) {
1484 + continue;
1485 + }
1486 + switch (propKey) {
1487 + case 'children':
1488 + case 'dangerouslySetInnerHTML':
1489 + throw new Error(
1490 + `${'link'} is a self-closing tag and must neither have \`children\` nor ` +
1491 + 'use `dangerouslySetInnerHTML`.',
1492 + );
1493 + // eslint-disable-next-line-no-fallthrough
1494 + default:
1495 + pushAttribute(target, propKey, propValue);
1496 + break;
1497 + }
1498 }
1259 - // We have converted this link exclusively to a resource and no longer
1260 - // need to emit it
1261 - return null;
1499 }
1500
1264 - return pushSelfClosing(target, props, 'meta', responseState);
1501 + target.push(endOfStartTagSelfClosing);
1502 + return null;
1503 }
1504
1267 -function pushLink(
1505 +function pushStyle(
1506 target: Array<Chunk | PrecomputedChunk>,
1507 props: Object,
1270 - responseState: ResponseState,
1508 + resources: Resources,
1509 textEmbedded: boolean,
1510 + insertionMode: InsertionMode,
1511 noscriptTagInScope: boolean,
1512 ): ReactNodeList {
1274 - if (enableFloat && !noscriptTagInScope && resourcesFromLink(props)) {
1513 + if (__DEV__) {
1514 + if (hasOwnProperty.call(props, 'children')) {
1515 + const children = props.children;
1516 +
1517 + const child = Array.isArray(children)
1518 + ? children.length < 2
1519 + ? children[0]
1520 + : null
1521 + : children;
1522 +
1523 + if (
1524 + typeof child === 'function' ||
1525 + typeof child === 'symbol' ||
1526 + Array.isArray(child)
1527 + ) {
1528 + const childType =
1529 + typeof child === 'function'
1530 + ? 'a Function'
1531 + : typeof child === 'symbol'
1532 + ? 'a Sybmol'
1533 + : 'an Array';
1534 + console.error(
1535 + 'React expect children of <style> tags to be a string, number, or object with a `toString` method but found %s instead. ' +
1536 + 'In browsers style Elements can only have `Text` Nodes as children.',
1537 + childType,
1538 + );
1539 + }
1540 + }
1541 + }
1542 + if (enableFloat) {
1543 + const precedence = props.precedence;
1544 + const href = props.href;
1545 +
1546 + if (
1547 + insertionMode === SVG_MODE ||
1548 + noscriptTagInScope ||
1549 + typeof precedence !== 'string' ||
1550 + typeof href !== 'string' ||
1551 + href === ''
1552 + ) {
1553 + // This style tag is not able to be turned into a Style Resource
1554 + return pushStyleImpl(target, props);
1555 + }
1556 +
1557 + const key = getResourceKey('style', href);
1558 + let resource = resources.stylesMap.get(key);
1559 + if (!resource) {
1560 + resource = {
1561 + type: 'style',
1562 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
1563 + state: resources.boundaryResources ? Blocked : NoState,
1564 + props: styleTagPropsFromRawProps(props),
1565 + };
1566 + resources.stylesMap.set(key, resource);
1567 + if (__DEV__) {
1568 + markAsRenderedResourceDEV(resource, props);
1569 + }
1570 + pushStyleImpl(resource.chunks, resource.props);
1571 +
1572 + let precedenceSet = resources.precedences.get(precedence);
1573 + if (!precedenceSet) {
1574 + precedenceSet = new Set();
1575 + resources.precedences.set(precedence, precedenceSet);
1576 + }
1577 + precedenceSet.add(resource);
1578 + if (resources.boundaryResources) {
1579 + resources.boundaryResources.add(resource);
1580 + }
1581 + }
1582 +
1583 if (textEmbedded) {
1584 // This link follows text but we aren't writing a tag. while not as efficient as possible we need
1585 // to be safe and assume text will follow by inserting a textSeparator
1586 target.push(textSeparator);
1587 }
1280 - // We have converted this link exclusively to a resource and no longer
1281 - // need to emit it
1282 - return null;
1588 + } else {
1589 + return pushStartGenericElement(target, props, 'style');
1590 }
1284 -
1285 - return pushLinkImpl(target, props, responseState);
1591 }
1592
1288 -function pushLinkImpl(
1593 +function pushStyleImpl(
1594 target: Array<Chunk | PrecomputedChunk>,
1595 props: Object,
1291 - responseState: ResponseState,
1596 ): ReactNodeList {
1293 - const isStylesheet = props.rel === 'stylesheet';
1294 - target.push(startChunkForTag('link'));
1597 + target.push(startChunkForTag('style'));
1598
1599 + let children = null;
1600 + let innerHTML = null;
1601 for (const propKey in props) {
1602 if (hasOwnProperty.call(props, propKey)) {
1603 const propValue = props[propKey];
@@ -1301,27 +1606,35 @@ function pushLinkImpl(
1606 }
1607 switch (propKey) {
1608 case 'children':
1609 + children = propValue;
1610 + break;
1611 case 'dangerouslySetInnerHTML':
1305 - throw new Error(
1306 - `${'link'} is a self-closing tag and must neither have \`children\` nor ` +
1307 - 'use `dangerouslySetInnerHTML`.',
1308 - );
1309 - case 'precedence': {
1310 - if (enableFloat && isStylesheet) {
1311 - // precedence is a reversed property for stylesheets to opt-into resource semantcs
1312 - continue;
1313 - }
1314 - // intentionally fall through
1315 - }
1316 - // eslint-disable-next-line-no-fallthrough
1612 + innerHTML = propValue;
1613 + break;
1614 default:
1318 - pushAttribute(target, responseState, propKey, propValue);
1615 + pushAttribute(target, propKey, propValue);
1616 break;
1617 }
1618 }
1619 }
1620 + target.push(endOfStartTag);
1621
1324 - target.push(endOfStartTagSelfClosing);
1622 + const child = Array.isArray(children)
1623 + ? children.length < 2
1624 + ? children[0]
1625 + : null
1626 + : children;
1627 + if (
1628 + typeof child !== 'function' &&
1629 + typeof child !== 'symbol' &&
1630 + child !== null &&
1631 + child !== undefined
1632 + ) {
1633 + // eslint-disable-next-line react-internal/safe-string-coercion
1634 + target.push(stringToChunk(escapeTextForBrowser('' + child)));
1635 + }
1636 + pushInnerHTML(target, innerHTML, children);
1637 + target.push(endTag1, stringToChunk('style'), endTag2);
1638 return null;
1639 }
1640
@@ -1329,8 +1642,7 @@ function pushSelfClosing(
1642 target: Array<Chunk | PrecomputedChunk>,
1643 props: Object,
1644 tag: string,
1332 - responseState: ResponseState,
1333 -): ReactNodeList {
1645 +): null {
1646 target.push(startChunkForTag(tag));
1647
1648 for (const propKey in props) {
@@ -1348,7 +1660,7 @@ function pushSelfClosing(
1660 );
1661 // eslint-disable-next-line-no-fallthrough
1662 default:
1351 - pushAttribute(target, responseState, propKey, propValue);
1663 + pushAttribute(target, propKey, propValue);
1664 break;
1665 }
1666 }
@@ -1361,7 +1673,6 @@ function pushSelfClosing(
1673 function pushStartMenuItem(
1674 target: Array<Chunk | PrecomputedChunk>,
1675 props: Object,
1364 - responseState: ResponseState,
1676 ): ReactNodeList {
1677 target.push(startChunkForTag('menuitem'));
1678
@@ -1379,7 +1690,7 @@ function pushStartMenuItem(
1690 );
1691 // eslint-disable-next-line-no-fallthrough
1692 default:
1382 - pushAttribute(target, responseState, propKey, propValue);
1693 + pushAttribute(target, propKey, propValue);
1694 break;
1695 }
1696 }
@@ -1397,67 +1708,73 @@ function pushTitle(
1708 noscriptTagInScope: boolean,
1709 ): ReactNodeList {
1710 if (__DEV__) {
1400 - const children = props.children;
1401 - const childForValidation =
1402 - Array.isArray(children) && children.length < 2
1403 - ? children[0] || null
1711 + if (hasOwnProperty.call(props, 'children')) {
1712 + const children = props.children;
1713 +
1714 + const child = Array.isArray(children)
1715 + ? children.length < 2
1716 + ? children[0]
1717 + : null
1718 : children;
1405 - if (Array.isArray(children) && children.length > 1) {
1406 - console.error(
1407 - 'A title element received an array with more than 1 element as children. ' +
1408 - 'In browsers title Elements can only have Text Nodes as children. If ' +
1409 - 'the children being rendered output more than a single text node in aggregate the browser ' +
1410 - 'will display markup and comments as text in the title and hydration will likely fail and ' +
1411 - 'fall back to client rendering',
1412 - );
1413 - } else if (
1414 - childForValidation != null &&
1415 - childForValidation.$$typeof != null
1416 - ) {
1417 - console.error(
1418 - 'A title element received a React element for children. ' +
1419 - 'In the browser title Elements can only have Text Nodes as children. If ' +
1420 - 'the children being rendered output more than a single text node in aggregate the browser ' +
1421 - 'will display markup and comments as text in the title and hydration will likely fail and ' +
1422 - 'fall back to client rendering',
1423 - );
1424 - } else if (
1425 - childForValidation != null &&
1426 - typeof childForValidation !== 'string' &&
1427 - typeof childForValidation !== 'number'
1428 - ) {
1429 - console.error(
1430 - 'A title element received a value that was not a string or number for children. ' +
1431 - 'In the browser title Elements can only have Text Nodes as children. If ' +
1432 - 'the children being rendered output more than a single text node in aggregate the browser ' +
1433 - 'will display markup and comments as text in the title and hydration will likely fail and ' +
1434 - 'fall back to client rendering',
1435 - );
1719 +
1720 + if (Array.isArray(children) && children.length > 1) {
1721 + console.error(
1722 + 'React expects the `children` prop of <title> tags to be a string, number, or object with a novel `toString` method but found an Array with length %s instead.' +
1723 + ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value' +
1724 + ' which is why Arrays of length greater than 1 are not supported. When using JSX it can be commong to combine text nodes and value nodes.' +
1725 + ' For example: <title>hello {nameOfUser}</title>. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' +
1726 + ' is using this form try rewriting it using a template string: <title>{`hello ${nameOfUser}`}</title>.',
1727 + children.length,
1728 + );
1729 + } else if (typeof child === 'function' || typeof child === 'symbol') {
1730 + const childType =
1731 + typeof child === 'function' ? 'a Function' : 'a Sybmol';
1732 + console.error(
1733 + 'React expect children of <title> tags to be a string, number, or object with a novel `toString` method but found %s instead.' +
1734 + ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title>' +
1735 + ' tags to a single string value.',
1736 + childType,
1737 + );
1738 + } else if (child && child.toString === {}.toString) {
1739 + if (child.$$typeof != null) {
1740 + console.error(
1741 + 'React expects the `children` prop of <title> tags to be a string, number, or object with a novel `toString` method but found an object that appears to be' +
1742 + ' a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to' +
1743 + ' be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is' +
1744 + ' a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.',
1745 + );
1746 + } else {
1747 + console.error(
1748 + 'React expects the `children` prop of <title> tags to be a string, number, or object with a novel `toString` method but found an object that does not implement' +
1749 + ' a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags' +
1750 + ' to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title>' +
1751 + ' is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.',
1752 + );
1753 + }
1754 + }
1755 }
1756 }
1757
1439 - if (
1440 - enableFloat &&
1441 - // title is valid in SVG so we avoid resour
1442 - insertionMode !== SVG_MODE &&
1443 - !noscriptTagInScope &&
1444 - resourcesFromElement('title', props)
1445 - ) {
1446 - // We have converted this link exclusively to a resource and no longer
1447 - // need to emit it
1448 - return null;
1758 + if (enableFloat) {
1759 + if (insertionMode !== SVG_MODE && !noscriptTagInScope) {
1760 + pushTitleImpl(responseState.hoistableChunks, props);
1761 + return null;
1762 + } else {
1763 + return pushTitleImpl(target, props);
1764 + }
1765 + } else {
1766 + return pushTitleImpl(target, props);
1767 }
1450 - return pushTitleImpl(target, props, responseState);
1768 }
1769
1770 function pushTitleImpl(
1771 target: Array<Chunk | PrecomputedChunk>,
1772 props: Object,
1456 - responseState: ResponseState,
1773 ): null {
1774 target.push(startChunkForTag('title'));
1775
1776 let children = null;
1777 + let innerHTML = null;
1778 for (const propKey in props) {
1779 if (hasOwnProperty.call(props, propKey)) {
1780 const propValue = props[propKey];
@@ -1469,12 +1786,10 @@ function pushTitleImpl(
1786 children = propValue;
1787 break;
1788 case 'dangerouslySetInnerHTML':
1472 - throw new Error(
1473 - '`dangerouslySetInnerHTML` does not make sense on <title>.',
1474 - );
1475 - // eslint-disable-next-line-no-fallthrough
1789 + innerHTML = propValue;
1790 + break;
1791 default:
1477 - pushAttribute(target, responseState, propKey, propValue);
1792 + pushAttribute(target, propKey, propValue);
1793 break;
1794 }
1795 }
@@ -1495,6 +1810,7 @@ function pushTitleImpl(
1810 // eslint-disable-next-line react-internal/safe-string-coercion
1811 target.push(stringToChunk(escapeTextForBrowser('' + child)));
1812 }
1813 + pushInnerHTML(target, innerHTML, children);
1814 target.push(endTag1, stringToChunk('title'), endTag2);
1815 return null;
1816 }
@@ -1502,7 +1818,6 @@ function pushTitleImpl(
1818 function pushStartTitle(
1819 target: Array<Chunk | PrecomputedChunk>,
1820 props: Object,
1505 - responseState: ResponseState,
1821 ): ReactNodeList {
1822 target.push(startChunkForTag('title'));
1823
@@ -1523,7 +1838,7 @@ function pushStartTitle(
1838 );
1839 // eslint-disable-next-line-no-fallthrough
1840 default:
1526 - pushAttribute(target, responseState, propKey, propValue);
1841 + pushAttribute(target, propKey, propValue);
1842 break;
1843 }
1844 }
@@ -1574,62 +1889,195 @@ function pushStartTitle(
1889
1890 function pushStartHead(
1891 target: Array<Chunk | PrecomputedChunk>,
1577 - preamble: Array<Chunk | PrecomputedChunk>,
1892 props: Object,
1579 - tag: string,
1893 responseState: ResponseState,
1894 + insertionMode: InsertionMode,
1895 ): ReactNodeList {
1582 - return pushStartGenericElement(
1583 - enableFloat ? preamble : target,
1584 - props,
1585 - tag,
1586 - responseState,
1587 - );
1896 + if (enableFloat) {
1897 + if (insertionMode < HTML_MODE && responseState.headChunks === null) {
1898 + // This <head> is the Document.head and should be part of the preamble
1899 + responseState.headChunks = [];
1900 + return pushStartGenericElement(responseState.headChunks, props, 'head');
1901 + } else {
1902 + // This <head> is deep and is likely just an error. we emit it inline though.
1903 + // Validation should warn that this tag is the the wrong spot.
1904 + return pushStartGenericElement(target, props, 'head');
1905 + }
1906 + } else {
1907 + return pushStartGenericElement(target, props, 'head');
1908 + }
1909 }
1910
1911 function pushStartHtml(
1912 target: Array<Chunk | PrecomputedChunk>,
1592 - preamble: Array<Chunk | PrecomputedChunk>,
1913 props: Object,
1594 - tag: string,
1914 responseState: ResponseState,
1596 - formatContext: FormatContext,
1915 + insertionMode: InsertionMode,
1916 ): ReactNodeList {
1598 - target = enableFloat ? preamble : target;
1599 - if (formatContext.insertionMode === ROOT_HTML_MODE) {
1600 - // If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
1601 - // then we also emit the DOCTYPE as part of the root content as a convenience for
1602 - // rendering the whole document.
1603 - target.push(DOCTYPE);
1917 + if (enableFloat) {
1918 + if (insertionMode === ROOT_HTML_MODE && responseState.htmlChunks === null) {
1919 + // This <html> is the Document.documentElement and should be part of the preamble
1920 + responseState.htmlChunks = [DOCTYPE];
1921 + return pushStartGenericElement(responseState.htmlChunks, props, 'html');
1922 + } else {
1923 + // This <html> is deep and is likely just an error. we emit it inline though.
1924 + // Validation should warn that this tag is the the wrong spot.
1925 + return pushStartGenericElement(target, props, 'html');
1926 + }
1927 + } else {
1928 + if (insertionMode === ROOT_HTML_MODE) {
1929 + // If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
1930 + // then we also emit the DOCTYPE as part of the root content as a convenience for
1931 + // rendering the whole document.
1932 + target.push(DOCTYPE);
1933 + }
1934 + return pushStartGenericElement(target, props, 'html');
1935 }
1605 - return pushStartGenericElement(target, props, tag, responseState);
1936 }
1937
1938 function pushScript(
1939 target: Array<Chunk | PrecomputedChunk>,
1940 props: Object,
1611 - responseState: ResponseState,
1941 + resources: Resources,
1942 textEmbedded: boolean,
1943 + insertionMode: InsertionMode,
1944 noscriptTagInScope: boolean,
1945 ): null {
1615 - if (enableFloat && !noscriptTagInScope && resourcesFromScript(props)) {
1946 + if (enableFloat) {
1947 + if (
1948 + insertionMode === SVG_MODE ||
1949 + noscriptTagInScope ||
1950 + typeof props.src !== 'string' ||
1951 + !props.src
1952 + ) {
1953 + // This script will not be a resource nor can it be preloaded, we bailout early
1954 + // and emit it in place.
1955 + return pushScriptImpl(target, props);
1956 + }
1957 +
1958 + const src = props.src;
1959 + const key = getResourceKey('script', src);
1960 + if (props.async !== true || props.onLoad || props.onError) {
1961 + // We can't resourcify scripts with load listeners. To avoid ambiguity with
1962 + // other Resourcified async scripts on the server we omit them from the server
1963 + // stream and expect them to be inserted during hydration on the client.
1964 + // We can still preload them however so the client can start fetching the script
1965 + // as soon as possible
1966 + let resource = resources.preloadsMap.get(key);
1967 + if (!resource) {
1968 + resource = {
1969 + type: 'preload',
1970 + chunks: [],
1971 + state: NoState,
1972 + props: preloadAsScriptPropsFromProps(props.src, props),
1973 + };
1974 + resources.preloadsMap.set(key, resource);
1975 + if (__DEV__) {
1976 + markAsImplicitResourceDEV(resource, props, resource.props);
1977 + }
1978 + resources.usedScripts.add(resource);
1979 + pushLinkImpl(resource.chunks, resource.props);
1980 + }
1981 +
1982 + if (props.async !== true) {
1983 + // This is not an async script, we can preloaded it but it still needs to
1984 + // be emitted in place since it needs to hydrate on the client
1985 + pushScriptImpl(target, props);
1986 + return null;
1987 + }
1988 + } else {
1989 + // We can make this <script> into a ScriptResource
1990 + let resource = resources.scriptsMap.get(key);
1991 + if (__DEV__) {
1992 + const devResource = getAsResourceDEV(resource);
1993 + if (devResource) {
1994 + switch (devResource.__provenance) {
1995 + case 'rendered': {
1996 + const differenceDescription = describeDifferencesForScripts(
1997 + // Diff the props from the JSX element, not the derived resource props
1998 + props,
1999 + devResource.__originalProps,
2000 + );
2001 + if (differenceDescription) {
2002 + console.error(
2003 + 'React encountered a <script async={true} src="%s" .../> that has props that conflict' +
2004 + ' with another hoistable script with the same `src`. When rendering hoistable scripts (async scripts without any loading handlers)' +
2005 + ' the props from the first encountered instance will be used and props from later instances will be ignored.' +
2006 + ' Update the props on both <script async={true} .../> instance so they agree.%s',
2007 + src,
2008 + differenceDescription,
2009 + );
2010 + }
2011 + break;
2012 + }
2013 + case 'preinit': {
2014 + const differenceDescription =
2015 + describeDifferencesForScriptOverPreinit(
2016 + // Diff the props from the JSX element, not the derived resource props
2017 + props,
2018 + devResource.__propsEquivalent,
2019 + );
2020 + if (differenceDescription) {
2021 + console.error(
2022 + 'React encountered a <script async={true} src="%s" .../> with props that conflict' +
2023 + ' with the options provided to `ReactDOM.preinit("%s", { as: "script", ... })`. React will use the first props or preinitialization' +
2024 + ' options encountered when rendering a hoistable script with a particular `src` and will ignore any newer props or' +
2025 + ' options. The first instance of this script resource was created using the `ReactDOM.preinit()` function.' +
2026 + ' Please note, `ReactDOM.preinit()` is modeled off of module import assertions capabilities and does not support' +
2027 + ' arbitrary props. If you need to have props not included with the preinit options you will need to rely on rendering' +
2028 + ' <script> tags only.%s',
2029 + src,
2030 + src,
2031 + differenceDescription,
2032 + );
2033 + }
2034 + break;
2035 + }
2036 + }
2037 + }
2038 + }
2039 + if (!resource) {
2040 + resource = {
2041 + type: 'script',
2042 + chunks: [],
2043 + state: NoState,
2044 + props: null,
2045 + };
2046 + resources.scriptsMap.set(key, resource);
2047 + if (__DEV__) {
2048 + markAsRenderedResourceDEV(resource, props);
2049 + }
2050 + // Add to the script flushing queue
2051 + resources.scripts.add(resource);
2052 +
2053 + let scriptProps = props;
2054 + const preloadResource = resources.preloadsMap.get(key);
2055 + if (preloadResource) {
2056 + // If we already had a preload we don't want that resource to flush directly.
2057 + // We let the newly created resource govern flushing.
2058 + preloadResource.state |= Blocked;
2059 + scriptProps = {...props};
2060 + adoptPreloadPropsForScriptProps(scriptProps, preloadResource.props);
2061 + }
2062 + // encode the tag as Chunks
2063 + pushScriptImpl(resource.chunks, scriptProps);
2064 + }
2065 + }
2066 +
2067 if (textEmbedded) {
1617 - // This link follows text but we aren't writing a tag. while not as efficient as possible we need
2068 + // This script follows text but we aren't writing a tag. while not as efficient as possible we need
2069 // to be safe and assume text will follow by inserting a textSeparator
2070 target.push(textSeparator);
2071 }
1621 - // We have converted this link exclusively to a resource and no longer
1622 - // need to emit it
2072 return null;
2073 + } else {
2074 + return pushScriptImpl(target, props);
2075 }
1625 -
1626 - return pushScriptImpl(target, props, responseState);
2076 }
2077
2078 function pushScriptImpl(
2079 target: Array<Chunk | PrecomputedChunk>,
2080 props: Object,
1632 - responseState: ResponseState,
2081 ): null {
2082 target.push(startChunkForTag('script'));
2083
@@ -1649,7 +2097,7 @@ function pushScriptImpl(
2097 innerHTML = propValue;
2098 break;
2099 default:
1652 - pushAttribute(target, responseState, propKey, propValue);
2100 + pushAttribute(target, propKey, propValue);
2101 break;
2102 }
2103 }
@@ -1684,7 +2132,6 @@ function pushStartGenericElement(
2132 target: Array<Chunk | PrecomputedChunk>,
2133 props: Object,
2134 tag: string,
1687 - responseState: ResponseState,
2135 ): ReactNodeList {
2136 target.push(startChunkForTag(tag));
2137
@@ -1704,7 +2151,7 @@ function pushStartGenericElement(
2151 innerHTML = propValue;
2152 break;
2153 default:
1707 - pushAttribute(target, responseState, propKey, propValue);
2154 + pushAttribute(target, propKey, propValue);
2155 break;
2156 }
2157 }
@@ -1725,7 +2172,6 @@ function pushStartCustomElement(
2172 target: Array<Chunk | PrecomputedChunk>,
2173 props: Object,
2174 tag: string,
1728 - responseState: ResponseState,
2175 ): ReactNodeList {
2176 target.push(startChunkForTag(tag));
2177
@@ -1765,7 +2211,7 @@ function pushStartCustomElement(
2211 innerHTML = propValue;
2212 break;
2213 case 'style':
1768 - pushStyle(target, responseState, propValue);
2214 + pushStyleAttribute(target, propValue);
2215 break;
2216 case 'suppressContentEditableWarning':
2217 case 'suppressHydrationWarning':
@@ -1801,7 +2247,6 @@ function pushStartPreformattedElement(
2247 target: Array<Chunk | PrecomputedChunk>,
2248 props: Object,
2249 tag: string,
1804 - responseState: ResponseState,
2250 ): ReactNodeList {
2251 target.push(startChunkForTag(tag));
2252
@@ -1821,7 +2266,7 @@ function pushStartPreformattedElement(
2266 innerHTML = propValue;
2267 break;
2268 default:
1824 - pushAttribute(target, responseState, propKey, propValue);
2269 + pushAttribute(target, propKey, propValue);
2270 break;
2271 }
2272 }
@@ -1896,9 +2341,9 @@ const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');
2341
2342 export function pushStartInstance(
2343 target: Array<Chunk | PrecomputedChunk>,
1899 - preamble: Array<Chunk | PrecomputedChunk>,
2344 type: string,
2345 props: Object,
2346 + resources: Resources,
2347 responseState: ResponseState,
2348 formatContext: FormatContext,
2349 textEmbedded: boolean,
@@ -1943,15 +2388,15 @@ export function pushStartInstance(
2388 switch (type) {
2389 // Special tags
2390 case 'select':
1946 - return pushStartSelect(target, props, responseState);
2391 + return pushStartSelect(target, props);
2392 case 'option':
1948 - return pushStartOption(target, props, responseState, formatContext);
2393 + return pushStartOption(target, props, formatContext);
2394 case 'textarea':
1950 - return pushStartTextArea(target, props, responseState);
2395 + return pushStartTextArea(target, props);
2396 case 'input':
1952 - return pushInput(target, props, responseState);
2397 + return pushInput(target, props);
2398 case 'menuitem':
1954 - return pushStartMenuItem(target, props, responseState);
2399 + return pushStartMenuItem(target, props);
2400 case 'title':
2401 return enableFloat
2402 ? pushTitle(
@@ -1961,13 +2406,15 @@ export function pushStartInstance(
2406 formatContext.insertionMode,
2407 formatContext.noscriptTagInScope,
2408 )
1964 - : pushStartTitle(target, props, responseState);
2409 + : pushStartTitle(target, props);
2410 case 'link':
2411 return pushLink(
2412 target,
2413 props,
2414 responseState,
2415 + resources,
2416 textEmbedded,
2417 + formatContext.insertionMode,
2418 formatContext.noscriptTagInScope,
2419 );
2420 case 'script':
@@ -1975,33 +2422,37 @@ export function pushStartInstance(
2422 ? pushScript(
2423 target,
2424 props,
1978 - responseState,
2425 + resources,
2426 textEmbedded,
2427 + formatContext.insertionMode,
2428 formatContext.noscriptTagInScope,
2429 )
1982 - : pushStartGenericElement(target, props, type, responseState);
1983 - case 'meta':
1984 - return pushMeta(
2430 + : pushStartGenericElement(target, props, type);
2431 + case 'style':
2432 + return pushStyle(
2433 target,
2434 props,
1987 - responseState,
2435 + resources,
2436 textEmbedded,
2437 + formatContext.insertionMode,
2438 formatContext.noscriptTagInScope,
2439 );
1991 - case 'base':
1992 - return pushBase(
2440 + case 'meta':
2441 + return pushMeta(
2442 target,
2443 props,
2444 responseState,
2445 textEmbedded,
2446 + formatContext.insertionMode,
2447 formatContext.noscriptTagInScope,
2448 );
2449 // Newline eating tags
2450 case 'listing':
2451 case 'pre': {
2002 - return pushStartPreformattedElement(target, props, type, responseState);
2452 + return pushStartPreformattedElement(target, props, type);
2453 }
2454 // Omitted close tags
2455 + case 'base':
2456 case 'area':
2457 case 'br':
2458 case 'col':
@@ -2013,7 +2464,7 @@ export function pushStartInstance(
2464 case 'source':
2465 case 'track':
2466 case 'wbr': {
2016 - return pushSelfClosing(target, props, type, responseState);
2467 + return pushSelfClosing(target, props, type);
2468 }
2469 // These are reserved SVG and MathML elements, that are never custom elements.
2470 // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
@@ -2025,28 +2476,31 @@ export function pushStartInstance(
2476 case 'font-face-format':
2477 case 'font-face-name':
2478 case 'missing-glyph': {
2028 - return pushStartGenericElement(target, props, type, responseState);
2479 + return pushStartGenericElement(target, props, type);
2480 }
2481 // Preamble start tags
2482 case 'head':
2032 - return pushStartHead(target, preamble, props, type, responseState);
2483 + return pushStartHead(
2484 + target,
2485 + props,
2486 + responseState,
2487 + formatContext.insertionMode,
2488 + );
2489 case 'html': {
2490 return pushStartHtml(
2491 target,
2036 - preamble,
2492 props,
2038 - type,
2493 responseState,
2040 - formatContext,
2494 + formatContext.insertionMode,
2495 );
2496 }
2497 default: {
2498 if (type.indexOf('-') === -1 && typeof props.is !== 'string') {
2499 // Generic element
2046 - return pushStartGenericElement(target, props, type, responseState);
2500 + return pushStartGenericElement(target, props, type);
2501 } else {
2502 // Custom element
2049 - return pushStartCustomElement(target, props, type, responseState);
2503 + return pushStartCustomElement(target, props, type);
2504 }
2505 }
2506 }
@@ -2057,15 +2511,17 @@ const endTag2 = stringToPrecomputedChunk('>');
2511
2512 export function pushEndInstance(
2513 target: Array<Chunk | PrecomputedChunk>,
2060 - postamble: Array<Chunk | PrecomputedChunk>,
2514 type: string,
2515 props: Object,
2516 + responseState: ResponseState,
2517 + formatContext: FormatContext,
2518 ): void {
2519 switch (type) {
2520 // When float is on we expect title and script tags to always be pushed in
2521 // a unit and never return children. when we end up pushing the end tag we
2522 // want to ensure there is no extra closing tag pushed
2523 case 'title':
2524 + case 'style':
2525 case 'script': {
2526 if (!enableFloat) {
2527 break;
@@ -2094,16 +2550,19 @@ export function pushEndInstance(
2550 return;
2551 }
2552 // Postamble end tags
2553 + // When float is enabled we omit the end tags for body and html when
2554 + // they represent the Document.body and Document.documentElement Nodes.
2555 + // This is so we can withhold them until the postamble when we know
2556 + // we won't emit any more tags
2557 case 'body': {
2098 - if (enableFloat) {
2099 - postamble.unshift(endTag1, stringToChunk(type), endTag2);
2558 + if (enableFloat && formatContext.insertionMode <= HTML_HTML_MODE) {
2559 + responseState.hasBody = true;
2560 return;
2561 }
2562 break;
2563 }
2564 case 'html':
2105 - if (enableFloat) {
2106 - postamble.push(endTag1, stringToChunk(type), endTag2);
2565 + if (enableFloat && formatContext.insertionMode === ROOT_HTML_MODE) {
2566 return;
2567 }
2568 break;
@@ -2316,6 +2775,7 @@ export function writeStartSegment(
2775 ): boolean {
2776 switch (formatContext.insertionMode) {
2777 case ROOT_HTML_MODE:
2778 + case HTML_HTML_MODE:
2779 case HTML_MODE: {
2780 writeChunk(destination, startSegmentHTML);
2781 writeChunk(destination, responseState.segmentPrefix);
@@ -2373,6 +2833,7 @@ export function writeEndSegment(
2833 ): boolean {
2834 switch (formatContext.insertionMode) {
2835 case ROOT_HTML_MODE:
2836 + case HTML_HTML_MODE:
2837 case HTML_MODE: {
2838 return writeChunkAndReturn(destination, endSegmentHTML);
2839 }
@@ -2423,9 +2884,12 @@ export function writeCompletedSegmentInstruction(
2884 responseState.streamingFormat === ScriptStreamingFormat;
2885 if (scriptFormat) {
2886 writeChunk(destination, responseState.startInlineScript);
2426 - if (!responseState.sentCompleteSegmentFunction) {
2887 + if (
2888 + (responseState.instructions & SentCompleteSegmentFunction) ===
2889 + NothingSent
2890 + ) {
2891 // The first time we write this, we'll need to include the full implementation.
2428 - responseState.sentCompleteSegmentFunction = true;
2892 + responseState.instructions |= SentCompleteSegmentFunction;
2893 writeChunk(destination, completeSegmentScript1Full);
2894 } else {
2895 // Future calls can just reuse the same function.
@@ -2455,15 +2919,15 @@ export function writeCompletedSegmentInstruction(
2919 }
2920
2921 const completeBoundaryScript1Full = stringToPrecomputedChunk(
2458 - completeBoundaryFunction + ';$RC("',
2922 + completeBoundaryFunction + '$RC("',
2923 );
2924 const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
2925
2926 const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk(
2463 - completeBoundaryFunction + ';' + styleInsertionFunction + ';$RR("',
2927 + completeBoundaryFunction + styleInsertionFunction + '$RR("',
2928 );
2929 const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
2466 - styleInsertionFunction + ';$RR("',
2930 + styleInsertionFunction + '$RR("',
2931 );
2932 const completeBoundaryWithStylesScript1Partial =
2933 stringToPrecomputedChunk('$RR("');
@@ -2499,22 +2963,31 @@ export function writeCompletedBoundaryInstruction(
2963 if (scriptFormat) {
2964 writeChunk(destination, responseState.startInlineScript);
2965 if (enableFloat && hasStyleDependencies) {
2502 - if (!responseState.sentCompleteBoundaryFunction) {
2503 - responseState.sentCompleteBoundaryFunction = true;
2504 - responseState.sentStyleInsertionFunction = true;
2966 + if (
2967 + (responseState.instructions & SentCompleteBoundaryFunction) ===
2968 + NothingSent
2969 + ) {
2970 + responseState.instructions |=
2971 + SentStyleInsertionFunction | SentCompleteBoundaryFunction;
2972 writeChunk(
2973 destination,
2974 clonePrecomputedChunk(completeBoundaryWithStylesScript1FullBoth),
2975 );
2509 - } else if (!responseState.sentStyleInsertionFunction) {
2510 - responseState.sentStyleInsertionFunction = true;
2976 + } else if (
2977 + (responseState.instructions & SentStyleInsertionFunction) ===
2978 + NothingSent
2979 + ) {
2980 + responseState.instructions |= SentStyleInsertionFunction;
2981 writeChunk(destination, completeBoundaryWithStylesScript1FullPartial);
2982 } else {
2983 writeChunk(destination, completeBoundaryWithStylesScript1Partial);
2984 }
2985 } else {
2516 - if (!responseState.sentCompleteBoundaryFunction) {
2517 - responseState.sentCompleteBoundaryFunction = true;
2986 + if (
2987 + (responseState.instructions & SentCompleteBoundaryFunction) ===
2988 + NothingSent
2989 + ) {
2990 + responseState.instructions |= SentCompleteBoundaryFunction;
2991 writeChunk(destination, completeBoundaryScript1Full);
2992 } else {
2993 writeChunk(destination, completeBoundaryScript1Partial);
@@ -2599,9 +3072,12 @@ export function writeClientRenderBoundaryInstruction(
3072 responseState.streamingFormat === ScriptStreamingFormat;
3073 if (scriptFormat) {
3074 writeChunk(destination, responseState.startInlineScript);
2602 - if (!responseState.sentClientRenderFunction) {
3075 + if (
3076 + (responseState.instructions & SentClientRenderFunction) ===
3077 + NothingSent
3078 + ) {
3079 // The first time we write this, we'll need to include the full implementation.
2604 - responseState.sentClientRenderFunction = true;
3080 + responseState.instructions |= SentClientRenderFunction;
3081 writeChunk(destination, clientRenderScript1Full);
3082 } else {
3083 // Future calls can just reuse the same function.
@@ -2736,18 +3212,210 @@ function escapeJSObjectForInstructionScripts(input: Object): string {
3212 });
3213 }
3214
3215 +const styleTagTemplateOpen = stringToPrecomputedChunk(
3216 + '<template data-precedence="">',
3217 +);
3218 +const styleTagTemplateClose = stringToPrecomputedChunk('</template>');
3219 +
3220 +// Tracks whether we wrote any late style tags. We use this to determine
3221 +// whether we need to emit a closing template tag after flushing late style tags
3222 +let didWrite = false;
3223 +
3224 +function flushStyleTagsLateForBoundary(
3225 + this: Destination,
3226 + resource: StyleResource,
3227 +) {
3228 + if (resource.type === 'style' && (resource.state & Flushed) === NoState) {
3229 + if (didWrite === false) {
3230 + // we are going to write so we need to emit the open tag
3231 + didWrite = true;
3232 + writeChunk(this, styleTagTemplateOpen);
3233 + }
3234 + // This <style> tag can be flushed now
3235 + const chunks = resource.chunks;
3236 + for (let i = 0; i < chunks.length; i++) {
3237 + writeChunk(this, chunks[i]);
3238 + }
3239 + resource.state |= FlushedLate;
3240 + }
3241 +}
3242 +
3243 +export function writeResourcesForBoundary(
3244 + destination: Destination,
3245 + boundaryResources: BoundaryResources,
3246 +): boolean {
3247 + didWrite = false;
3248 + boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
3249 + if (didWrite) {
3250 + return writeChunkAndReturn(destination, styleTagTemplateClose);
3251 + } else {
3252 + return true;
3253 + }
3254 +}
3255 +
3256 const precedencePlaceholderStart = stringToPrecomputedChunk(
3257 '<style data-precedence="',
3258 );
3259 const precedencePlaceholderEnd = stringToPrecomputedChunk('"></style>');
3260
2744 -export function writeInitialResources(
3261 +function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
3262 + if ((resource.state & (Flushed | Blocked)) === NoState) {
3263 + const chunks = resource.chunks;
3264 + for (let i = 0; i < chunks.length; i++) {
3265 + writeChunk(this, chunks[i]);
3266 + }
3267 + resource.state |= FlushedInPreamble;
3268 + }
3269 +}
3270 +
3271 +function flushResourceLate<T: Resource>(this: Destination, resource: T) {
3272 + if ((resource.state & Flushed) === NoState) {
3273 + const chunks = resource.chunks;
3274 + for (let i = 0; i < chunks.length; i++) {
3275 + writeChunk(this, chunks[i]);
3276 + }
3277 + resource.state |= FlushedLate;
3278 + }
3279 +}
3280 +
3281 +let didFlush = false;
3282 +
3283 +function flushUnblockedStyle(
3284 + this: Destination,
3285 + resource: StyleResource,
3286 + key: mixed,
3287 + set: Set<StyleResource>,
3288 +) {
3289 + const chunks = resource.chunks;
3290 + if (resource.state & Flushed) {
3291 + // In theory this should never happen because we clear from the
3292 + // Set on flush but to ensure correct semantics we don't emit
3293 + // anything if we are in this state.
3294 + set.delete(resource);
3295 + } else if (resource.state & Blocked) {
3296 + // We can't flush but we can preload. We will do this in a second pass
3297 + } else {
3298 + didFlush = true;
3299 + // We can emit this style or stylesheet as is.
3300 +
3301 + if (resource.type === 'stylesheet') {
3302 + // We still need to encode stylesheet chunks
3303 + // because unlike most Hoistables and Resources we do not eagerly encode
3304 + // them during render. This is because if we flush late we have to send a
3305 + // different encoding and we don't want to encode multiple times
3306 + pushLinkImpl(chunks, resource.props);
3307 + }
3308 + for (let i = 0; i < chunks.length; i++) {
3309 + writeChunk(this, chunks[i]);
3310 + }
3311 + resource.state |= FlushedInPreamble;
3312 + set.delete(resource);
3313 + }
3314 +}
3315 +
3316 +function flushUnblockedStyles(
3317 + this: Destination,
3318 + set: Set<StyleResource>,
3319 + precedence: string,
3320 +) {
3321 + didFlush = false;
3322 + set.forEach(flushUnblockedStyle, this);
3323 + if (!didFlush) {
3324 + // if we did not flush anything for this precedence slot we emit
3325 + // an empty <style data-precedence="..." /> tag to ensure the
3326 + // precedence remains in the correct order
3327 + writeChunk(this, precedencePlaceholderStart);
3328 + writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
3329 + writeChunk(this, precedencePlaceholderEnd);
3330 + }
3331 +}
3332 +
3333 +function preloadBlockedStyle(this: Destination, resource: StyleResource) {
3334 + // The only Resources that should remain are Blocked resources
3335 + if (__DEV__) {
3336 + if ((resource.state & Blocked) === NoState) {
3337 + console.error(
3338 + 'React encountered a Stylesheet Resource that was not Blocked when it was expected to be. This is a bug in React.',
3339 + );
3340 + } else if (resource.state & PreloadFlushed) {
3341 + console.error(
3342 + 'React encountered a Stylesheet Resource that already flushed a Preload when it was not expected to. This is a bug in React.',
3343 + );
3344 + }
3345 + }
3346 + if (resource.type === 'style') {
3347 + // <style> tags do not need to be preloaded
3348 + return;
3349 + }
3350 + const chunks = resource.chunks;
3351 + const preloadProps = preloadAsStylePropsFromProps(
3352 + resource.props.href,
3353 + resource.props,
3354 + );
3355 + pushLinkImpl(chunks, preloadProps);
3356 + for (let i = 0; i < chunks.length; i++) {
3357 + writeChunk(this, chunks[i]);
3358 + }
3359 + resource.state |= PreloadFlushed;
3360 + chunks.length = 0;
3361 +}
3362 +
3363 +function preloadBlockedStyles(
3364 + this: Destination,
3365 + set: Set<StyleResource>,
3366 + precedence: string,
3367 +) {
3368 + set.forEach(preloadBlockedStyle, this);
3369 + set.clear();
3370 +}
3371 +
3372 +function preloadLateStyle(this: Destination, resource: StyleResource) {
3373 + if (__DEV__) {
3374 + if (resource.state & PreloadFlushed) {
3375 + console.error(
3376 + 'React encountered a Stylesheet Resource that already flushed a Preload when it was not expected to. This is a bug in React.',
3377 + );
3378 + }
3379 + }
3380 +
3381 + if (resource.type === 'style') {
3382 + // <style> tags do not need to be preloaded
3383 + return;
3384 + }
3385 +
3386 + const chunks = resource.chunks;
3387 + const preloadProps = preloadAsStylePropsFromProps(
3388 + resource.props.href,
3389 + resource.props,
3390 + );
3391 + pushLinkImpl(chunks, preloadProps);
3392 + for (let i = 0; i < chunks.length; i++) {
3393 + writeChunk(this, chunks[i]);
3394 + }
3395 + resource.state |= PreloadFlushed;
3396 + chunks.length = 0;
3397 +}
3398 +
3399 +function preloadLateStyles(
3400 + this: Destination,
3401 + set: Set<StyleResource>,
3402 + precedence: string,
3403 +) {
3404 + set.forEach(preloadLateStyle, this);
3405 + set.clear();
3406 +}
3407 +
3408 +// We don't bother reporting backpressure at the moment because we expect to
3409 +// flush the entire preamble in a single pass. This probably should be modified
3410 +// in the future to be backpressure sensitive but that requires a larger refactor
3411 +// of the flushing code in Fizz.
3412 +export function writePreamble(
3413 destination: Destination,
3414 resources: Resources,
3415 responseState: ResponseState,
3416 willFlushAllSegments: boolean,
2749 -): boolean {
2750 - // Write initially discovered resources after the shell completes
3417 +): void {
3418 + // This function must be called exactly once on every request
3419 if (
3420 enableFizzExternalRuntime &&
3421 !willFlushAllSegments &&
@@ -2761,222 +3429,209 @@ export function writeInitialResources(
3429 const {src, integrity} = responseState.externalRuntimeConfig;
3430 preinitImpl(resources, src, {as: 'script', integrity});
3431 }
2764 - // $FlowFixMe[missing-local-annot]
2765 - function flushLinkResource(resource) {
2766 - if (!resource.flushed) {
2767 - pushLinkImpl(target, resource.props, responseState);
2768 - resource.flushed = true;
3432 +
3433 + const htmlChunks = responseState.htmlChunks;
3434 + const headChunks = responseState.headChunks;
3435 +
3436 + let i = 0;
3437 +
3438 + // Emit open tags before Hoistables and Resources
3439 + if (htmlChunks) {
3440 + // We have an <html> to emit as part of the preamble
3441 + for (i = 0; i < htmlChunks.length; i++) {
3442 + writeChunk(destination, htmlChunks[i]);
3443 + }
3444 + if (headChunks) {
3445 + for (i = 0; i < headChunks.length; i++) {
3446 + writeChunk(destination, headChunks[i]);
3447 + }
3448 + } else {
3449 + // We did not render a head but we emitted an <html> so we emit one now
3450 + writeChunk(destination, startChunkForTag('head'));
3451 + writeChunk(destination, endOfStartTag);
3452 + }
3453 + } else if (headChunks) {
3454 + // We do not have an <html> but we do have a <head>
3455 + for (i = 0; i < headChunks.length; i++) {
3456 + writeChunk(destination, headChunks[i]);
3457 }
3458 }
3459
2772 - const target: Array<Chunk | PrecomputedChunk> = [];
2773 -
2774 - const {
2775 - charset,
2776 - bases,
2777 - preconnects,
2778 - fontPreloads,
2779 - precedences,
2780 - usedStylePreloads,
2781 - scripts,
2782 - usedScriptPreloads,
2783 - explicitStylePreloads,
2784 - explicitScriptPreloads,
2785 - headResources,
2786 - } = resources;
3460 + // Emit high priority Hoistables
3461 + const charsetChunks = responseState.charsetChunks;
3462 + for (i = 0; i < charsetChunks.length; i++) {
3463 + writeChunk(destination, charsetChunks[i]);
3464 + }
3465 + charsetChunks.length = 0;
3466
2788 - if (charset) {
2789 - pushSelfClosing(target, charset.props, 'meta', responseState);
2790 - charset.flushed = true;
2791 - resources.charset = null;
3467 + const preconnectChunks = responseState.preconnectChunks;
3468 + for (i = 0; i < preconnectChunks.length; i++) {
3469 + writeChunk(destination, preconnectChunks[i]);
3470 }
3471 + preconnectChunks.length = 0;
3472
2794 - bases.forEach(r => {
2795 - pushSelfClosing(target, r.props, 'base', responseState);
2796 - r.flushed = true;
2797 - });
2798 - bases.clear();
3473 + resources.fontPreloads.forEach(flushResourceInPreamble, destination);
3474 + resources.fontPreloads.clear();
3475
2800 - preconnects.forEach(r => {
2801 - // font preload Resources should not already be flushed so we elide this check
2802 - pushLinkImpl(target, r.props, responseState);
2803 - r.flushed = true;
2804 - });
2805 - preconnects.clear();
3476 + // Flush unblocked stylesheets by precedence
3477 + resources.precedences.forEach(flushUnblockedStyles, destination);
3478
2807 - fontPreloads.forEach(r => {
2808 - // font preload Resources should not already be flushed so we elide this check
2809 - pushLinkImpl(target, r.props, responseState);
2810 - r.flushed = true;
2811 - });
2812 - fontPreloads.clear();
2813 -
2814 - // Flush stylesheets first by earliest precedence
2815 - precedences.forEach((p, precedence) => {
2816 - if (p.size) {
2817 - p.forEach(r => {
2818 - // resources should not already be flushed so we elide this check
2819 - pushLinkImpl(target, r.props, responseState);
2820 - r.flushed = true;
2821 - r.inShell = true;
2822 - r.hint.flushed = true;
2823 - });
2824 - p.clear();
3479 + // Flush preloads for Blocked stylesheets
3480 + resources.precedences.forEach(preloadBlockedStyles, destination);
3481 +
3482 + resources.usedStylesheets.forEach(resource => {
3483 + const key = getResourceKey(resource.props.as, resource.props.href);
3484 + if (resources.stylesMap.has(key)) {
3485 + // The underlying stylesheet is represented both as a used stylesheet
3486 + // (a regular component we will attempt to preload) and as a StylesheetResource.
3487 + // We don't want to emit two preloads for the same href so we defer
3488 + // the preload rules of the StylesheetResource when there is a conflict
3489 } else {
2826 - target.push(
2827 - precedencePlaceholderStart,
2828 - stringToChunk(escapeTextForBrowser(precedence)),
2829 - precedencePlaceholderEnd,
2830 - );
3490 + const chunks = resource.chunks;
3491 + for (i = 0; i < chunks.length; i++) {
3492 + writeChunk(destination, chunks[i]);
3493 + }
3494 }
3495 });
3496 + resources.usedStylesheets.clear();
3497
2834 - usedStylePreloads.forEach(flushLinkResource);
2835 - usedStylePreloads.clear();
3498 + resources.scripts.forEach(flushResourceInPreamble, destination);
3499 + resources.scripts.clear();
3500
2837 - scripts.forEach(r => {
2838 - // should never be flushed already
2839 - pushScriptImpl(target, r.props, responseState);
2840 - r.flushed = true;
2841 - r.hint.flushed = true;
2842 - });
2843 - scripts.clear();
3501 + resources.usedScripts.forEach(flushResourceInPreamble, destination);
3502 + resources.usedScripts.clear();
3503
2845 - usedScriptPreloads.forEach(flushLinkResource);
2846 - usedScriptPreloads.clear();
3504 + resources.explicitStylesheetPreloads.forEach(
3505 + flushResourceInPreamble,
3506 + destination,
3507 + );
3508 + resources.explicitStylesheetPreloads.clear();
3509
2848 - explicitStylePreloads.forEach(flushLinkResource);
2849 - explicitStylePreloads.clear();
3510 + resources.explicitScriptPreloads.forEach(
3511 + flushResourceInPreamble,
3512 + destination,
3513 + );
3514 + resources.explicitScriptPreloads.clear();
3515
2851 - explicitScriptPreloads.forEach(flushLinkResource);
2852 - explicitScriptPreloads.clear();
3516 + resources.explicitOtherPreloads.forEach(flushResourceInPreamble, destination);
3517 + resources.explicitOtherPreloads.clear();
3518
2854 - headResources.forEach(r => {
2855 - switch (r.type) {
2856 - case 'title': {
2857 - pushTitleImpl(target, r.props, responseState);
2858 - break;
2859 - }
2860 - case 'meta': {
2861 - pushSelfClosing(target, r.props, 'meta', responseState);
2862 - break;
2863 - }
2864 - case 'link': {
2865 - pushLinkImpl(target, r.props, responseState);
2866 - break;
2867 - }
2868 - }
2869 - r.flushed = true;
2870 - });
2871 - headResources.clear();
3519 + // Write embedding preloadChunks
3520 + const preloadChunks = responseState.preloadChunks;
3521 + for (i = 0; i < preloadChunks.length; i++) {
3522 + writeChunk(destination, preloadChunks[i]);
3523 + }
3524 + preloadChunks.length = 0;
3525
2873 - let i;
2874 - let r = true;
2875 - for (i = 0; i < target.length - 1; i++) {
2876 - writeChunk(destination, target[i]);
3526 + // Write embedding hoistableChunks
3527 + const hoistableChunks = responseState.hoistableChunks;
3528 + for (i = 0; i < hoistableChunks.length; i++) {
3529 + writeChunk(destination, hoistableChunks[i]);
3530 }
2878 - if (i < target.length) {
2879 - r = writeChunkAndReturn(destination, target[i]);
3531 + hoistableChunks.length = 0;
3532 +
3533 + // Flush closing head if necessary
3534 + if (htmlChunks && headChunks === null) {
3535 + // We have an <html> rendered but no <head> rendered. We however inserted
3536 + // a <head> up above so we need to emit the </head> now. This is safe because
3537 + // if the main content contained the </head> it would also have provided a
3538 + // <head>. This means that all the content inside <html> is either <body> or
3539 + // invalid HTML
3540 + writeChunk(destination, endTag1);
3541 + writeChunk(destination, stringToChunk('head'));
3542 + writeChunk(destination, endTag2);
3543 }
2881 - return r;
3544 }
3545
2884 -export function writeImmediateResources(
3546 +// We don't bother reporting backpressure at the moment because we expect to
3547 +// flush the entire preamble in a single pass. This probably should be modified
3548 +// in the future to be backpressure sensitive but that requires a larger refactor
3549 +// of the flushing code in Fizz.
3550 +export function writeHoistables(
3551 destination: Destination,
3552 resources: Resources,
3553 responseState: ResponseState,
2888 -): boolean {
2889 - // $FlowFixMe[missing-local-annot]
2890 - function flushLinkResource(resource) {
2891 - if (!resource.flushed) {
2892 - pushLinkImpl(target, resource.props, responseState);
2893 - resource.flushed = true;
2894 - }
2895 - }
2896 -
2897 - const target: Array<Chunk | PrecomputedChunk> = [];
2898 -
2899 - const {
2900 - charset,
2901 - preconnects,
2902 - fontPreloads,
2903 - usedStylePreloads,
2904 - scripts,
2905 - usedScriptPreloads,
2906 - explicitStylePreloads,
2907 - explicitScriptPreloads,
2908 - headResources,
2909 - } = resources;
2910 -
2911 - if (charset) {
2912 - pushSelfClosing(target, charset.props, 'meta', responseState);
2913 - charset.flushed = true;
2914 - resources.charset = null;
2915 - }
2916 -
2917 - preconnects.forEach(r => {
2918 - // font preload Resources should not already be flushed so we elide this check
2919 - pushLinkImpl(target, r.props, responseState);
2920 - r.flushed = true;
2921 - });
2922 - preconnects.clear();
3554 +): void {
3555 + let i = 0;
3556
2924 - fontPreloads.forEach(r => {
2925 - // font preload Resources should not already be flushed so we elide this check
2926 - pushLinkImpl(target, r.props, responseState);
2927 - r.flushed = true;
2928 - });
2929 - fontPreloads.clear();
3557 + // Emit high priority Hoistables
3558
2931 - usedStylePreloads.forEach(flushLinkResource);
2932 - usedStylePreloads.clear();
3559 + // We omit charsetChunks because we have already sent the shell and if it wasn't
3560 + // already sent it is too late now.
3561
2934 - scripts.forEach(r => {
2935 - // should never be flushed already
2936 - pushStartGenericElement(target, r.props, 'script', responseState);
2937 - pushEndInstance(target, target, 'script', r.props);
2938 - r.flushed = true;
2939 - r.hint.flushed = true;
3562 + const preconnectChunks = responseState.preconnectChunks;
3563 + for (i = 0; i < preconnectChunks.length; i++) {
3564 + writeChunk(destination, preconnectChunks[i]);
3565 + }
3566 + preconnectChunks.length = 0;
3567 +
3568 + resources.fontPreloads.forEach(flushResourceLate, destination);
3569 + resources.fontPreloads.clear();
3570 +
3571 + // Preload any stylesheets. these will emit in a render instruction that follows this
3572 + // but we want to kick off preloading as soon as possible
3573 + resources.precedences.forEach(preloadLateStyles, destination);
3574 +
3575 + resources.usedStylesheets.forEach(resource => {
3576 + const key = getResourceKey(resource.props.as, resource.props.href);
3577 + if (resources.stylesMap.has(key)) {
3578 + // The underlying stylesheet is represented both as a used stylesheet
3579 + // (a regular component we will attempt to preload) and as a StylesheetResource.
3580 + // We don't want to emit two preloads for the same href so we defer
3581 + // the preload rules of the StylesheetResource when there is a conflict
3582 + } else {
3583 + const chunks = resource.chunks;
3584 + for (i = 0; i < chunks.length; i++) {
3585 + writeChunk(destination, chunks[i]);
3586 + }
3587 + }
3588 });
2941 - scripts.clear();
3589 + resources.usedStylesheets.clear();
3590
2943 - usedScriptPreloads.forEach(flushLinkResource);
2944 - usedScriptPreloads.clear();
3591 + resources.scripts.forEach(flushResourceLate, destination);
3592 + resources.scripts.clear();
3593
2946 - explicitStylePreloads.forEach(flushLinkResource);
2947 - explicitStylePreloads.clear();
3594 + resources.usedScripts.forEach(flushResourceLate, destination);
3595 + resources.usedScripts.clear();
3596
2949 - explicitScriptPreloads.forEach(flushLinkResource);
2950 - explicitScriptPreloads.clear();
3597 + resources.explicitStylesheetPreloads.forEach(flushResourceLate, destination);
3598 + resources.explicitStylesheetPreloads.clear();
3599
2952 - headResources.forEach(r => {
2953 - switch (r.type) {
2954 - case 'title': {
2955 - pushTitleImpl(target, r.props, responseState);
2956 - break;
2957 - }
2958 - case 'meta': {
2959 - pushSelfClosing(target, r.props, 'meta', responseState);
2960 - break;
2961 - }
2962 - case 'link': {
2963 - pushLinkImpl(target, r.props, responseState);
2964 - break;
2965 - }
2966 - }
2967 - r.flushed = true;
2968 - });
2969 - headResources.clear();
3600 + resources.explicitScriptPreloads.forEach(flushResourceLate, destination);
3601 + resources.explicitScriptPreloads.clear();
3602 +
3603 + resources.explicitOtherPreloads.forEach(flushResourceLate, destination);
3604 + resources.explicitOtherPreloads.clear();
3605
2971 - let i;
2972 - let r = true;
2973 - for (i = 0; i < target.length - 1; i++) {
2974 - writeChunk(destination, target[i]);
3606 + // Write embedding preloadChunks
3607 + const preloadChunks = responseState.preloadChunks;
3608 + for (i = 0; i < preloadChunks.length; i++) {
3609 + writeChunk(destination, preloadChunks[i]);
3610 }
2976 - if (i < target.length) {
2977 - r = writeChunkAndReturn(destination, target[i]);
3611 + preloadChunks.length = 0;
3612 +
3613 + // Write embedding hoistableChunks
3614 + const hoistableChunks = responseState.hoistableChunks;
3615 + for (i = 0; i < hoistableChunks.length; i++) {
3616 + writeChunk(destination, hoistableChunks[i]);
3617 + }
3618 + hoistableChunks.length = 0;
3619 +}
3620 +
3621 +export function writePostamble(
3622 + destination: Destination,
3623 + responseState: ResponseState,
3624 +): void {
3625 + if (responseState.hasBody) {
3626 + writeChunk(destination, endTag1);
3627 + writeChunk(destination, stringToChunk('body'));
3628 + writeChunk(destination, endTag2);
3629 + }
3630 + if (responseState.htmlChunks) {
3631 + writeChunk(destination, endTag1);
3632 + writeChunk(destination, stringToChunk('html'));
3633 + writeChunk(destination, endTag2);
3634 }
2979 - return r;
3635 }
3636
3637 function hasStyleResourceDependencies(
@@ -2991,7 +3646,7 @@ function hasStyleResourceDependencies(
3646
3647 // If every style Resource flushed in the shell we do not need to send
3648 // any dependencies
2994 - if (!resource.inShell) {
3649 + if ((resource.state & FlushedInPreamble) === NoState) {
3650 return true;
3651 }
3652 }
@@ -3014,27 +3669,35 @@ function writeStyleResourceDependenciesInJS(
3669
3670 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
3671 boundaryResources.forEach(resource => {
3017 - if (resource.inShell) {
3672 + if (resource.state & FlushedInPreamble) {
3673 // We can elide this dependency because it was flushed in the shell and
3674 // should be ready before content is shown on the client
3020 - } else if (resource.flushed) {
3675 + } else if (resource.state & Flushed) {
3676 + // We only need to emit the href because this resource flushed in an earlier
3677 + // boundary already which encoded the attributes necessary to construct
3678 + // the resource instance on the client.
3679 writeChunk(destination, nextArrayOpenBrackChunk);
3022 - writeStyleResourceDependencyHrefOnlyInJS(destination, resource.href);
3680 + writeStyleResourceDependencyHrefOnlyInJS(
3681 + destination,
3682 + resource.type === 'style'
3683 + ? resource.props['data-href']
3684 + : resource.props.href,
3685 + );
3686 writeChunk(destination, arrayCloseBracket);
3687 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
3025 - } else {
3688 + } else if (resource.type === 'stylesheet') {
3689 + // We need to emit the whole resource for insertion on the client
3690 writeChunk(destination, nextArrayOpenBrackChunk);
3691 writeStyleResourceDependencyInJS(
3692 destination,
3029 - resource.href,
3030 - resource.precedence,
3693 + resource.props.href,
3694 + resource.props['data-precedence'],
3695 resource.props,
3696 );
3697 writeChunk(destination, arrayCloseBracket);
3698 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
3699
3036 - resource.flushed = true;
3037 - resource.hint.flushed = true;
3700 + resource.state |= FlushedLate;
3701 }
3702 });
3703 writeChunk(destination, arrayCloseBracket);
@@ -3203,27 +3866,35 @@ function writeStyleResourceDependenciesInAttr(
3866
3867 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
3868 boundaryResources.forEach(resource => {
3206 - if (resource.inShell) {
3869 + if (resource.state & FlushedInPreamble) {
3870 // We can elide this dependency because it was flushed in the shell and
3871 // should be ready before content is shown on the client
3209 - } else if (resource.flushed) {
3872 + } else if (resource.state & Flushed) {
3873 + // We only need to emit the href because this resource flushed in an earlier
3874 + // boundary already which encoded the attributes necessary to construct
3875 + // the resource instance on the client.
3876 writeChunk(destination, nextArrayOpenBrackChunk);
3211 - writeStyleResourceDependencyHrefOnlyInAttr(destination, resource.href);
3877 + writeStyleResourceDependencyHrefOnlyInAttr(
3878 + destination,
3879 + resource.type === 'style'
3880 + ? resource.props['data-href']
3881 + : resource.props.href,
3882 + );
3883 writeChunk(destination, arrayCloseBracket);
3884 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
3214 - } else {
3885 + } else if (resource.type === 'stylesheet') {
3886 + // We need to emit the whole resource for insertion on the client
3887 writeChunk(destination, nextArrayOpenBrackChunk);
3888 writeStyleResourceDependencyInAttr(
3889 destination,
3218 - resource.href,
3219 - resource.precedence,
3890 + resource.props.href,
3891 + resource.props['data-precedence'],
3892 resource.props,
3893 );
3894 writeChunk(destination, arrayCloseBracket);
3895 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
3896
3225 - resource.flushed = true;
3226 - resource.hint.flushed = true;
3897 + resource.state |= FlushedLate;
3898 }
3899 });
3900 writeChunk(destination, arrayCloseBracket);
@@ -3379,3 +4050,732 @@ function writeStyleResourceAttributeInAttr(
4050 stringToChunk(escapeTextForBrowser(JSON.stringify(attributeValue))),
4051 );
4052 }
4053 +
4054 +/**
4055 + * Resources
4056 + */
4057 +
4058 +type ResourceStateTag = number;
4059 +const NoState /* */ = 0b0000;
4060 +// These tags indicate whether the Resource was flushed and in which phase
4061 +const FlushedInPreamble /* */ = 0b0001;
4062 +const FlushedLate /* */ = 0b0010;
4063 +const Flushed /* */ = 0b0011;
4064 +// This tag indicates whether this Resource is blocked from flushing.
4065 +// This currently is only used with stylesheets that are blocked by a Boundary
4066 +const Blocked /* */ = 0b0100;
4067 +// This tag indicates whether this Resource has been preloaded.
4068 +// This generally only makes sense for Resources other than PreloadResource
4069 +const PreloadFlushed /* */ = 0b1000;
4070 +
4071 +type TResource<T: 'stylesheet' | 'style' | 'script' | 'preload', P> = {
4072 + type: T,
4073 + chunks: Array<Chunk | PrecomputedChunk>,
4074 + state: ResourceStateTag,
4075 + props: P,
4076 +};
4077 +// Dev extensions.
4078 +// Stylesheets and Scripts rendered with jsx
4079 +type RenderedResourceDEV = {
4080 + __provenance: 'rendered',
4081 + __originalProps: any,
4082 +};
4083 +// Preloads, Stylesheets, and Scripts from ReactDOM.preload or ReactDOM.preinit
4084 +type ImperativeResourceDEV = {
4085 + __provenance: 'preload' | 'preinit',
4086 + __originalHref: string,
4087 + __originalOptions: any,
4088 + __propsEquivalent: any,
4089 +};
4090 +// Preloads created for normal components we rendered but know we can preload early such as
4091 +// sync Scripts and stylesheets without precedence or with onLoad/onError handlers
4092 +type ImplicitResourceDEV = {
4093 + __provenance: 'implicit',
4094 + __underlyingProps: any,
4095 + __impliedProps: any,
4096 +};
4097 +type ResourceDEV =
4098 + | RenderedResourceDEV
4099 + | ImperativeResourceDEV
4100 + | ImplicitResourceDEV;
4101 +
4102 +type PreloadProps = {
4103 + rel: 'preload',
4104 + as: string,
4105 + href: string,
4106 + [string]: mixed,
4107 +};
4108 +type PreloadResource = TResource<'preload', PreloadProps>;
4109 +
4110 +type StylesheetProps = {
4111 + rel: 'stylesheet',
4112 + href: string,
4113 + 'data-precedence': string,
4114 + [string]: mixed,
4115 +};
4116 +type StylesheetResource = TResource<'stylesheet', StylesheetProps>;
4117 +
4118 +type StyleTagProps = {
4119 + 'data-href': string,
4120 + 'data-precedence': string,
4121 + [string]: mixed,
4122 +};
4123 +type StyleTagResource = TResource<'style', StyleTagProps>;
4124 +
4125 +type StyleResource = StylesheetResource | StyleTagResource;
4126 +
4127 +type ScriptProps = {
4128 + async: true,
4129 + src: string,
4130 + [string]: mixed,
4131 +};
4132 +type ScriptResource = TResource<'script', null>;
4133 +
4134 +type Resource = StyleResource | ScriptResource | PreloadResource;
4135 +
4136 +export type Resources = {
4137 + // Request local cache
4138 + preloadsMap: Map<string, PreloadResource>,
4139 + stylesMap: Map<string, StyleResource>,
4140 + scriptsMap: Map<string, ScriptResource>,
4141 +
4142 + // Flushing queues for Resource dependencies
4143 + fontPreloads: Set<PreloadResource>,
4144 + // usedImagePreloads: Set<PreloadResource>,
4145 + precedences: Map<string, Set<StyleResource>>,
4146 + usedStylesheets: Set<PreloadResource>,
4147 + scripts: Set<ScriptResource>,
4148 + usedScripts: Set<PreloadResource>,
4149 + explicitStylesheetPreloads: Set<PreloadResource>,
4150 + // explicitImagePreloads: Set<PreloadResource>,
4151 + explicitScriptPreloads: Set<PreloadResource>,
4152 + explicitOtherPreloads: Set<PreloadResource>,
4153 +
4154 + // Module-global-like reference for current boundary resources
4155 + boundaryResources: ?BoundaryResources,
4156 + ...
4157 +};
4158 +
4159 +// @TODO add bootstrap script to implicit preloads
4160 +export function createResources(): Resources {
4161 + return {
4162 + // persistent
4163 + preloadsMap: new Map(),
4164 + stylesMap: new Map(),
4165 + scriptsMap: new Map(),
4166 +
4167 + // cleared on flush
4168 + fontPreloads: new Set(),
4169 + // usedImagePreloads: new Set(),
4170 + precedences: new Map(),
4171 + usedStylesheets: new Set(),
4172 + scripts: new Set(),
4173 + usedScripts: new Set(),
4174 + explicitStylesheetPreloads: new Set(),
4175 + // explicitImagePreloads: new Set(),
4176 + explicitScriptPreloads: new Set(),
4177 + explicitOtherPreloads: new Set(),
4178 +
4179 + // like a module global for currently rendering boundary
4180 + boundaryResources: null,
4181 + };
4182 +}
4183 +
4184 +export type BoundaryResources = Set<StyleResource>;
4185 +
4186 +export function createBoundaryResources(): BoundaryResources {
4187 + return new Set();
4188 +}
4189 +
4190 +export function setCurrentlyRenderingBoundaryResourcesTarget(
4191 + resources: Resources,
4192 + boundaryResources: null | BoundaryResources,
4193 +) {
4194 + resources.boundaryResources = boundaryResources;
4195 +}
4196 +
4197 +function getResourceKey(as: string, href: string): string {
4198 + return `[${as}]${href}`;
4199 +}
4200 +
4201 +type PreloadAs = 'style' | 'font' | 'script';
4202 +type PreloadOptions = {
4203 + as: PreloadAs,
4204 + crossOrigin?: string,
4205 + integrity?: string,
4206 + media?: string,
4207 +};
4208 +export function preload(href: string, options: PreloadOptions) {
4209 + if (!currentResources) {
4210 + // While we expect that preload calls are primarily going to be observed
4211 + // during render because effects and events don't run on the server it is
4212 + // still possible that these get called in module scope. This is valid on
4213 + // the client since there is still a document to interact with but on the
4214 + // server we need a request to associate the call to. Because of this we
4215 + // simply return and do not warn.
4216 + return;
4217 + }
4218 + const resources = currentResources;
4219 + if (__DEV__) {
4220 + if (typeof href !== 'string' || !href) {
4221 + console.error(
4222 + 'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
4223 + getValueDescriptorExpectingObjectForWarning(href),
4224 + );
4225 + } else if (options == null || typeof options !== 'object') {
4226 + console.error(
4227 + 'ReactDOM.preload(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preloaded but encountered %s instead.',
4228 + getValueDescriptorExpectingEnumForWarning(options),
4229 + );
4230 + } else if (typeof options.as !== 'string') {
4231 + console.error(
4232 + 'ReactDOM.preload(): Expected the `as` property in the `options` argument (second) to contain a string value describing the type of resource to be preloaded but encountered %s instead. Values that are valid in for the `as` attribute of a `<link rel="preload" as="..." />` tag are valid here.',
4233 + getValueDescriptorExpectingEnumForWarning(options.as),
4234 + );
4235 + }
4236 + }
4237 + if (
4238 + typeof href === 'string' &&
4239 + href &&
4240 + typeof options === 'object' &&
4241 + options !== null &&
4242 + typeof options.as === 'string'
4243 + ) {
4244 + const as = options.as;
4245 + const key = getResourceKey(as, href);
4246 + let resource = resources.preloadsMap.get(key);
4247 + if (__DEV__) {
4248 + const devResource = getAsResourceDEV(resource);
4249 + if (devResource) {
4250 + switch (devResource.__provenance) {
4251 + case 'preload': {
4252 + const differenceDescription = describeDifferencesForPreloads(
4253 + options,
4254 + devResource.__originalOptions,
4255 + );
4256 + if (differenceDescription) {
4257 + console.error(
4258 + 'ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("%s", { as: "%s", ...})`.' +
4259 + ' React will always use the options it first encounters when preloading a resource for a given `href` and `as` type, and any later options will be ignored if different.' +
4260 + ' Try updating all calls to `ReactDOM.preload()` with the same `href` and `as` type to use the same options, or eliminate one of the calls.%s',
4261 + href,
4262 + as,
4263 + differenceDescription,
4264 + );
4265 + }
4266 + break;
4267 + }
4268 + case 'implicit': {
4269 + const differenceDescription =
4270 + describeDifferencesForPreloadOverImplicitPreload(
4271 + options,
4272 + devResource.__impliedProps,
4273 + );
4274 + if (differenceDescription) {
4275 + const elementDescription =
4276 + as === 'style'
4277 + ? '<link rel="stylesheet" ... />'
4278 + : as === 'script'
4279 + ? '<script ... />'
4280 + : null;
4281 + if (elementDescription) {
4282 + console.error(
4283 + 'ReactDOM.preload(): For `href` "%s", The options provided conflict with props on a matching %s element. When the preload' +
4284 + ' options disagree with the underlying resource it usually means the browser will not be able to use the preload when the resource' +
4285 + ' is fetched, negating any benefit the preload would provide. React will preload the resource using props derived from the resource instead' +
4286 + ' and ignore the options provided to the `ReactDOM.preload()` call. In general, preloading is useful when you expect to' +
4287 + ' render a resource soon but have not yet done so. In this case since the underlying resource was already rendered the preload call' +
4288 + ' may be extraneous. Try removing the call, otherwise try adjusting both the props on the %s and the options' +
4289 + ' passed to `ReactDOM.preload()` to agree.%s',
4290 + href,
4291 + elementDescription,
4292 + elementDescription,
4293 + differenceDescription,
4294 + );
4295 + }
4296 + }
4297 + break;
4298 + }
4299 + }
4300 + }
4301 + }
4302 + if (!resource) {
4303 + resource = {
4304 + type: 'preload',
4305 + chunks: [],
4306 + state: NoState,
4307 + props: preloadPropsFromPreloadOptions(href, as, options),
4308 + };
4309 + resources.preloadsMap.set(key, resource);
4310 + if (__DEV__) {
4311 + markAsImperativeResourceDEV(
4312 + resource,
4313 + 'preload',
4314 + href,
4315 + options,
4316 + resource.props,
4317 + );
4318 + }
4319 +
4320 + pushLinkImpl(resource.chunks, resource.props);
4321 + }
4322 + switch (as) {
4323 + case 'font': {
4324 + resources.fontPreloads.add(resource);
4325 + break;
4326 + }
4327 + case 'style': {
4328 + resources.explicitStylesheetPreloads.add(resource);
4329 + break;
4330 + }
4331 + case 'script': {
4332 + resources.explicitScriptPreloads.add(resource);
4333 + break;
4334 + }
4335 + default: {
4336 + resources.explicitOtherPreloads.add(resource);
4337 + }
4338 + }
4339 + }
4340 +}
4341 +
4342 +type PreinitAs = 'style' | 'script';
4343 +type PreinitOptions = {
4344 + as: PreinitAs,
4345 + precedence?: string,
4346 + crossOrigin?: string,
4347 + integrity?: string,
4348 +};
4349 +export function preinit(href: string, options: PreinitOptions): void {
4350 + if (!currentResources) {
4351 + // While we expect that preinit calls are primarily going to be observed
4352 + // during render because effects and events don't run on the server it is
4353 + // still possible that these get called in module scope. This is valid on
4354 + // the client since there is still a document to interact with but on the
4355 + // server we need a request to associate the call to. Because of this we
4356 + // simply return and do not warn.
4357 + return;
4358 + }
4359 + preinitImpl(currentResources, href, options);
4360 +}
4361 +
4362 +// On the server, preinit may be called outside of render when sending an
4363 +// external SSR runtime as part of the initial resources payload. Since this
4364 +// is an internal React call, we do not need to use the resources stack.
4365 +function preinitImpl(
4366 + resources: Resources,
4367 + href: string,
4368 + options: PreinitOptions,
4369 +): void {
4370 + if (__DEV__) {
4371 + if (typeof href !== 'string' || !href) {
4372 + console.error(
4373 + 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
4374 + getValueDescriptorExpectingObjectForWarning(href),
4375 + );
4376 + } else if (options == null || typeof options !== 'object') {
4377 + console.error(
4378 + 'ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered %s instead.',
4379 + getValueDescriptorExpectingEnumForWarning(options),
4380 + );
4381 + } else if (options.as !== 'style' && options.as !== 'script') {
4382 + console.error(
4383 + 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered %s instead. Valid values for `as` are "style" and "script".',
4384 + getValueDescriptorExpectingEnumForWarning(options.as),
4385 + );
4386 + }
4387 + }
4388 + if (
4389 + typeof href === 'string' &&
4390 + href &&
4391 + typeof options === 'object' &&
4392 + options !== null
4393 + ) {
4394 + const as = options.as;
4395 + switch (as) {
4396 + case 'style': {
4397 + const key = getResourceKey(as, href);
4398 + let resource = resources.stylesMap.get(key);
4399 + const precedence = options.precedence || 'default';
4400 + if (__DEV__) {
4401 + const devResource = getAsResourceDEV(resource);
4402 + if (devResource) {
4403 + const resourceProps = stylesheetPropsFromPreinitOptions(
4404 + href,
4405 + precedence,
4406 + options,
4407 + );
4408 + const propsEquivalent = {
4409 + ...resourceProps,
4410 + precedence: options.precedence,
4411 + ['data-precedence']: null,
4412 + };
4413 + switch (devResource.__provenance) {
4414 + case 'rendered': {
4415 + const differenceDescription =
4416 + describeDifferencesForPreinitOverStylesheet(
4417 + // Diff the props from the JSX element, not the derived resource props
4418 + propsEquivalent,
4419 + devResource.__originalProps,
4420 + );
4421 + if (differenceDescription) {
4422 + console.error(
4423 + 'ReactDOM.preinit(): For `href` "%s", the options provided conflict with props found on a <link rel="stylesheet" precedence="%s" href="%s" .../> that was already rendered.' +
4424 + ' React will always use the props or options it first encounters for a hoistable stylesheet for a given `href` and any later props or options will be ignored if different.' +
4425 + ' Generally, ReactDOM.preinit() is useful when you are not yet rendering a stylesheet but you anticipate it will be used soon.' +
4426 + ' In this case the stylesheet was already rendered so preinitializing it does not provide any additional benefit.' +
4427 + ' To resolve, try making the props and options agree between the <link rel="stylesheet" .../> and the `ReactDOM.preinit()` call or' +
4428 + ' remove the `ReactDOM.preinit()` call.%s',
4429 + href,
4430 + devResource.__originalProps.precedence,
4431 + href,
4432 + differenceDescription,
4433 + );
4434 + }
4435 + break;
4436 + }
4437 + case 'preinit': {
4438 + const differenceDescription = describeDifferencesForPreinits(
4439 + // Diff the props from the JSX element, not the derived resource props
4440 + propsEquivalent,
4441 + devResource.__propsEquivalent,
4442 + );
4443 + if (differenceDescription) {
4444 + console.error(
4445 + 'ReactDOM.preinit(): For `href` "%s", the options provided conflict with another call to `ReactDOM.preinit("%s", { as: "style", ... })`.' +
4446 + ' React will always use the options it first encounters when preinitializing a hoistable stylesheet for a given `href` and any later options will be ignored if different.' +
4447 + ' Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.%s',
4448 + href,
4449 + href,
4450 + differenceDescription,
4451 + );
4452 + }
4453 + break;
4454 + }
4455 + }
4456 + }
4457 + }
4458 + if (!resource) {
4459 + resource = {
4460 + type: 'stylesheet',
4461 + chunks: ([]: Array<Chunk | PrecomputedChunk>),
4462 + state: NoState,
4463 + props: stylesheetPropsFromPreinitOptions(href, precedence, options),
4464 + };
4465 + resources.stylesMap.set(key, resource);
4466 + if (__DEV__) {
4467 + markAsImperativeResourceDEV(resource, 'preinit', href, options, {
4468 + ...resource.props,
4469 + precedence,
4470 + ['data-precedence']: undefined,
4471 + });
4472 + }
4473 + let precedenceSet = resources.precedences.get(precedence);
4474 + if (!precedenceSet) {
4475 + precedenceSet = new Set();
4476 + resources.precedences.set(precedence, precedenceSet);
4477 + }
4478 + precedenceSet.add(resource);
4479 + }
4480 + return;
4481 + }
4482 + case 'script': {
4483 + const src = href;
4484 + const key = getResourceKey(as, src);
4485 + let resource = resources.scriptsMap.get(key);
4486 + if (__DEV__) {
4487 + const devResource = getAsResourceDEV(resource);
4488 + if (devResource) {
4489 + const propsEquivalent = scriptPropsFromPreinitOptions(src, options);
4490 + switch (devResource.__provenance) {
4491 + case 'rendered': {
4492 + const differenceDescription =
4493 + describeDifferencesForPreinitOverScript(
4494 + // Diff the props from the JSX element, not the derived resource props
4495 + propsEquivalent,
4496 + devResource.__originalProps,
4497 + );
4498 + if (differenceDescription) {
4499 + console.error(
4500 + 'ReactDOM.preinit(): For `href` "%s", the options provided conflict with props found on a <script async={true} src="%s" .../> that was already rendered.' +
4501 + ' React will always use the props or options it first encounters for a hoistable script for a given `href` and any later props or options will be ignored if different.' +
4502 + ' Generally, ReactDOM.preinit() is useful when you are not yet rendering a script but you anticipate it will be used soon and want to go beyond preloading it and have it' +
4503 + ' execute early. In this case the script was already rendered so preinitializing it does not provide any additional benefit.' +
4504 + ' To resolve, try making the props and options agree between the <script .../> and the `ReactDOM.preinit()` call or remove the `ReactDOM.preinit()` call.%s',
4505 + href,
4506 + href,
4507 + differenceDescription,
4508 + );
4509 + }
4510 + break;
4511 + }
4512 + case 'preinit': {
4513 + const differenceDescription = describeDifferencesForPreinits(
4514 + // Diff the props from the JSX element, not the derived resource props
4515 + propsEquivalent,
4516 + devResource.__propsEquivalent,
4517 + );
4518 + if (differenceDescription) {
4519 + console.error(
4520 + 'ReactDOM.preinit(): For `href` "%s", the options provided conflict with another call to `ReactDOM.preinit("%s", { as: "script", ... })`.' +
4521 + ' React will always use the options it first encounters when preinitializing a hoistable script for a given `href` and any later options will be ignored if different.' +
4522 + ' Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.%s',
4523 + href,
4524 + href,
4525 + differenceDescription,
4526 + );
4527 + }
4528 + break;
4529 + }
4530 + }
4531 + }
4532 + }
4533 + if (!resource) {
4534 + resource = {
4535 + type: 'script',
4536 + chunks: [],
4537 + state: NoState,
4538 + props: null,
4539 + };
4540 + resources.scriptsMap.set(key, resource);
4541 + const resourceProps = scriptPropsFromPreinitOptions(src, options);
4542 + if (__DEV__) {
4543 + markAsImperativeResourceDEV(
4544 + resource,
4545 + 'preinit',
4546 + href,
4547 + options,
4548 + resourceProps,
4549 + );
4550 + }
4551 + resources.scripts.add(resource);
4552 + pushScriptImpl(resource.chunks, resourceProps);
4553 + }
4554 + return;
4555 + }
4556 + }
4557 + }
4558 +}
4559 +
4560 +function preloadPropsFromPreloadOptions(
4561 + href: string,
4562 + as: PreloadAs,
4563 + options: PreloadOptions,
4564 +): PreloadProps {
4565 + return {
4566 + rel: 'preload',
4567 + as,
4568 + href,
4569 + crossOrigin: as === 'font' ? '' : options.crossOrigin,
4570 + integrity: options.integrity,
4571 + };
4572 +}
4573 +
4574 +function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps {
4575 + return {
4576 + rel: 'preload',
4577 + as: 'style',
4578 + href: href,
4579 + crossOrigin: props.crossOrigin,
4580 + integrity: props.integrity,
4581 + media: props.media,
4582 + hrefLang: props.hrefLang,
4583 + referrerPolicy: props.referrerPolicy,
4584 + };
4585 +}
4586 +
4587 +function preloadAsScriptPropsFromProps(href: string, props: any): PreloadProps {
4588 + return {
4589 + rel: 'preload',
4590 + as: 'script',
4591 + href,
4592 + crossOrigin: props.crossOrigin,
4593 + integrity: props.integrity,
4594 + referrerPolicy: props.referrerPolicy,
4595 + };
4596 +}
4597 +
4598 +function stylesheetPropsFromPreinitOptions(
4599 + href: string,
4600 + precedence: string,
4601 + options: PreinitOptions,
4602 +): StylesheetProps {
4603 + return {
4604 + rel: 'stylesheet',
4605 + href,
4606 + 'data-precedence': precedence,
4607 + crossOrigin: options.crossOrigin,
4608 + integrity: options.integrity,
4609 + };
4610 +}
4611 +
4612 +function stylesheetPropsFromRawProps(rawProps: any): StylesheetProps {
4613 + return {
4614 + ...rawProps,
4615 + 'data-precedence': rawProps.precedence,
4616 + precedence: null,
4617 + };
4618 +}
4619 +
4620 +function adoptPreloadPropsForStylesheetProps(
4621 + resourceProps: StylesheetProps,
4622 + preloadProps: PreloadProps,
4623 +): void {
4624 + if (resourceProps.crossOrigin == null)
4625 + resourceProps.crossOrigin = preloadProps.crossOrigin;
4626 + if (resourceProps.integrity == null)
4627 + resourceProps.integrity = preloadProps.integrity;
4628 +}
4629 +
4630 +function styleTagPropsFromRawProps(rawProps: any): StyleTagProps {
4631 + return {
4632 + ...rawProps,
4633 + 'data-precedence': rawProps.precedence,
4634 + precedence: null,
4635 + 'data-href': rawProps.href,
4636 + href: null,
4637 + };
4638 +}
4639 +
4640 +function scriptPropsFromPreinitOptions(
4641 + src: string,
4642 + options: PreinitOptions,
4643 +): ScriptProps {
4644 + return {
4645 + src,
4646 + async: true,
4647 + crossOrigin: options.crossOrigin,
4648 + integrity: options.integrity,
4649 + };
4650 +}
4651 +
4652 +function adoptPreloadPropsForScriptProps(
4653 + resourceProps: ScriptProps,
4654 + preloadProps: PreloadProps,
4655 +): void {
4656 + if (resourceProps.crossOrigin == null)
4657 + resourceProps.crossOrigin = preloadProps.crossOrigin;
4658 + if (resourceProps.integrity == null)
4659 + resourceProps.integrity = preloadProps.integrity;
4660 +}
4661 +
4662 +function hoistStylesheetResource(
4663 + this: BoundaryResources,
4664 + resource: StyleResource,
4665 +) {
4666 + this.add(resource);
4667 +}
4668 +
4669 +export function hoistResources(
4670 + resources: Resources,
4671 + source: BoundaryResources,
4672 +): void {
4673 + const currentBoundaryResources = resources.boundaryResources;
4674 + if (currentBoundaryResources) {
4675 + source.forEach(hoistStylesheetResource, currentBoundaryResources);
4676 + source.clear();
4677 + }
4678 +}
4679 +
4680 +function unblockStylesheet(resource: StyleResource) {
4681 + resource.state &= ~Blocked;
4682 +}
4683 +
4684 +export function hoistResourcesToRoot(
4685 + resources: Resources,
4686 + boundaryResources: BoundaryResources,
4687 +): void {
4688 + boundaryResources.forEach(unblockStylesheet);
4689 + boundaryResources.clear();
4690 +}
4691 +
4692 +function markAsRenderedResourceDEV(
4693 + resource: Resource,
4694 + originalProps: any,
4695 +): void {
4696 + if (__DEV__) {
4697 + const devResource: RenderedResourceDEV = (resource: any);
4698 + if (typeof devResource.__provenance === 'string') {
4699 + console.error(
4700 + 'Resource already marked for DEV type. This is a bug in React.',
4701 + );
4702 + }
4703 + devResource.__provenance = 'rendered';
4704 + devResource.__originalProps = originalProps;
4705 + } else {
4706 + // eslint-disable-next-line react-internal/prod-error-codes
4707 + throw new Error(
4708 + 'markAsRenderedResourceDEV was included in a production build. This is a bug in React.',
4709 + );
4710 + }
4711 +}
4712 +
4713 +function markAsImperativeResourceDEV(
4714 + resource: Resource,
4715 + provenance: 'preload' | 'preinit',
4716 + originalHref: string,
4717 + originalOptions: any,
4718 + propsEquivalent: any,
4719 +): void {
4720 + if (__DEV__) {
4721 + const devResource: ImperativeResourceDEV = (resource: any);
4722 + if (typeof devResource.__provenance === 'string') {
4723 + console.error(
4724 + 'Resource already marked for DEV type. This is a bug in React.',
4725 + );
4726 + }
4727 + devResource.__provenance = provenance;
4728 + devResource.__originalHref = originalHref;
4729 + devResource.__originalOptions = originalOptions;
4730 + devResource.__propsEquivalent = propsEquivalent;
4731 + } else {
4732 + // eslint-disable-next-line react-internal/prod-error-codes
4733 + throw new Error(
4734 + 'markAsImperativeResourceDEV was included in a production build. This is a bug in React.',
4735 + );
4736 + }
4737 +}
4738 +
4739 +function markAsImplicitResourceDEV(
4740 + resource: Resource,
4741 + underlyingProps: any,
4742 + impliedProps: any,
4743 +): void {
4744 + if (__DEV__) {
4745 + const devResource: ImplicitResourceDEV = (resource: any);
4746 + if (typeof devResource.__provenance === 'string') {
4747 + console.error(
4748 + 'Resource already marked for DEV type. This is a bug in React.',
4749 + );
4750 + }
4751 + devResource.__provenance = 'implicit';
4752 + devResource.__underlyingProps = underlyingProps;
4753 + devResource.__impliedProps = impliedProps;
4754 + } else {
4755 + // eslint-disable-next-line react-internal/prod-error-codes
4756 + throw new Error(
4757 + 'markAsImplicitResourceDEV was included in a production build. This is a bug in React.',
4758 + );
4759 + }
4760 +}
4761 +
4762 +function getAsResourceDEV(
4763 + resource: null | void | Resource,
4764 +): null | ResourceDEV {
4765 + if (__DEV__) {
4766 + if (resource) {
4767 + if (typeof (resource: any).__provenance === 'string') {
4768 + return (resource: any);
4769 + }
4770 + console.error(
4771 + 'Resource was not marked for DEV type. This is a bug in React.',
4772 + );
4773 + }
4774 + return null;
4775 + } else {
4776 + // eslint-disable-next-line react-internal/prod-error-codes
4777 + throw new Error(
4778 + 'getAsResourceDEV was included in a production build. This is a bug in React.',
4779 + );
4780 + }
4781 +}
packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js
+22 -10
@@ -11,6 +11,7 @@ import type {
11 BootstrapScriptDescriptor,
12 FormatContext,
13 StreamingFormat,
14 + InstructionState,
15 } from './ReactDOMServerFormatConfig';
16
17 import {
@@ -42,11 +43,15 @@ export type ResponseState = {
43 nextSuspenseID: number,
44 streamingFormat: StreamingFormat,
45 startInlineScript: PrecomputedChunk,
45 - sentCompleteSegmentFunction: boolean,
46 - sentCompleteBoundaryFunction: boolean,
47 - sentClientRenderFunction: boolean,
48 - sentStyleInsertionFunction: boolean,
46 + instructions: InstructionState,
47 externalRuntimeConfig: BootstrapScriptDescriptor | null,
48 + htmlChunks: null | Array<Chunk | PrecomputedChunk>,
49 + headChunks: null | Array<Chunk | PrecomputedChunk>,
50 + hasBody: boolean,
51 + charsetChunks: Array<Chunk | PrecomputedChunk>,
52 + preconnectChunks: Array<Chunk | PrecomputedChunk>,
53 + preloadChunks: Array<Chunk | PrecomputedChunk>,
54 + hoistableChunks: Array<Chunk | PrecomputedChunk>,
55 // This is an extra field for the legacy renderer
56 generateStaticMarkup: boolean,
57 };
@@ -74,11 +79,16 @@ export function createResponseState(
79 nextSuspenseID: responseState.nextSuspenseID,
80 streamingFormat: responseState.streamingFormat,
81 startInlineScript: responseState.startInlineScript,
77 - sentCompleteSegmentFunction: responseState.sentCompleteSegmentFunction,
78 - sentCompleteBoundaryFunction: responseState.sentCompleteBoundaryFunction,
79 - sentClientRenderFunction: responseState.sentClientRenderFunction,
80 - sentStyleInsertionFunction: responseState.sentStyleInsertionFunction,
82 + instructions: responseState.instructions,
83 externalRuntimeConfig: responseState.externalRuntimeConfig,
84 + htmlChunks: responseState.htmlChunks,
85 + headChunks: responseState.headChunks,
86 + hasBody: responseState.hasBody,
87 + charsetChunks: responseState.charsetChunks,
88 + preconnectChunks: responseState.preconnectChunks,
89 + preloadChunks: responseState.preloadChunks,
90 + hoistableChunks: responseState.hoistableChunks,
91 +
92 // This is an extra field for the legacy renderer
93 generateStaticMarkup,
94 };
@@ -115,12 +125,14 @@ export {
125 writeClientRenderBoundaryInstruction,
126 writeStartPendingSuspenseBoundary,
127 writeEndPendingSuspenseBoundary,
128 + writeResourcesForBoundary,
129 writePlaceholder,
130 writeCompletedRoot,
131 createResources,
132 createBoundaryResources,
122 - writeInitialResources,
123 - writeImmediateResources,
133 + writePreamble,
134 + writeHoistables,
135 + writePostamble,
136 hoistResources,
137 hoistResourcesToRoot,
138 setCurrentlyRenderingBoundaryResourcesTarget,
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
+85 -50
@@ -22,21 +22,37 @@ export function completeBoundaryWithStyles(
22 contentID,
23 styles,
24 ) {
25 + const stylesToHoist = new Map();
26 const precedences = new Map();
27 const thisDocument = document;
28 let lastResource, node;
29
30 + let nodes = thisDocument.querySelectorAll('template[data-precedence]');
31 + for (let i = 0; (node = nodes[i++]); ) {
32 + let child = node.content.firstChild;
33 + for (; child; child = child.nextSibling) {
34 + stylesToHoist.set(child.getAttribute('data-href'), child);
35 + }
36 + node.parentNode.removeChild(node);
37 + }
38 +
39 // Seed the precedence list with existing resources
30 - const nodes = thisDocument.querySelectorAll(
40 + nodes = thisDocument.querySelectorAll(
41 'link[data-precedence],style[data-precedence]',
42 );
43 for (let i = 0; (node = nodes[i++]); ) {
44 + // We populate the resourceMap from found nodes so we can incorporate any
45 + // resources the client runtime adds when the two runtimes are running concurrently
46 + resourceMap.set(
47 + node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
48 + node,
49 + );
50 precedences.set(node.dataset['precedence'], (lastResource = node));
51 }
52
53 let i = 0;
54 const dependencies = [];
39 - let style, href, precedence, attr, loadingState, resourceEl;
55 + let style, href, precedence, attr, loadingState, resourceEl, media;
56
57 function setStatus(s) {
58 this['s'] = s;
@@ -45,59 +61,78 @@ export function completeBoundaryWithStyles(
61 while ((style = styles[i++])) {
62 let j = 0;
63 href = style[j++];
48 - // We check if this resource is already in our resourceMap and reuse it if so.
49 - // If it is already loaded we don't return it as a depenendency since there is nothing
50 - // to wait for
51 - loadingState = resourceMap.get(href);
52 - if (loadingState) {
53 - if (loadingState['s'] !== 'l') {
54 - dependencies.push(loadingState);
55 - }
56 - continue;
57 - }
64
59 - // We construct our new resource element, looping over remaining attributes if any
60 - // setting them to the Element.
61 - resourceEl = thisDocument.createElement('link');
62 - resourceEl.href = href;
63 - resourceEl.rel = 'stylesheet';
64 - resourceEl.dataset['precedence'] = precedence = style[j++];
65 - while ((attr = style[j++])) {
66 - resourceEl.setAttribute(attr, style[j++]);
67 - }
65 + if ((resourceEl = resourceMap.get(href))) {
66 + // We have an already known resource. It could be a <style>, a <link> created
67 + // by this runtime (which will have a loadingState) or a <link> created by
68 + // the client runtime (which will also have a loadingState). We look for a
69 + // loadingState and test whether it is not loaded yet and whether the media matches
70 + // before using it as a dependency. If it is a <style> there will be no loadingState
71 + // and we can avoid tracking it as a dependency because these tags don't load
72 + } else {
73 + // We haven't already processed this href so we need to hoist an element. It will
74 + // either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
75 + // or we will need to create a <link>
76 + if ((resourceEl = stylesToHoist.get(href))) {
77 + // We have a <style> which needs to be hoisted to the correct precedence
78 + // We set it in the resourceMap so we can bail out on future passes
79 + // if this is depended on more than once
80 + precedence = resourceEl.getAttribute('data-precedence');
81 + } else {
82 + // If we got this far we are depending on a <link> which is not yet in the document.
83 + // We construct it here and attach a loadingState. We also check whether it matches
84 + // media before we include it in the dependency array.
85 + resourceEl = thisDocument.createElement('link');
86 + resourceEl.href = href;
87 + resourceEl.rel = 'stylesheet';
88 + resourceEl.dataset['precedence'] = precedence = style[j++];
89 + while ((attr = style[j++])) {
90 + resourceEl.setAttribute(attr, style[j++]);
91 + }
92 + loadingState = resourceEl['_p'] = new Promise((re, rj) => {
93 + resourceEl.onload = re;
94 + resourceEl.onerror = rj;
95 + });
96 + loadingState.then(
97 + setStatus.bind(loadingState, LOADED),
98 + setStatus.bind(loadingState, ERRORED),
99 + );
100 + }
101 + // Save this resource element so we can bailout if it is used again
102 + resourceMap.set(href, resourceEl);
103
69 - // We stash a pending promise in our map by href which will resolve or reject
70 - // when the underlying resource loads or errors. We add it to the dependencies
71 - // array to be returned.
72 - loadingState = resourceEl['_p'] = new Promise((re, rj) => {
73 - resourceEl.onload = re;
74 - resourceEl.onerror = rj;
75 - });
76 - loadingState.then(
77 - setStatus.bind(loadingState, LOADED),
78 - setStatus.bind(loadingState, ERRORED),
79 - );
80 - resourceMap.set(href, loadingState);
81 - dependencies.push(loadingState);
104 + // The prior style resource is the last one placed at a given
105 + // precedence or the last resource itself which may be null.
106 + // We grab this value and then update the last resource for this
107 + // precedence to be the inserted element, updating the lastResource
108 + // pointer if needed.
109 + const prior = precedences.get(precedence) || lastResource;
110 + if (prior === lastResource) {
111 + lastResource = resourceEl;
112 + }
113 + precedences.set(precedence, resourceEl);
114
83 - // The prior style resource is the last one placed at a given
84 - // precedence or the last resource itself which may be null.
85 - // We grab this value and then update the last resource for this
86 - // precedence to be the inserted element, updating the lastResource
87 - // pointer if needed.
88 - const prior = precedences.get(precedence) || lastResource;
89 - if (prior === lastResource) {
90 - lastResource = resourceEl;
115 + // Finally, we insert the newly constructed instance at an appropriate location
116 + // in the Document.
117 + if (prior) {
118 + prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
119 + } else {
120 + const head = thisDocument.head;
121 + head.insertBefore(resourceEl, head.firstChild);
122 + }
123 }
92 - precedences.set(precedence, resourceEl);
124
94 - // Finally, we insert the newly constructed instance at an appropriate location
95 - // in the Document.
96 - if (prior) {
97 - prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
98 - } else {
99 - const head = thisDocument.head;
100 - head.insertBefore(resourceEl, head.firstChild);
125 + // If we are a <link> we will have a loadingState and we can use this
126 + // combined with matchMedia to decide if we need to await this dependency
127 + // loading. <style> tags won't have a loadingState so they are never awaited
128 + loadingState = resourceEl['_p'];
129 + media = resourceEl.getAttribute('media');
130 + if (
131 + loadingState &&
132 + loadingState['s'] !== 'l' &&
133 + (!media || window['matchMedia'](media).matches)
134 + ) {
135 + dependencies.push(loadingState);
136 }
137 }
138
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-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"))};';
9 + '$RM=new Map;\n$RR=function(p,q,w){function r(l){this.s=l}for(var t=$RC,m=$RM,u=new Map,n=new Map,g=document,h,e,f=g.querySelectorAll("template[data-precedence]"),c=0;e=f[c++];){for(var b=e.content.firstChild;b;b=b.nextSibling)u.set(b.getAttribute("data-href"),b);e.parentNode.removeChild(e)}f=g.querySelectorAll("link[data-precedence],style[data-precedence]");for(c=0;e=f[c++];)m.set(e.getAttribute("STYLE"===e.nodeName?"data-href":"href"),e),n.set(e.dataset.precedence,h=e);e=0;f=[];for(var d,\nv,a;d=w[e++];){var k=0;b=d[k++];if(!(a=m.get(b))){if(a=u.get(b))c=a.getAttribute("data-precedence");else{a=g.createElement("link");a.href=b;a.rel="stylesheet";for(a.dataset.precedence=c=d[k++];v=d[k++];)a.setAttribute(v,d[k++]);d=a._p=new Promise(function(l,x){a.onload=l;a.onerror=x});d.then(r.bind(d,"l"),r.bind(d,"e"))}m.set(b,a);b=n.get(c)||h;b===h&&(h=a);n.set(c,a);b?b.parentNode.insertBefore(a,b.nextSibling):(c=g.head,c.insertBefore(a,c.firstChild))}d=a._p;c=a.getAttribute("media");!d||"l"===\nd.s||c&&!matchMedia(c).matches||f.push(d)}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/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js
+85 -50
@@ -25,21 +25,37 @@ export function completeBoundaryWithStyles(
25 const completeBoundaryImpl = window['$RC'];
26 const resourceMap = window['$RM'];
27
28 + const stylesToHoist = new Map();
29 const precedences = new Map();
30 const thisDocument = document;
31 let lastResource, node;
32
33 + let nodes = thisDocument.querySelectorAll('template[data-precedence]');
34 + for (let i = 0; (node = nodes[i++]); ) {
35 + let child = node.content.firstChild;
36 + for (; child; child = child.nextSibling) {
37 + stylesToHoist.set(child.getAttribute('data-href'), child);
38 + }
39 + node.parentNode.removeChild(node);
40 + }
41 +
42 // Seed the precedence list with existing resources
33 - const nodes = thisDocument.querySelectorAll(
43 + nodes = thisDocument.querySelectorAll(
44 'link[data-precedence],style[data-precedence]',
45 );
46 for (let i = 0; (node = nodes[i++]); ) {
47 + // We populate the resourceMap from found nodes so we can incorporate any
48 + // resources the client runtime adds when the two runtimes are running concurrently
49 + resourceMap.set(
50 + node.getAttribute(node.nodeName === 'STYLE' ? 'data-href' : 'href'),
51 + node,
52 + );
53 precedences.set(node.dataset['precedence'], (lastResource = node));
54 }
55
56 let i = 0;
57 const dependencies = [];
42 - let style, href, precedence, attr, loadingState, resourceEl;
58 + let style, href, precedence, attr, loadingState, resourceEl, media;
59
60 function setStatus(s) {
61 this['s'] = s;
@@ -48,59 +64,78 @@ export function completeBoundaryWithStyles(
64 while ((style = styles[i++])) {
65 let j = 0;
66 href = style[j++];
51 - // We check if this resource is already in our resourceMap and reuse it if so.
52 - // If it is already loaded we don't return it as a depenendency since there is nothing
53 - // to wait for
54 - loadingState = resourceMap.get(href);
55 - if (loadingState) {
56 - if (loadingState['s'] !== 'l') {
57 - dependencies.push(loadingState);
58 - }
59 - continue;
60 - }
67
62 - // We construct our new resource element, looping over remaining attributes if any
63 - // setting them to the Element.
64 - resourceEl = thisDocument.createElement('link');
65 - resourceEl.href = href;
66 - resourceEl.rel = 'stylesheet';
67 - resourceEl.dataset['precedence'] = precedence = style[j++];
68 - while ((attr = style[j++])) {
69 - resourceEl.setAttribute(attr, style[j++]);
70 - }
68 + if ((resourceEl = resourceMap.get(href))) {
69 + // We have an already known resource. It could be a <style>, a <link> created
70 + // by this runtime (which will have a loadingState) or a <link> created by
71 + // the client runtime (which will also have a loadingState). We look for a
72 + // loadingState and test whether it is not loaded yet and whether the media matches
73 + // before using it as a dependency. If it is a <style> there will be no loadingState
74 + // and we can avoid tracking it as a dependency because these tags don't load
75 + } else {
76 + // We haven't already processed this href so we need to hoist an element. It will
77 + // either be a <style> that was sent in a <template> and prepped in `stylesToHoist`
78 + // or we will need to create a <link>
79 + if ((resourceEl = stylesToHoist.get(href))) {
80 + // We have a <style> which needs to be hoisted to the correct precedence
81 + // We set it in the resourceMap so we can bail out on future passes
82 + // if this is depended on more than once
83 + precedence = resourceEl.getAttribute('data-precedence');
84 + } else {
85 + // If we got this far we are depending on a <link> which is not yet in the document.
86 + // We construct it here and attach a loadingState. We also check whether it matches
87 + // media before we include it in the dependency array.
88 + resourceEl = thisDocument.createElement('link');
89 + resourceEl.href = href;
90 + resourceEl.rel = 'stylesheet';
91 + resourceEl.dataset['precedence'] = precedence = style[j++];
92 + while ((attr = style[j++])) {
93 + resourceEl.setAttribute(attr, style[j++]);
94 + }
95 + loadingState = resourceEl['_p'] = new Promise((re, rj) => {
96 + resourceEl.onload = re;
97 + resourceEl.onerror = rj;
98 + });
99 + loadingState.then(
100 + setStatus.bind(loadingState, LOADED),
101 + setStatus.bind(loadingState, ERRORED),
102 + );
103 + }
104 + // Save this resource element so we can bailout if it is used again
105 + resourceMap.set(href, resourceEl);
106
72 - // We stash a pending promise in our map by href which will resolve or reject
73 - // when the underlying resource loads or errors. We add it to the dependencies
74 - // array to be returned.
75 - loadingState = resourceEl['_p'] = new Promise((re, rj) => {
76 - resourceEl.onload = re;
77 - resourceEl.onerror = rj;
78 - });
79 - loadingState.then(
80 - setStatus.bind(loadingState, LOADED),
81 - setStatus.bind(loadingState, ERRORED),
82 - );
83 - resourceMap.set(href, loadingState);
84 - dependencies.push(loadingState);
107 + // The prior style resource is the last one placed at a given
108 + // precedence or the last resource itself which may be null.
109 + // We grab this value and then update the last resource for this
110 + // precedence to be the inserted element, updating the lastResource
111 + // pointer if needed.
112 + const prior = precedences.get(precedence) || lastResource;
113 + if (prior === lastResource) {
114 + lastResource = resourceEl;
115 + }
116 + precedences.set(precedence, resourceEl);
117
86 - // The prior style resource is the last one placed at a given
87 - // precedence or the last resource itself which may be null.
88 - // We grab this value and then update the last resource for this
89 - // precedence to be the inserted element, updating the lastResource
90 - // pointer if needed.
91 - const prior = precedences.get(precedence) || lastResource;
92 - if (prior === lastResource) {
93 - lastResource = resourceEl;
118 + // Finally, we insert the newly constructed instance at an appropriate location
119 + // in the Document.
120 + if (prior) {
121 + prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
122 + } else {
123 + const head = thisDocument.head;
124 + head.insertBefore(resourceEl, head.firstChild);
125 + }
126 }
95 - precedences.set(precedence, resourceEl);
127
97 - // Finally, we insert the newly constructed instance at an appropriate location
98 - // in the Document.
99 - if (prior) {
100 - prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
101 - } else {
102 - const head = thisDocument.head;
103 - head.insertBefore(resourceEl, head.firstChild);
128 + // If we are a <link> we will have a loadingState and we can use this
129 + // combined with matchMedia to decide if we need to await this dependency
130 + // loading. <style> tags won't have a loadingState so they are never awaited
131 + loadingState = resourceEl['_p'];
132 + media = resourceEl.getAttribute('media');
133 + if (
134 + loadingState &&
135 + loadingState['s'] !== 'l' &&
136 + (!media || window['matchMedia'](media).matches)
137 + ) {
138 + dependencies.push(loadingState);
139 }
140 }
141
packages/react-dom-bindings/src/shared/ReactDOMResourceValidation.js
+425 -629
@@ -7,614 +7,31 @@
7 * @flow
8 */
9
10 -import hasOwnProperty from 'shared/hasOwnProperty';
11 -
12 -type Props = {[string]: mixed};
13 -
14 -export function warnOnMissingHrefAndRel(
15 - pendingProps: Props,
16 - currentProps: ?Props,
17 -) {
18 - if (__DEV__) {
19 - if (currentProps != null) {
20 - const originalResourceName =
21 - typeof currentProps.href === 'string'
22 - ? `Resource with href "${currentProps.href}"`
23 - : 'Resource';
24 - const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
25 - currentProps.rel,
26 - );
27 - const pendingRel = getValueDescriptorExpectingEnumForWarning(
28 - pendingProps.rel,
29 - );
30 - const pendingHref = getValueDescriptorExpectingEnumForWarning(
31 - pendingProps.href,
32 - );
33 - if (typeof pendingProps.rel !== 'string') {
34 - console.error(
35 - 'A <link> previously rendered as a %s with rel "%s" but was updated with an invalid rel: %s. When a link' +
36 - ' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
37 - ' do not render the <link> anymore.',
38 - originalResourceName,
39 - originalRelStatement,
40 - pendingRel,
41 - );
42 - } else if (typeof pendingProps.href !== 'string') {
43 - console.error(
44 - 'A <link> previously rendered as a %s but was updated with an invalid href prop: %s. When a link' +
45 - ' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
46 - ' do not render the <link> anymore.',
47 - originalResourceName,
48 - pendingHref,
49 - );
50 - }
51 - } else {
52 - const pendingRel = getValueDescriptorExpectingEnumForWarning(
53 - pendingProps.rel,
54 - );
55 - const pendingHref = getValueDescriptorExpectingEnumForWarning(
56 - pendingProps.href,
57 - );
58 - if (typeof pendingProps.rel !== 'string') {
59 - console.error(
60 - 'A <link> is rendering with an invalid rel: %s. When a link' +
61 - ' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
62 - ' do not render the <link> anymore.',
63 - pendingRel,
64 - );
65 - } else if (typeof pendingProps.href !== 'string') {
66 - console.error(
67 - 'A <link> is rendering with an invalid href: %s. When a link' +
68 - ' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
69 - ' do not render the <link> anymore.',
70 - pendingHref,
71 - );
72 - }
73 - }
74 - }
75 -}
76 -
77 -export function validatePreloadResourceDifference(
78 - originalProps: any,
79 - originalImplicit: boolean,
80 - latestProps: any,
81 - latestImplicit: boolean,
82 -) {
83 - if (__DEV__) {
84 - const {href} = originalProps;
85 - const originalWarningName = getResourceNameForWarning(
86 - 'preload',
87 - originalProps,
88 - originalImplicit,
89 - );
90 - const latestWarningName = getResourceNameForWarning(
91 - 'preload',
92 - latestProps,
93 - latestImplicit,
94 - );
95 -
96 - if (latestProps.as !== originalProps.as) {
97 - console.error(
98 - 'A %s is using the same href "%s" as a %s. This is always an error and React will only keep the first preload' +
99 - ' for any given href, discarding subsequent instances. To fix, find where you are using this href in link' +
100 - ' tags or in calls to ReactDOM.preload() or ReactDOM.preinit() and either make the Resource types agree or' +
101 - ' update the hrefs to be distinct for different Resource types.',
102 - latestWarningName,
103 - href,
104 - originalWarningName,
105 - );
106 - } else {
107 - let missingProps: {[string]: $FlowFixMe} | null = null;
108 - let extraProps: {[string]: $FlowFixMe} | null = null;
109 - let differentProps: {
110 - [string]: {latest: $FlowFixMe, original: $FlowFixMe},
111 - } | null = null;
112 - if (originalProps.media != null && latestProps.media == null) {
113 - missingProps = missingProps || ({}: {[string]: $FlowFixMe});
114 - missingProps.media = originalProps.media;
115 - }
116 -
117 - for (const propName in latestProps) {
118 - const propValue = latestProps[propName];
119 - const originalValue = originalProps[propName];
120 -
121 - if (propValue != null && propValue !== originalValue) {
122 - if (originalValue == null) {
123 - extraProps = extraProps || ({}: {[string]: $FlowFixMe});
124 - extraProps[propName] = propValue;
125 - } else {
126 - differentProps =
127 - differentProps ||
128 - ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
129 - differentProps[propName] = {
130 - original: originalValue,
131 - latest: propValue,
132 - };
133 - }
134 - }
135 - }
136 -
137 - if (missingProps || extraProps || differentProps) {
138 - warnDifferentProps(
139 - href,
140 - 'href',
141 - originalWarningName,
142 - latestWarningName,
143 - extraProps,
144 - missingProps,
145 - differentProps,
146 - );
147 - }
148 - }
149 - }
150 -}
151 -
152 -export function validateStyleResourceDifference(
153 - originalProps: any,
154 - latestProps: any,
155 -) {
156 - if (__DEV__) {
157 - const {href} = originalProps;
158 - // eslint-disable-next-line no-labels
159 - const originalWarningName = getResourceNameForWarning(
160 - 'style',
161 - originalProps,
162 - false,
163 - );
164 - const latestWarningName = getResourceNameForWarning(
165 - 'style',
166 - latestProps,
167 - false,
168 - );
169 - let missingProps: {[string]: $FlowFixMe} | null = null;
170 - let extraProps: {[string]: $FlowFixMe} | null = null;
171 - let differentProps: {
172 - [string]: {latest: $FlowFixMe, original: $FlowFixMe},
173 - } | null = null;
174 - if (originalProps.media != null && latestProps.media == null) {
175 - missingProps = missingProps || ({}: {[string]: $FlowFixMe});
176 - missingProps.media = originalProps.media;
177 - }
178 -
179 - for (let propName in latestProps) {
180 - const propValue = latestProps[propName];
181 - const originalValue = originalProps[propName];
182 -
183 - if (propValue != null && propValue !== originalValue) {
184 - propName = propName === 'data-precedence' ? 'precedence' : propName;
185 - if (originalValue == null) {
186 - extraProps = extraProps || ({}: {[string]: $FlowFixMe});
187 - extraProps[propName] = propValue;
188 - } else {
189 - differentProps =
190 - differentProps ||
191 - ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
192 - differentProps[propName] = {
193 - original: originalValue,
194 - latest: propValue,
195 - };
196 - }
197 - }
198 - }
199 -
200 - if (missingProps || extraProps || differentProps) {
201 - warnDifferentProps(
202 - href,
203 - 'href',
204 - originalWarningName,
205 - latestWarningName,
206 - extraProps,
207 - missingProps,
208 - differentProps,
209 - );
210 - }
211 - }
212 -}
213 -
214 -export function validateScriptResourceDifference(
215 - originalProps: any,
216 - latestProps: any,
217 -) {
218 - if (__DEV__) {
219 - const {src} = originalProps;
220 - // eslint-disable-next-line no-labels
221 - const originalWarningName = getResourceNameForWarning(
222 - 'script',
223 - originalProps,
224 - false,
225 - );
226 - const latestWarningName = getResourceNameForWarning(
227 - 'script',
228 - latestProps,
229 - false,
230 - );
231 - let extraProps: {[string]: $FlowFixMe} | null = null;
232 - let differentProps: {
233 - [string]: {latest: $FlowFixMe, original: $FlowFixMe},
234 - } | null = null;
235 -
236 - for (const propName in latestProps) {
237 - const propValue = latestProps[propName];
238 - const originalValue = originalProps[propName];
239 -
240 - if (propValue != null && propValue !== originalValue) {
241 - if (originalValue == null) {
242 - extraProps = extraProps || ({}: {[string]: $FlowFixMe});
243 - extraProps[propName] = propValue;
244 - } else {
245 - differentProps =
246 - differentProps ||
247 - ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
248 - differentProps[propName] = {
249 - original: originalValue,
250 - latest: propValue,
251 - };
252 - }
253 - }
254 - }
255 -
256 - if (extraProps || differentProps) {
257 - warnDifferentProps(
258 - src,
259 - 'src',
260 - originalWarningName,
261 - latestWarningName,
262 - extraProps,
263 - null,
264 - differentProps,
265 - );
266 - }
267 - }
268 -}
269 -
270 -export function validateStyleAndHintProps(
271 - preloadProps: any,
272 - styleProps: any,
273 - implicitPreload: boolean,
274 -) {
275 - if (__DEV__) {
276 - const {href} = preloadProps;
277 -
278 - const originalWarningName = getResourceNameForWarning(
279 - 'preload',
280 - preloadProps,
281 - implicitPreload,
282 - );
283 - const latestWarningName = getResourceNameForWarning(
284 - 'style',
285 - styleProps,
286 - false,
287 - );
288 -
289 - if (preloadProps.as !== 'style') {
290 - console.error(
291 - 'While creating a %s for href "%s" a %s for this same href was found. When preloading a stylesheet the' +
292 - ' "as" prop must be of type "style". This most likely ocurred by rendering a preload link with an incorrect' +
293 - ' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
294 - latestWarningName,
295 - href,
296 - originalWarningName,
297 - );
298 - }
299 -
300 - let missingProps: {[string]: $FlowFixMe} | null = null;
301 - let extraProps: {[string]: $FlowFixMe} | null = null;
302 - let differentProps: {
303 - [string]: {latest: $FlowFixMe, original: $FlowFixMe},
304 - } | null = null;
305 -
306 - for (const propName in styleProps) {
307 - const styleValue = styleProps[propName];
308 - const preloadValue = preloadProps[propName];
309 - switch (propName) {
310 - // Check for difference on specific props that cross over or influence
311 - // the relationship between the preload and stylesheet
312 - case 'crossOrigin':
313 - case 'referrerPolicy':
314 - case 'media':
315 - case 'title': {
316 - if (
317 - preloadValue !== styleValue &&
318 - !(preloadValue == null && styleValue == null)
319 - ) {
320 - if (styleValue == null) {
321 - missingProps = missingProps || ({}: {[string]: $FlowFixMe});
322 - missingProps[propName] = preloadValue;
323 - } else if (preloadValue == null) {
324 - extraProps = extraProps || ({}: {[string]: $FlowFixMe});
325 - extraProps[propName] = styleValue;
326 - } else {
327 - differentProps =
328 - differentProps ||
329 - ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
330 - differentProps[propName] = {
331 - original: preloadValue,
332 - latest: styleValue,
333 - };
334 - }
335 - }
336 - }
337 - }
338 - }
339 -
340 - if (missingProps || extraProps || differentProps) {
341 - warnDifferentProps(
342 - href,
343 - 'href',
344 - originalWarningName,
345 - latestWarningName,
346 - extraProps,
347 - missingProps,
348 - differentProps,
349 - );
350 - }
351 - }
352 -}
353 -
354 -export function validateScriptAndHintProps(
355 - preloadProps: any,
356 - scriptProps: any,
357 - implicitPreload: boolean,
358 -) {
359 - if (__DEV__) {
360 - const {href} = preloadProps;
361 -
362 - const originalWarningName = getResourceNameForWarning(
363 - 'preload',
364 - preloadProps,
365 - implicitPreload,
366 - );
367 - const latestWarningName = getResourceNameForWarning(
368 - 'script',
369 - scriptProps,
370 - false,
371 - );
372 -
373 - if (preloadProps.as !== 'script') {
374 - console.error(
375 - 'While creating a %s for href "%s" a %s for this same url was found. When preloading a script the' +
376 - ' "as" prop must be of type "script". This most likely ocurred by rendering a preload link with an incorrect' +
377 - ' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
378 - latestWarningName,
379 - href,
380 - originalWarningName,
381 - );
382 - }
383 -
384 - let missingProps: {[string]: $FlowFixMe} | null = null;
385 - let extraProps: {[string]: $FlowFixMe} | null = null;
386 - let differentProps: {
387 - [string]: {latest: $FlowFixMe, original: $FlowFixMe},
388 - } | null = null;
389 -
390 - for (const propName in scriptProps) {
391 - const scriptValue = scriptProps[propName];
392 - const preloadValue = preloadProps[propName];
393 - switch (propName) {
394 - // Check for difference on specific props that cross over or influence
395 - // the relationship between the preload and stylesheet
396 - case 'crossOrigin':
397 - case 'referrerPolicy':
398 - case 'integrity': {
399 - if (
400 - preloadValue !== scriptValue &&
401 - !(preloadValue == null && scriptValue == null)
402 - ) {
403 - if (scriptValue == null) {
404 - missingProps = missingProps || ({}: {[string]: $FlowFixMe});
405 - missingProps[propName] = preloadValue;
406 - } else if (preloadValue == null) {
407 - extraProps = extraProps || ({}: {[string]: $FlowFixMe});
408 - extraProps[propName] = scriptValue;
409 - } else {
410 - differentProps =
411 - differentProps ||
412 - ({}: {[string]: {latest: $FlowFixMe, original: $FlowFixMe}});
413 - differentProps[propName] = {
414 - original: preloadValue,
415 - latest: scriptValue,
416 - };
417 - }
418 - }
419 - }
420 - }
421 - }
422 -
423 - if (missingProps || extraProps || differentProps) {
424 - warnDifferentProps(
425 - href,
426 - 'href',
427 - originalWarningName,
428 - latestWarningName,
429 - extraProps,
430 - missingProps,
431 - differentProps,
432 - );
433 - }
434 - }
435 -}
436 -
437 -function warnDifferentProps(
438 - url: string,
439 - urlPropKey: string,
440 - originalName: string,
441 - latestName: string,
442 - extraProps: ?{[string]: $FlowFixMe},
443 - missingProps: ?{[string]: $FlowFixMe},
444 - differentProps: ?{[string]: {original: any, latest: any}},
445 -): void {
446 - if (__DEV__) {
447 - const juxtaposedNameStatement =
448 - latestName === originalName
449 - ? 'an earlier instance of this Resource'
450 - : `a ${originalName} with the same ${urlPropKey}`;
451 -
452 - let comparisonStatement = '';
453 - if (missingProps !== null && typeof missingProps === 'object') {
454 - for (const propName in missingProps) {
455 - comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
456 - }
457 - }
458 - if (extraProps !== null && typeof extraProps === 'object') {
459 - for (const propName in extraProps) {
460 - comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
461 - }
462 - }
463 - if (differentProps !== null && typeof differentProps === 'object') {
464 - for (const propName in differentProps) {
465 - comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
466 - }
467 - }
468 -
469 - console.error(
470 - 'A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
471 - ' that were provided the first time they are encountered so any differences will be ignored. Please' +
472 - ' update Resources that share an %s to have props that agree. The differences are described below.%s',
473 - latestName,
474 - urlPropKey,
475 - url,
476 - juxtaposedNameStatement,
477 - urlPropKey,
478 - comparisonStatement,
479 - );
480 - }
481 -}
482 -
483 -function getResourceNameForWarning(
484 - type: string,
485 - props: Object,
486 - implicit: boolean,
487 -) {
488 - if (__DEV__) {
489 - switch (type) {
490 - case 'style': {
491 - return 'style Resource';
492 - }
493 - case 'script': {
494 - return 'script Resource';
495 - }
496 - case 'preload': {
497 - if (implicit) {
498 - return `preload for a ${props.as} Resource`;
499 - }
500 - return `preload Resource (as "${props.as}")`;
501 - }
502 - }
503 - }
504 - return 'Resource';
505 -}
506 -
507 -export function validateURLKeyedUpdatedProps(
508 - pendingProps: Props,
509 - currentProps: Props,
510 - resourceType: 'style' | 'script' | 'href',
511 - urlPropKey: 'href' | 'src',
512 -): boolean {
513 - if (__DEV__) {
514 - // This function should never be called if we don't have /srcs so we don't bother considering
515 - // Whether they are null or undefined
516 - if (pendingProps[urlPropKey] === currentProps[urlPropKey]) {
517 - // If we have the same href/src we need all other props to be the same
518 - let missingProps: ?{[string]: mixed};
519 - let extraProps: ?{[string]: mixed};
520 - let differentProps: ?{[string]: {latest: mixed, original: mixed}};
521 - const allProps = Array.from(
522 - new Set(Object.keys(currentProps).concat(Object.keys(pendingProps))),
523 - );
524 - for (let i = 0; i < allProps.length; i++) {
525 - const propName = allProps[i];
526 - const pendingValue = pendingProps[propName];
527 - const currentValue = currentProps[propName];
528 - if (
529 - pendingValue !== currentValue &&
530 - !(pendingValue == null && currentValue == null)
531 - ) {
532 - if (pendingValue == null) {
533 - missingProps = missingProps || ({}: {[string]: mixed});
534 - missingProps[propName] = currentValue;
535 - } else if (currentValue == null) {
536 - extraProps = extraProps || ({}: {[string]: mixed});
537 - extraProps[propName] = pendingValue;
538 - } else {
539 - differentProps =
540 - differentProps ||
541 - ({}: {[string]: {latest: mixed, original: mixed}});
542 - differentProps[propName] = {
543 - original: currentValue,
544 - latest: pendingValue,
545 - };
546 - }
547 - }
548 - }
549 - if (missingProps || extraProps || differentProps) {
550 - const latestWarningName = getResourceNameForWarning(
551 - resourceType,
552 - currentProps,
553 - false,
554 - );
555 -
556 - let comparisonStatement = '';
557 - if (missingProps !== null && typeof missingProps === 'object') {
558 - for (const propName in missingProps) {
559 - // $FlowFixMe[incompatible-type]
560 - comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
561 - }
562 - }
563 - if (extraProps !== null && typeof extraProps === 'object') {
564 - for (const propName in extraProps) {
565 - // $FlowFixMe[incompatible-type]
566 - comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
567 - }
568 - }
569 - if (differentProps !== null && typeof differentProps === 'object') {
570 - for (const propName in differentProps) {
571 - // $FlowFixMe[incompatible-type]
572 - comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
573 - }
574 - }
575 - console.error(
576 - 'A %s with %s "%s" received new props with different values from the props used' +
577 - ' when this Resource was first rendered. React will only use the props provided when' +
578 - ' this resource was first rendered until a new %s is provided. Unlike conventional' +
579 - ' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
580 - ' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
581 - ' (%s) must have props that agree with each other. The differences are described below.%s',
582 - latestWarningName,
583 - urlPropKey,
584 - currentProps[urlPropKey],
585 - urlPropKey,
586 - urlPropKey,
587 - comparisonStatement,
588 - );
589 - return true;
590 - }
591 - }
592 - }
593 - return false;
594 -}
595 -
596 -export function validateLinkPropsForStyleResource(props: Props): boolean {
10 +export function validateLinkPropsForStyleResource(props: any): boolean {
11 if (__DEV__) {
12 // This should only be called when we know we are opting into Resource semantics (i.e. precedence is not null)
13 const {href, onLoad, onError, disabled} = props;
600 - const allProps = ['onLoad', 'onError', 'disabled'];
14 const includedProps = [];
602 - if (onLoad) includedProps.push('onLoad');
603 - if (onError) includedProps.push('onError');
604 - if (disabled != null) includedProps.push('disabled');
15 + if (onLoad) includedProps.push('`onLoad`');
16 + if (onError) includedProps.push('`onError`');
17 + if (disabled != null) includedProps.push('`disabled`');
18
606 - const allPropsUnionPhrase = propNamesListJoin(allProps, 'or');
19 let includedPropsPhrase = propNamesListJoin(includedProps, 'and');
20 includedPropsPhrase += includedProps.length === 1 ? ' prop' : ' props';
21 + const withArticlePhrase =
22 + includedProps.length === 1
23 + ? 'an ' + includedPropsPhrase
24 + : 'the ' + includedPropsPhrase;
25
26 if (includedProps.length) {
27 console.error(
612 - 'A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
613 - ' When using %s React will opt out of Resource behavior. If you meant for this' +
614 - ' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.',
28 + 'React encountered a <link rel="stylesheet" href="%s" ... /> with a `precedence` prop that' +
29 + ' also included %s. The presence of loading and error handlers indicates an intent to manage' +
30 + ' the stylesheet loading state from your from your Component code and React will not hoist or' +
31 + ' deduplicate this stylesheet. If your intent was to have React hoist and deduplciate this stylesheet' +
32 + ' using the `precedence` prop remove the %s, otherwise remove the `precedence` prop.',
33 href,
616 - includedPropsPhrase,
617 - allPropsUnionPhrase,
34 + withArticlePhrase,
35 includedPropsPhrase,
36 );
37 return true;
@@ -645,32 +62,6 @@ function propNamesListJoin(
62 }
63 }
64
648 -export function validateLinkPropsForPreloadResource(linkProps: any) {
649 - if (__DEV__) {
650 - const {href, as} = linkProps;
651 - if (as === 'font') {
652 - const name = getResourceNameForWarning('preload', linkProps, false);
653 - if (!hasOwnProperty.call(linkProps, 'crossOrigin')) {
654 - console.error(
655 - 'A %s with href "%s" did not specify the crossOrigin prop. Font preloads must always use' +
656 - ' anonymouse CORS mode. To fix add an empty string, "anonymous", or any other string' +
657 - ' value except "use-credentials" for the crossOrigin prop of all font preloads.',
658 - name,
659 - href,
660 - );
661 - } else if (linkProps.crossOrigin === 'use-credentials') {
662 - console.error(
663 - 'A %s with href "%s" specified a crossOrigin value of "use-credentials". Font preloads must always use' +
664 - ' anonymouse CORS mode. To fix use an empty string, "anonymous", or any other string' +
665 - ' value except "use-credentials" for the crossOrigin prop of all font preloads.',
666 - name,
667 - href,
668 - );
669 - }
670 - }
671 - }
672 -}
673 -
65 export function validatePreloadArguments(href: mixed, options: mixed) {
66 if (__DEV__) {
67 if (!href || typeof href !== 'string') {
@@ -765,24 +156,429 @@ export function validatePreinitArguments(href: mixed, options: mixed) {
156 }
157 }
158
768 -function getValueDescriptorExpectingObjectForWarning(thing: any): string {
159 +export function getValueDescriptorExpectingObjectForWarning(
160 + thing: any,
161 +): string {
162 return thing === null
770 - ? 'null'
163 + ? '`null`'
164 : thing === undefined
772 - ? 'undefined'
165 + ? '`undefined`'
166 : thing === ''
167 ? 'an empty string'
168 : `something with type "${typeof thing}"`;
169 }
170
778 -function getValueDescriptorExpectingEnumForWarning(thing: any): string {
171 +export function getValueDescriptorExpectingEnumForWarning(thing: any): string {
172 return thing === null
780 - ? 'null'
173 + ? '`null`'
174 : thing === undefined
782 - ? 'undefined'
175 + ? '`undefined`'
176 : thing === ''
177 ? 'an empty string'
178 : typeof thing === 'string'
179 ? JSON.stringify(thing)
180 : `something with type "${typeof thing}"`;
181 }
182 +
183 +type PropDifferences = {
184 + missing: {
185 + [string]: any,
186 + },
187 + extra: {
188 + [string]: any,
189 + },
190 + different: {
191 + [string]: {
192 + original: any,
193 + latest: any,
194 + },
195 + },
196 +};
197 +function compareResourcePropsForWarning(
198 + newProps: any,
199 + currentProps: any,
200 +): null | PropDifferences {
201 + if (__DEV__) {
202 + let propDiffs: null | PropDifferences = null;
203 +
204 + const allProps = Array.from(
205 + new Set(Object.keys(currentProps).concat(Object.keys(newProps))),
206 + );
207 +
208 + for (let i = 0; i < allProps.length; i++) {
209 + const propName = allProps[i];
210 + const newValue = newProps[propName];
211 + const currentValue = currentProps[propName];
212 + if (
213 + newValue !== currentValue &&
214 + !(newValue == null && currentValue == null)
215 + ) {
216 + if (newValue == null) {
217 + if (propDiffs === null) {
218 + propDiffs = ({
219 + missing: {},
220 + extra: {},
221 + different: {},
222 + }: PropDifferences);
223 + }
224 + propDiffs.missing[propName] = currentValue;
225 + } else if (currentValue == null) {
226 + if (propDiffs === null) {
227 + propDiffs = ({
228 + missing: {},
229 + extra: {},
230 + different: {},
231 + }: PropDifferences);
232 + }
233 + propDiffs.extra[propName] = newValue;
234 + } else {
235 + if (propDiffs === null) {
236 + propDiffs = ({
237 + missing: {},
238 + extra: {},
239 + different: {},
240 + }: PropDifferences);
241 + }
242 + propDiffs.different[propName] = {
243 + original: currentValue,
244 + latest: newValue,
245 + };
246 + }
247 + }
248 + }
249 +
250 + return propDiffs;
251 + }
252 + return null;
253 +}
254 +
255 +export function describeDifferencesForStylesheets(
256 + newProps: any,
257 + currentProps: any,
258 +): string {
259 + const diff = compareResourcePropsForWarning(newProps, currentProps);
260 + if (!diff) return '';
261 +
262 + let description = '';
263 +
264 + for (const propName in diff.missing) {
265 + const propValue = diff.missing[propName];
266 + if (propName === 'media') {
267 + description += `\n "${propName}" missing for props, original value: ${getValueDescriptorExpectingEnumForWarning(
268 + propValue,
269 + )}`;
270 + }
271 + }
272 + for (const propName in diff.extra) {
273 + const propValue = diff.extra[propName];
274 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
275 + propValue,
276 + )}, missing from original props`;
277 + }
278 + for (const propName in diff.different) {
279 + const latestValue = diff.different[propName].latest;
280 + const originalValue = diff.different[propName].original;
281 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
282 + latestValue,
283 + )}, original value: ${getValueDescriptorExpectingEnumForWarning(
284 + originalValue,
285 + )}`;
286 + }
287 + return description;
288 +}
289 +
290 +export function describeDifferencesForStylesheetOverPreinit(
291 + newProps: any,
292 + currentProps: any,
293 +): string {
294 + const diff = compareResourcePropsForWarning(newProps, currentProps);
295 + if (!diff) return '';
296 +
297 + let description = '';
298 +
299 + for (const propName in diff.extra) {
300 + const propValue = diff.extra[propName];
301 + if (
302 + propName === 'precedence' ||
303 + propName === 'crossOrigin' ||
304 + propName === 'integrity'
305 + ) {
306 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
307 + propValue,
308 + )}, option missing`;
309 + } else {
310 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
311 + propValue,
312 + )}, option not available with ReactDOM.preinit()`;
313 + }
314 + }
315 + for (const propName in diff.different) {
316 + const latestValue = diff.different[propName].latest;
317 + const originalValue = diff.different[propName].original;
318 + if (propName === 'precedence' && originalValue === 'default') {
319 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
320 + latestValue,
321 + )}, missing from options`;
322 + } else {
323 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
324 + latestValue,
325 + )}, option value: ${getValueDescriptorExpectingEnumForWarning(
326 + originalValue,
327 + )}`;
328 + }
329 + }
330 + return description;
331 +}
332 +
333 +export function describeDifferencesForPreinitOverStylesheet(
334 + newProps: any,
335 + currentProps: any,
336 +): string {
337 + const diff = compareResourcePropsForWarning(newProps, currentProps);
338 + if (!diff) return '';
339 +
340 + let description = '';
341 + for (const propName in diff.missing) {
342 + const propValue = diff.missing[propName];
343 + if (propName === 'precedence' && propValue !== 'default') {
344 + description += `\n "${propName}" missing from options, prop value: ${getValueDescriptorExpectingEnumForWarning(
345 + propValue,
346 + )}`;
347 + }
348 + }
349 + for (const propName in diff.extra) {
350 + const propValue = diff.extra[propName];
351 + if (
352 + propName === 'precedence' ||
353 + propName === 'crossOrigin' ||
354 + propName === 'integrity'
355 + ) {
356 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
357 + propValue,
358 + )}, missing from props`;
359 + }
360 + }
361 + for (const propName in diff.different) {
362 + const latestValue = diff.different[propName].latest;
363 + const originalValue = diff.different[propName].original;
364 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
365 + latestValue,
366 + )}, prop value: ${getValueDescriptorExpectingEnumForWarning(
367 + originalValue,
368 + )}`;
369 + }
370 + return description;
371 +}
372 +
373 +export function describeDifferencesForPreinits(
374 + newProps: any,
375 + currentProps: any,
376 +): string {
377 + const diff = compareResourcePropsForWarning(newProps, currentProps);
378 + if (!diff) return '';
379 +
380 + let description = '';
381 + for (const propName in diff.missing) {
382 + const propValue = diff.missing[propName];
383 + if (propName === 'precedence' && propValue !== 'default') {
384 + description += `\n "${propName}" missing from options, original option value: ${getValueDescriptorExpectingEnumForWarning(
385 + propValue,
386 + )}`;
387 + }
388 + }
389 + for (const propName in diff.extra) {
390 + const propValue = diff.extra[propName];
391 + if (
392 + (propName === 'precedence' && propValue !== 'default') ||
393 + propName === 'crossOrigin' ||
394 + propName === 'integrity'
395 + ) {
396 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
397 + propValue,
398 + )}, missing from original options`;
399 + }
400 + }
401 + for (const propName in diff.different) {
402 + const latestValue = diff.different[propName].latest;
403 + const originalValue = diff.different[propName].original;
404 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
405 + latestValue,
406 + )}, original option value: ${getValueDescriptorExpectingEnumForWarning(
407 + originalValue,
408 + )}`;
409 + }
410 + return description;
411 +}
412 +
413 +const preloadOptionsForComparison = ['as', 'crossOrigin', 'integrity', 'media'];
414 +
415 +export function describeDifferencesForPreloads(
416 + newProps: any,
417 + currentProps: any,
418 +): string {
419 + const diff = compareResourcePropsForWarning(newProps, currentProps);
420 + if (!diff) return '';
421 +
422 + let description = '';
423 + for (const propName in diff.missing) {
424 + const propValue = diff.missing[propName];
425 + if (preloadOptionsForComparison.includes(propName)) {
426 + description += `\n "${propName}" missing from options, original option value: ${getValueDescriptorExpectingEnumForWarning(
427 + propValue,
428 + )}`;
429 + }
430 + }
431 + for (const propName in diff.extra) {
432 + const propValue = diff.extra[propName];
433 + if (preloadOptionsForComparison.includes(propName)) {
434 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
435 + propValue,
436 + )}, missing from original options`;
437 + }
438 + }
439 + for (const propName in diff.different) {
440 + const latestValue = diff.different[propName].latest;
441 + const originalValue = diff.different[propName].original;
442 + if (preloadOptionsForComparison.includes(propName)) {
443 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
444 + latestValue,
445 + )}, original option value: ${getValueDescriptorExpectingEnumForWarning(
446 + originalValue,
447 + )}`;
448 + }
449 + }
450 + return description;
451 +}
452 +
453 +export function describeDifferencesForPreloadOverImplicitPreload(
454 + newProps: any,
455 + currentProps: any,
456 +): string {
457 + const diff = compareResourcePropsForWarning(newProps, currentProps);
458 + if (!diff) return '';
459 +
460 + let description = '';
461 + for (const propName in diff.missing) {
462 + const propValue = diff.missing[propName];
463 + if (preloadOptionsForComparison.includes(propName)) {
464 + description += `\n "${propName}" missing from options, underlying prop value: ${getValueDescriptorExpectingEnumForWarning(
465 + propValue,
466 + )}`;
467 + }
468 + }
469 + for (const propName in diff.extra) {
470 + const propValue = diff.extra[propName];
471 + if (preloadOptionsForComparison.includes(propName)) {
472 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
473 + propValue,
474 + )}, missing from underlying props`;
475 + }
476 + }
477 + for (const propName in diff.different) {
478 + const latestValue = diff.different[propName].latest;
479 + const originalValue = diff.different[propName].original;
480 + if (preloadOptionsForComparison.includes(propName)) {
481 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
482 + latestValue,
483 + )}, underlying prop value: ${getValueDescriptorExpectingEnumForWarning(
484 + originalValue,
485 + )}`;
486 + }
487 + }
488 + return description;
489 +}
490 +
491 +export function describeDifferencesForScripts(
492 + newProps: any,
493 + currentProps: any,
494 +): string {
495 + const diff = compareResourcePropsForWarning(newProps, currentProps);
496 + if (!diff) return '';
497 +
498 + let description = '';
499 +
500 + for (const propName in diff.missing) {
501 + const propValue = diff.missing[propName];
502 + description += `\n "${propName}" missing for props, original value: ${getValueDescriptorExpectingEnumForWarning(
503 + propValue,
504 + )}`;
505 + }
506 + for (const propName in diff.extra) {
507 + const propValue = diff.extra[propName];
508 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
509 + propValue,
510 + )}, missing from original props`;
511 + }
512 + for (const propName in diff.different) {
513 + const latestValue = diff.different[propName].latest;
514 + const originalValue = diff.different[propName].original;
515 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
516 + latestValue,
517 + )}, original value: ${getValueDescriptorExpectingEnumForWarning(
518 + originalValue,
519 + )}`;
520 + }
521 + return description;
522 +}
523 +
524 +export function describeDifferencesForScriptOverPreinit(
525 + newProps: any,
526 + currentProps: any,
527 +): string {
528 + const diff = compareResourcePropsForWarning(newProps, currentProps);
529 + if (!diff) return '';
530 +
531 + let description = '';
532 +
533 + for (const propName in diff.extra) {
534 + const propValue = diff.extra[propName];
535 + if (propName === 'crossOrigin' || propName === 'integrity') {
536 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
537 + propValue,
538 + )}, option missing`;
539 + } else {
540 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
541 + propValue,
542 + )}, option not available with ReactDOM.preinit()`;
543 + }
544 + }
545 + for (const propName in diff.different) {
546 + const latestValue = diff.different[propName].latest;
547 + const originalValue = diff.different[propName].original;
548 + description += `\n "${propName}" prop value: ${getValueDescriptorExpectingEnumForWarning(
549 + latestValue,
550 + )}, option value: ${getValueDescriptorExpectingEnumForWarning(
551 + originalValue,
552 + )}`;
553 + }
554 + return description;
555 +}
556 +
557 +export function describeDifferencesForPreinitOverScript(
558 + newProps: any,
559 + currentProps: any,
560 +): string {
561 + const diff = compareResourcePropsForWarning(newProps, currentProps);
562 + if (!diff) return '';
563 +
564 + let description = '';
565 +
566 + for (const propName in diff.extra) {
567 + const propValue = diff.extra[propName];
568 + if (propName === 'crossOrigin' || propName === 'integrity') {
569 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
570 + propValue,
571 + )}, missing from props`;
572 + }
573 + }
574 + for (const propName in diff.different) {
575 + const latestValue = diff.different[propName].latest;
576 + const originalValue = diff.different[propName].original;
577 + description += `\n "${propName}" option value: ${getValueDescriptorExpectingEnumForWarning(
578 + latestValue,
579 + )}, prop value: ${getValueDescriptorExpectingEnumForWarning(
580 + originalValue,
581 + )}`;
582 + }
583 + return description;
584 +}
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+85 -121
@@ -4608,6 +4608,7 @@ describe('ReactDOMFizzServer', () => {
4608 expect(div.outerHTML).toEqual(
4609 '<div id="app-div">hello<b>world, Foo</b>!</div>',
4610 );
4611 +
4612 // there may be either:
4613 // - an external runtime script and deleted nodes with data attributes
4614 // - extra script nodes containing fizz instructions at the end of container
@@ -5014,154 +5015,117 @@ describe('ReactDOMFizzServer', () => {
5015 });
5016
5017 it('should warn in dev when given an array of length 2 or more', async () => {
5017 - const originalConsoleError = console.error;
5018 - const mockError = jest.fn();
5019 - console.error = (...args) => {
5020 - if (args.length > 1) {
5021 - if (typeof args[1] === 'object') {
5022 - mockError(args[0].split('\n')[0]);
5023 - return;
5024 - }
5025 - }
5026 - mockError(...args.map(normalizeCodeLocInfo));
5027 - };
5028 -
5029 - // a Single string child
5018 function App() {
5019 return <title>{['hello1', 'hello2']}</title>;
5020 }
5021
5034 - try {
5035 - prepareJSDOMForTitle();
5022 + prepareJSDOMForTitle();
5023
5024 + await expect(async () => {
5025 await act(async () => {
5026 const {pipe} = renderToPipeableStream(<App />);
5027 pipe(writable);
5028 });
5041 - if (__DEV__) {
5042 - expect(mockError).toHaveBeenCalledWith(
5043 - 'Warning: A title element received an array with more than 1 element as children. ' +
5044 - 'In browsers title Elements can only have Text Nodes as children. If ' +
5045 - 'the children being rendered output more than a single text node in aggregate the browser ' +
5046 - 'will display markup and comments as text in the title and hydration will likely fail and ' +
5047 - 'fall back to client rendering%s',
5048 - '\n' + ' in title (at **)\n' + ' in App (at **)',
5049 - );
5050 - } else {
5051 - expect(mockError).not.toHaveBeenCalled();
5052 - }
5029 + }).toErrorDev([
5030 + 'React expects the `children` prop of <title> tags to be a string, number, or object with a novel `toString` method but found an Array with length 2 instead. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value which is why Arrays of length greater than 1 are not supported. When using JSX it can be commong to combine text nodes and value nodes. For example: <title>hello {nameOfUser}</title>. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop is using this form try rewriting it using a template string: <title>{`hello ${nameOfUser}`}</title>.',
5031 + ]);
5032
5054 - if (gate(flags => flags.enableFloat)) {
5055 - // This title was invalid so it is not emitted
5056 - expect(getVisibleChildren(container)).toEqual(undefined);
5057 - } else {
5058 - expect(getVisibleChildren(container)).toEqual(
5059 - <title>{'hello1<!-- -->hello2'}</title>,
5060 - );
5061 - }
5033 + if (gate(flags => flags.enableFloat)) {
5034 + expect(getVisibleChildren(container)).toEqual(<title />);
5035 + } else {
5036 + expect(getVisibleChildren(container)).toEqual(
5037 + <title>{'hello1<!-- -->hello2'}</title>,
5038 + );
5039 + }
5040
5063 - const errors = [];
5064 - ReactDOMClient.hydrateRoot(container, <App />, {
5065 - onRecoverableError(error) {
5066 - errors.push(error.message);
5067 - },
5068 - });
5069 - expect(Scheduler).toFlushAndYield([]);
5070 - if (gate(flags => flags.enableFloat)) {
5071 - expect(errors).toEqual([]);
5072 - // with float, the title doesn't render on the client or on the server
5073 - expect(getVisibleChildren(container)).toEqual(undefined);
5074 - } else {
5075 - expect(errors).toEqual(
5076 - [
5077 - gate(flags => flags.enableClientRenderFallbackOnTextMismatch)
5078 - ? 'Text content does not match server-rendered HTML.'
5079 - : null,
5080 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
5081 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
5082 - ].filter(Boolean),
5083 - );
5084 - expect(getVisibleChildren(container)).toEqual(
5085 - <title>{['hello1', 'hello2']}</title>,
5086 - );
5087 - }
5088 - } finally {
5089 - console.error = originalConsoleError;
5041 + const errors = [];
5042 + ReactDOMClient.hydrateRoot(container, <App />, {
5043 + onRecoverableError(error) {
5044 + errors.push(error.message);
5045 + },
5046 + });
5047 + expect(Scheduler).toFlushAndYield([]);
5048 + if (gate(flags => flags.enableFloat)) {
5049 + expect(errors).toEqual([]);
5050 + // with float, the title doesn't render on the client or on the server
5051 + expect(getVisibleChildren(container)).toEqual(<title />);
5052 + } else {
5053 + expect(errors).toEqual(
5054 + [
5055 + gate(flags => flags.enableClientRenderFallbackOnTextMismatch)
5056 + ? 'Text content does not match server-rendered HTML.'
5057 + : null,
5058 + 'Hydration failed because the initial UI does not match what was rendered on the server.',
5059 + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
5060 + ].filter(Boolean),
5061 + );
5062 + expect(getVisibleChildren(container)).toEqual(
5063 + <title>{['hello1', 'hello2']}</title>,
5064 + );
5065 }
5066 });
5067
5068 it('should warn in dev if you pass a React Component as a child to <title>', async () => {
5094 - const originalConsoleError = console.error;
5095 - const mockError = jest.fn();
5096 - console.error = (...args) => {
5097 - if (args.length > 1) {
5098 - if (typeof args[1] === 'object') {
5099 - mockError(args[0].split('\n')[0]);
5100 - return;
5101 - }
5102 - }
5103 - mockError(...args.map(normalizeCodeLocInfo));
5104 - };
5105 -
5069 function IndirectTitle() {
5070 return 'hello';
5071 }
5072
5073 function App() {
5074 return (
5112 - <title>
5113 - <IndirectTitle />
5114 - </title>
5075 + <>
5076 + <title>
5077 + <IndirectTitle />
5078 + </title>
5079 + </>
5080 );
5081 }
5082
5118 - try {
5119 - prepareJSDOMForTitle();
5083 + prepareJSDOMForTitle();
5084
5121 - await act(async () => {
5122 - const {pipe} = renderToPipeableStream(<App />);
5123 - pipe(writable);
5124 - });
5125 - if (__DEV__) {
5126 - expect(mockError).toHaveBeenCalledWith(
5127 - 'Warning: A title element received a React element for children. ' +
5128 - 'In the browser title Elements can only have Text Nodes as children. If ' +
5129 - 'the children being rendered output more than a single text node in aggregate the browser ' +
5130 - 'will display markup and comments as text in the title and hydration will likely fail and ' +
5131 - 'fall back to client rendering%s',
5132 - '\n' + ' in title (at **)\n' + ' in App (at **)',
5133 - );
5134 - } else {
5135 - expect(mockError).not.toHaveBeenCalled();
5136 - }
5085 + if (gate(flags => flags.enableFloat)) {
5086 + await expect(async () => {
5087 + await act(async () => {
5088 + const {pipe} = renderToPipeableStream(<App />);
5089 + pipe(writable);
5090 + });
5091 + }).toErrorDev([
5092 + 'React expects the `children` prop of <title> tags to be a string, number, or object with a novel `toString` method but found an object that appears to be a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.',
5093 + ]);
5094 + } else {
5095 + await expect(async () => {
5096 + await act(async () => {
5097 + const {pipe} = renderToPipeableStream(<App />);
5098 + pipe(writable);
5099 + });
5100 + }).toErrorDev([
5101 + 'A title element received a React element for children. In the browser title Elements can only have Text Nodes as children. If the children being rendered output more than a single text node in aggregate the browser will display markup and comments as text in the title and hydration will likely fail and fall back to client rendering',
5102 + ]);
5103 + }
5104
5138 - if (gate(flags => flags.enableFloat)) {
5139 - // object titles are toStringed when float is on
5140 - expect(getVisibleChildren(container)).toEqual(
5141 - <title>{'[object Object]'}</title>,
5142 - );
5143 - } else {
5144 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5145 - }
5105 + if (gate(flags => flags.enableFloat)) {
5106 + // object titles are toStringed when float is on
5107 + expect(getVisibleChildren(container)).toEqual(
5108 + <title>{'[object Object]'}</title>,
5109 + );
5110 + } else {
5111 + expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5112 + }
5113
5147 - const errors = [];
5148 - ReactDOMClient.hydrateRoot(container, <App />, {
5149 - onRecoverableError(error) {
5150 - errors.push(error.message);
5151 - },
5152 - });
5153 - expect(Scheduler).toFlushAndYield([]);
5154 - expect(errors).toEqual([]);
5155 - if (gate(flags => flags.enableFloat)) {
5156 - // object titles are toStringed when float is on
5157 - expect(getVisibleChildren(container)).toEqual(
5158 - <title>{'[object Object]'}</title>,
5159 - );
5160 - } else {
5161 - expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5162 - }
5163 - } finally {
5164 - console.error = originalConsoleError;
5114 + const errors = [];
5115 + ReactDOMClient.hydrateRoot(container, <App />, {
5116 + onRecoverableError(error) {
5117 + errors.push(error.message);
5118 + },
5119 + });
5120 + expect(Scheduler).toFlushAndYield([]);
5121 + expect(errors).toEqual([]);
5122 + if (gate(flags => flags.enableFloat)) {
5123 + // object titles are toStringed when float is on
5124 + expect(getVisibleChildren(container)).toEqual(
5125 + <title>{'[object Object]'}</title>,
5126 + );
5127 + } else {
5128 + expect(getVisibleChildren(container)).toEqual(<title>hello</title>);
5129 }
5130 });
5131
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+9 -3
@@ -62,9 +62,15 @@ describe('ReactDOMFizzServerBrowser', () => {
62 </html>,
63 );
64 const result = await readResult(stream);
65 - expect(result).toMatchInlineSnapshot(
66 - `"<!DOCTYPE html><html><body>hello world</body></html>"`,
67 - );
65 + if (gate(flags => flags.enableFloat)) {
66 + expect(result).toMatchInlineSnapshot(
67 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
68 + );
69 + } else {
70 + expect(result).toMatchInlineSnapshot(
71 + `"<!DOCTYPE html><html><body>hello world</body></html>"`,
72 + );
73 + }
74 });
75
76 it('should emit bootstrap script src at the end', async () => {
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+10 -3
@@ -73,9 +73,16 @@ describe('ReactDOMFizzServerNode', () => {
73 );
74 pipe(writable);
75 jest.runAllTimers();
76 - expect(output.result).toMatchInlineSnapshot(
77 - `"<!DOCTYPE html><html><body>hello world</body></html>"`,
78 - );
76 + if (gate(flags => flags.enableFloat)) {
77 + // with Float, we emit empty heads if they are elided when rendering <html>
78 + expect(output.result).toMatchInlineSnapshot(
79 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
80 + );
81 + } else {
82 + expect(output.result).toMatchInlineSnapshot(
83 + `"<!DOCTYPE html><html><body>hello world</body></html>"`,
84 + );
85 + }
86 });
87
88 it('should emit bootstrap script src at the end', () => {
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+9 -3
@@ -64,9 +64,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
64 </html>,
65 );
66 const prelude = await readContent(result.prelude);
67 - expect(prelude).toMatchInlineSnapshot(
68 - `"<!DOCTYPE html><html><body>hello world</body></html>"`,
69 - );
67 + if (gate(flags => flags.enableFloat)) {
68 + expect(prelude).toMatchInlineSnapshot(
69 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
70 + );
71 + } else {
72 + expect(prelude).toMatchInlineSnapshot(
73 + `"<!DOCTYPE html><html><body>hello world</body></html>"`,
74 + );
75 + }
76 });
77
78 // @gate experimental
packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
+9 -3
@@ -63,9 +63,15 @@ describe('ReactDOMFizzStaticNode', () => {
63 </html>,
64 );
65 const prelude = await readContent(result.prelude);
66 - expect(prelude).toMatchInlineSnapshot(
67 - `"<!DOCTYPE html><html><body>hello world</body></html>"`,
68 - );
66 + if (gate(flags => flags.enableFloat)) {
67 + expect(prelude).toMatchInlineSnapshot(
68 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
69 + );
70 + } else {
71 + expect(prelude).toMatchInlineSnapshot(
72 + `"<!DOCTYPE html><html><body>hello world</body></html>"`,
73 + );
74 + }
75 });
76
77 // @gate experimental
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+3547 -4703
@@ -39,6 +39,14 @@ function resetJSDOM(markup) {
39 const jsdom = new JSDOM(markup, {
40 runScripts: 'dangerously',
41 });
42 + // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else
43 + Object.defineProperty(jsdom.window, 'matchMedia', {
44 + writable: true,
45 + value: jest.fn().mockImplementation(query => ({
46 + matches: query === 'all' || query === '',
47 + media: query,
48 + })),
49 + });
50 window = jsdom.window;
51 document = jsdom.window.document;
52 }
@@ -80,21 +88,6 @@ describe('ReactDOMFloat', () => {
88 }
89 });
90
83 - function normalizeCodeLocInfo(str) {
84 - return (
85 - typeof str === 'string' &&
86 - str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
87 - return '\n in ' + name + ' (at **)';
88 - })
89 - );
90 - }
91 -
92 - function componentStack(components) {
93 - return components
94 - .map(component => `\n in ${component} (at **)`)
95 - .join('');
96 - }
97 -
91 async function act(callback) {
92 await callback();
93 // Await one turn around the event loop.
@@ -196,6 +189,11 @@ describe('ReactDOMFloat', () => {
189 : children;
190 }
191
192 + function BlockedOn({value, children}) {
193 + readText(value);
194 + return children;
195 + }
196 +
197 function resolveText(text) {
198 const record = textCache.get(text);
199 if (record === undefined) {
@@ -257,6 +255,19 @@ describe('ReactDOMFloat', () => {
255 );
256 }
257
258 + function renderSafelyAndExpect(root, children) {
259 + root.render(children);
260 + return expect(() => {
261 + try {
262 + expect(Scheduler).toFlushWithoutYielding();
263 + } catch (e) {
264 + try {
265 + expect(Scheduler).toFlushWithoutYielding();
266 + } catch (f) {}
267 + }
268 + });
269 + }
270 +
271 // @gate enableFloat
272 it('can render resources before singletons', async () => {
273 const root = ReactDOMClient.createRoot(document);
@@ -290,19 +301,6 @@ describe('ReactDOMFloat', () => {
301 );
302 });
303
293 - function renderSafelyAndExpect(root, children) {
294 - root.render(children);
295 - return expect(() => {
296 - try {
297 - expect(Scheduler).toFlushWithoutYielding();
298 - } catch (e) {
299 - try {
300 - expect(Scheduler).toFlushWithoutYielding();
301 - } catch (f) {}
302 - }
303 - });
304 - }
305 -
304 // @gate enableFloat
305 it('can hydrate non Resources in head when Resources are also inserted there', async () => {
306 await actIntoEmptyDocument(() => {
@@ -326,11 +324,12 @@ describe('ReactDOMFloat', () => {
324 expect(getMeaningfulChildren(document)).toEqual(
325 <html>
326 <head>
329 - <base target="foo" href="bar" />
327 <link rel="preload" href="foo" as="script" />
328 <meta property="foo" content="bar" />
329 <title>foo</title>
330 + <link rel="foo" href="bar" />
331 <noscript>&lt;link rel="icon" href="icon"/&gt;</noscript>
332 + <base target="foo" href="bar" />
333 </head>
334 <body>foo</body>
335 </html>,
@@ -356,12 +355,13 @@ describe('ReactDOMFloat', () => {
355 expect(getMeaningfulChildren(document)).toEqual(
356 <html>
357 <head>
359 - <base target="foo" href="bar" />
358 <link rel="preload" href="foo" as="script" />
359 <meta property="foo" content="bar" />
360 <title>foo</title>
361 <link rel="foo" href="bar" />
362 + <link rel="foo" href="bar" />
363 <noscript>&lt;link rel="icon" href="icon"/&gt;</noscript>
364 + <base target="foo" href="bar" />
365 <script async="" src="foo" />
366 </head>
367 <body>foo</body>
@@ -407,7 +407,7 @@ describe('ReactDOMFloat', () => {
407 <style>foo</style>
408 </html>,
409 ).toErrorDev([
410 - 'Cannot render <style> outside the main document. Try moving it into the root <head> tag.',
410 + 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key. React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. Note that hoisting <style> tags is considered an advanced feature that most will not use directly. Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style> to the <style> tag.',
411 'Warning: validateDOMNesting(...): <style> cannot appear as a child of <html>.',
412 ]);
413
@@ -473,14 +473,14 @@ describe('ReactDOMFloat', () => {
473 const root = ReactDOMClient.createRoot(container);
474 root.render(
475 <>
476 - <title>foo</title>
476 + <script async={true} src="foo" />
477 </>,
478 );
479 expect(Scheduler).toFlushWithoutYielding();
480 expect(getMeaningfulChildren(document)).toEqual(
481 <html>
482 <head>
483 - <title>foo</title>
483 + <script async="" src="foo" />
484 </head>
485 <body>
486 <div id="container" />
@@ -488,11 +488,10 @@ describe('ReactDOMFloat', () => {
488 </html>,
489 );
490
491 - // title is keyed off children so this second resource should match the first one
491 root.render(
492 <>
493 {null}
495 - <title data-new="new">foo</title>
494 + <script data-new="new" async={true} src="foo" />
495 </>,
496 );
497 expect(Scheduler).toFlushWithoutYielding();
@@ -500,7 +499,7 @@ describe('ReactDOMFloat', () => {
499 expect(getMeaningfulChildren(document)).toEqual(
500 <html>
501 <head>
503 - <title>foo</title>
502 + <script async="" src="foo" />
503 </head>
504 <body>
505 <div id="container" />
@@ -509,25 +508,6 @@ describe('ReactDOMFloat', () => {
508 );
509 });
510
512 - // @gate enableFloat
513 - it('errors if the document does not contain a head when inserting a resource', async () => {
514 - document.head.parentNode.removeChild(document.head);
515 - const root = ReactDOMClient.createRoot(document);
516 - root.render(
517 - <html>
518 - <body>
519 - <link rel="stylesheet" href="foo" precedence="default" />
520 - foo
521 - </body>
522 - </html>,
523 - );
524 - expect(() => {
525 - expect(Scheduler).toFlushWithoutYielding();
526 - }).toThrow(
527 - 'While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.',
528 - );
529 - });
530 -
511 // @gate enableFloat
512 it('emits resources before everything else when rendering with no head', async () => {
513 function App() {
@@ -583,7 +563,7 @@ describe('ReactDOMFloat', () => {
563 });
564
565 // @gate enableFloat
586 - it('does not emit closing tags in out of order position when rendering a non-void resource type', async () => {
566 + it('emits an implicit <head> element to hold resources when none is rendered but an <html> is rendered', async () => {
567 const chunks = [];
568
569 writable.on('data', chunk => {
@@ -603,7 +583,7 @@ describe('ReactDOMFloat', () => {
583 pipe(writable);
584 });
585 expect(chunks).toEqual([
606 - '<!DOCTYPE html><html><script async="" src="foo"></script><title>foo</title><body>bar',
586 + '<!DOCTYPE html><html><head><script async="" src="foo"></script><title>foo</title></head><body>bar',
587 '</body></html>',
588 ]);
589 });
@@ -642,5295 +622,4159 @@ describe('ReactDOMFloat', () => {
622 ).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
623 });
624
645 - describe('HostResource', () => {
646 - // @gate enableFloat
647 - it('warns when you update props to an invalid type', async () => {
648 - const root = ReactDOMClient.createRoot(container);
649 - root.render(
650 - <div>
651 - <link rel="stylesheet" href="foo" precedence="foo" />
652 - <link rel="stylesheet" href="bar" precedence="foo" />
653 - </div>,
654 - );
655 - expect(Scheduler).toFlushWithoutYielding();
656 - root.render(
657 - <div>
658 - <link rel={() => {}} href="bar" />
659 - <link rel="foo" href={() => {}} />
660 - </div>,
661 - );
662 - expect(() => {
663 - expect(Scheduler).toFlushWithoutYielding();
664 - }).toErrorDev([
665 - 'Warning: A <link> previously rendered as a Resource with href "foo" with rel ""stylesheet"" but was updated with an invalid rel: something with type "function". When a link does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
666 - 'Warning: A <link> previously rendered as a Resource with href "bar" but was updated with an invalid href prop: something with type "function". When a link does not have a valid href prop it is not represented in the DOM. If this is intentional, instead do not render the <link> anymore.',
667 - ]);
668 - expect(getMeaningfulChildren(document)).toEqual(
625 + // @gate enableFloat
626 + it('can avoid inserting a late stylesheet if it already rendered on the client', async () => {
627 + await actIntoEmptyDocument(() => {
628 + renderToPipeableStream(
629 <html>
670 - <head>
671 - <link rel="stylesheet" href="foo" data-precedence="foo" />
672 - <link rel="stylesheet" href="bar" data-precedence="foo" />
673 - <link rel="preload" as="style" href="foo" />
674 - <link rel="preload" as="style" href="bar" />
675 - </head>
630 <body>
677 - <div id="container">
678 - <div />
679 - </div>
631 + <Suspense fallback="loading foo...">
632 + <BlockedOn value="foo">
633 + <link rel="stylesheet" href="foo" precedence="foo" />
634 + foo
635 + </BlockedOn>
636 + </Suspense>
637 + <Suspense fallback="loading bar...">
638 + <BlockedOn value="bar">
639 + <link rel="stylesheet" href="bar" precedence="bar" />
640 + bar
641 + </BlockedOn>
642 + </Suspense>
643 </body>
644 </html>,
682 - );
645 + ).pipe(writable);
646 });
684 - });
647
686 - describe('ReactDOM.preload', () => {
687 - // @gate enableFloat
688 - it('inserts a preload resource into the stream when called during server rendering', async () => {
689 - function Component() {
690 - ReactDOM.preload('foo', {as: 'style'});
691 - return 'foo';
692 - }
693 - await actIntoEmptyDocument(() => {
694 - const {pipe} = renderToPipeableStream(
695 - <html>
696 - <head />
697 - <body>
698 - <Component />
699 - </body>
700 - </html>,
701 - );
702 - pipe(writable);
703 - });
704 - expect(getMeaningfulChildren(document)).toEqual(
705 - <html>
706 - <head>
707 - <link rel="preload" as="style" href="foo" />
708 - </head>
709 - <body>foo</body>
710 - </html>,
648 + expect(getMeaningfulChildren(document)).toEqual(
649 + <html>
650 + <head />
651 + <body>
652 + {'loading foo...'}
653 + {'loading bar...'}
654 + </body>
655 + </html>,
656 + );
657 +
658 + ReactDOMClient.hydrateRoot(
659 + document,
660 + <html>
661 + <body>
662 + <link rel="stylesheet" href="foo" precedence="foo" />
663 + <Suspense fallback="loading foo...">
664 + <link rel="stylesheet" href="foo" precedence="foo" />
665 + foo
666 + </Suspense>
667 + <Suspense fallback="loading bar...">
668 + <link rel="stylesheet" href="bar" precedence="bar" />
669 + bar
670 + </Suspense>
671 + </body>
672 + </html>,
673 + );
674 + expect(Scheduler).toFlushWithoutYielding();
675 + expect(getMeaningfulChildren(document)).toEqual(
676 + <html>
677 + <head>
678 + <link rel="stylesheet" href="foo" data-precedence="foo" />
679 + <link as="style" href="foo" rel="preload" />
680 + </head>
681 + <body>
682 + {'loading foo...'}
683 + {'loading bar...'}
684 + </body>
685 + </html>,
686 + );
687 +
688 + await act(() => {
689 + resolveText('bar');
690 + });
691 + await act(() => {
692 + const sheets = document.querySelectorAll(
693 + 'link[rel="stylesheet"][data-precedence]',
694 );
695 + const event = document.createEvent('Event');
696 + event.initEvent('load', true, true);
697 + for (let i = 0; i < sheets.length; i++) {
698 + sheets[i].dispatchEvent(event);
699 + }
700 });
701 + expect(getMeaningfulChildren(document)).toEqual(
702 + <html>
703 + <head>
704 + <link rel="stylesheet" href="foo" data-precedence="foo" />
705 + <link rel="stylesheet" href="bar" data-precedence="bar" />
706 + <link as="style" href="foo" rel="preload" />
707 + </head>
708 + <body>
709 + {'loading foo...'}
710 + {'bar'}
711 + <link as="style" href="bar" rel="preload" />
712 + </body>
713 + </html>,
714 + );
715
714 - // @gate enableFloat
715 - it('inserts a preload resource into the document during render when called during client rendering', async () => {
716 - function Component() {
717 - ReactDOM.preload('foo', {as: 'style'});
718 - return 'foo';
719 - }
720 - const root = ReactDOMClient.createRoot(container);
721 - root.render(<Component />);
722 - expect(Scheduler).toFlushWithoutYielding();
723 - expect(getMeaningfulChildren(document)).toEqual(
724 - <html>
725 - <head>
726 - <link rel="preload" as="style" href="foo" />
727 - </head>
728 - <body>
729 - <div id="container">foo</div>
730 - </body>
731 - </html>,
716 + await act(() => {
717 + resolveText('foo');
718 + });
719 + await act(() => {
720 + const sheets = document.querySelectorAll(
721 + 'link[rel="stylesheet"][data-precedence]',
722 );
723 + const event = document.createEvent('Event');
724 + event.initEvent('load', true, true);
725 + for (let i = 0; i < sheets.length; i++) {
726 + sheets[i].dispatchEvent(event);
727 + }
728 });
729 + expect(getMeaningfulChildren(document)).toEqual(
730 + <html>
731 + <head>
732 + <link rel="stylesheet" href="foo" data-precedence="foo" />
733 + <link rel="stylesheet" href="bar" data-precedence="bar" />
734 + <link as="style" href="foo" rel="preload" />
735 + </head>
736 + <body>
737 + {'foo'}
738 + {'bar'}
739 + <link as="style" href="bar" rel="preload" />
740 + <link as="style" href="foo" rel="preload" />
741 + </body>
742 + </html>,
743 + );
744 + });
745
735 - // @gate enableFloat
736 - it('inserts a preload resource when called in a layout effect', async () => {
737 - function App() {
738 - React.useLayoutEffect(() => {
739 - ReactDOM.preload('foo', {as: 'style'});
740 - }, []);
741 - return 'foobar';
742 - }
743 - const root = ReactDOMClient.createRoot(container);
744 - root.render(<App />);
745 - expect(Scheduler).toFlushWithoutYielding();
746 + // @gate enableFloat
747 + it('can hoist <link rel="stylesheet" .../> and <style /> tags together, respecting order of discovery', async () => {
748 + const css = `
749 +body {
750 + background-color: red;
751 +}`;
752
747 - expect(getMeaningfulChildren(document)).toEqual(
753 + await actIntoEmptyDocument(() => {
754 + renderToPipeableStream(
755 <html>
749 - <head>
750 - <link rel="preload" as="style" href="foo" />
751 - </head>
756 <body>
753 - <div id="container">foobar</div>
757 + <link rel="stylesheet" href="one1" precedence="one" />
758 + <style href="two1" precedence="two">
759 + {css}
760 + </style>
761 + <link rel="stylesheet" href="three1" precedence="three" />
762 + <style href="four1" precedence="four">
763 + {css}
764 + </style>
765 + <Suspense>
766 + <BlockedOn value="block">
767 + <link rel="stylesheet" href="one2" precedence="one" />
768 + <link rel="stylesheet" href="two2" precedence="two" />
769 + <style href="three2" precedence="three">
770 + {css}
771 + </style>
772 + <style href="four2" precedence="four">
773 + {css}
774 + </style>
775 + <link rel="stylesheet" href="five1" precedence="five" />
776 + </BlockedOn>
777 + </Suspense>
778 + <Suspense>
779 + <BlockedOn value="block2">
780 + <style href="one3" precedence="one">
781 + {css}
782 + </style>
783 + <style href="two3" precedence="two">
784 + {css}
785 + </style>
786 + <link rel="stylesheet" href="three3" precedence="three" />
787 + <link rel="stylesheet" href="four3" precedence="four" />
788 + <style href="six1" precedence="six">
789 + {css}
790 + </style>
791 + </BlockedOn>
792 + </Suspense>
793 + <Suspense>
794 + <BlockedOn value="block again">
795 + <link rel="stylesheet" href="one2" precedence="one" />
796 + <link rel="stylesheet" href="two2" precedence="two" />
797 + <style href="three2" precedence="three">
798 + {css}
799 + </style>
800 + <style href="four2" precedence="four">
801 + {css}
802 + </style>
803 + <link rel="stylesheet" href="five1" precedence="five" />
804 + </BlockedOn>
805 + </Suspense>
806 </body>
807 </html>,
756 - );
808 + ).pipe(writable);
809 });
810
759 - // @gate enableFloat
760 - it('inserts a preload resource when called in a passive effect', async () => {
761 - function App() {
762 - React.useEffect(() => {
763 - ReactDOM.preload('foo', {as: 'style'});
764 - }, []);
765 - return 'foobar';
766 - }
767 - const root = ReactDOMClient.createRoot(container);
768 - root.render(<App />);
769 - expect(Scheduler).toFlushWithoutYielding();
811 + expect(getMeaningfulChildren(document)).toEqual(
812 + <html>
813 + <head>
814 + <link rel="stylesheet" href="one1" data-precedence="one" />
815 + <style data-href="two1" data-precedence="two">
816 + {css}
817 + </style>
818 + <link rel="stylesheet" href="three1" data-precedence="three" />
819 + <style data-href="four1" data-precedence="four">
820 + {css}
821 + </style>
822 + </head>
823 + <body />
824 + </html>,
825 + );
826
771 - expect(getMeaningfulChildren(document)).toEqual(
772 - <html>
773 - <head>
774 - <link rel="preload" as="style" href="foo" />
775 - </head>
776 - <body>
777 - <div id="container">foobar</div>
778 - </body>
779 - </html>,
780 - );
827 + await act(() => {
828 + resolveText('block');
829 });
830
783 - // @gate enableFloat
784 - it('inserts a preload resource when called in module scope if a root has already been created', async () => {
785 - // The requirement that a root be created has to do with bootstrapping the dispatcher.
786 - // We are intentionally avoiding setting it to the default via import due to cycles and
787 - // we are trying to avoid doing a mutable initailation in module scope.
788 - ReactDOM.preload('foo', {as: 'style'});
789 - ReactDOMClient.createRoot(container);
790 - ReactDOM.preload('bar', {as: 'style'});
791 - // We need to use global.document because preload falls back
792 - // to the window.document global when no other documents have been used
793 - // The way the JSDOM runtim is created for these tests the local document
794 - // global does not point to the global.document
795 - expect(getMeaningfulChildren(global.document)).toEqual(
796 - <html>
797 - <head>
798 - <link rel="preload" as="style" href="bar" />
799 - </head>
800 - <body />
801 - </html>,
802 - );
831 + expect(getMeaningfulChildren(document)).toEqual(
832 + <html>
833 + <head>
834 + <link rel="stylesheet" href="one1" data-precedence="one" />
835 + <link rel="stylesheet" href="one2" data-precedence="one" />
836 + <style data-href="two1" data-precedence="two">
837 + {css}
838 + </style>
839 + <link rel="stylesheet" href="two2" data-precedence="two" />
840 + <link rel="stylesheet" href="three1" data-precedence="three" />
841 + <style data-href="three2" data-precedence="three">
842 + {css}
843 + </style>
844 + <style data-href="four1" data-precedence="four">
845 + {css}
846 + </style>
847 + <style data-href="four2" data-precedence="four">
848 + {css}
849 + </style>
850 + <link rel="stylesheet" href="five1" data-precedence="five" />
851 + </head>
852 + <body>
853 + <link rel="preload" href="one2" as="style" />
854 + <link rel="preload" href="two2" as="style" />
855 + <link rel="preload" href="five1" as="style" />
856 + </body>
857 + </html>,
858 + );
859 +
860 + await act(() => {
861 + resolveText('block2');
862 });
863
805 - // @gate enableFloat
806 - it('supports script preloads', async () => {
807 - function ServerApp() {
808 - ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
809 - ReactDOM.preload('bar', {
810 - as: 'script',
811 - crossOrigin: 'use-credentials',
812 - integrity: 'bar hash',
813 - });
814 - return (
815 - <html>
816 - <link rel="preload" href="baz" as="script" />
817 - <head>
818 - <title>hi</title>
819 - </head>
820 - <body>foo</body>
821 - </html>
822 - );
823 - }
824 - function ClientApp() {
825 - ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'});
826 - ReactDOM.preload('qux', {as: 'script'});
827 - return (
828 - <html>
829 - <head>
830 - <title>hi</title>
831 - </head>
832 - <body>foo</body>
833 - <link
834 - rel="preload"
835 - href="quux"
836 - as="script"
837 - crossOrigin=""
838 - integrity="quux hash"
839 - />
840 - </html>
841 - );
842 - }
864 + expect(getMeaningfulChildren(document)).toEqual(
865 + <html>
866 + <head>
867 + <link rel="stylesheet" href="one1" data-precedence="one" />
868 + <link rel="stylesheet" href="one2" data-precedence="one" />
869 + <style data-href="one3" data-precedence="one">
870 + {css}
871 + </style>
872 + <style data-href="two1" data-precedence="two">
873 + {css}
874 + </style>
875 + <link rel="stylesheet" href="two2" data-precedence="two" />
876 + <style data-href="two3" data-precedence="two">
877 + {css}
878 + </style>
879 + <link rel="stylesheet" href="three1" data-precedence="three" />
880 + <style data-href="three2" data-precedence="three">
881 + {css}
882 + </style>
883 + <link rel="stylesheet" href="three3" data-precedence="three" />
884 + <style data-href="four1" data-precedence="four">
885 + {css}
886 + </style>
887 + <style data-href="four2" data-precedence="four">
888 + {css}
889 + </style>
890 + <link rel="stylesheet" href="four3" data-precedence="four" />
891 + <link rel="stylesheet" href="five1" data-precedence="five" />
892 + <style data-href="six1" data-precedence="six">
893 + {css}
894 + </style>
895 + </head>
896 + <body>
897 + <link rel="preload" href="one2" as="style" />
898 + <link rel="preload" href="two2" as="style" />
899 + <link rel="preload" href="five1" as="style" />
900 + <link rel="preload" href="three3" as="style" />
901 + <link rel="preload" href="four3" as="style" />
902 + </body>
903 + </html>,
904 + );
905
844 - await actIntoEmptyDocument(() => {
845 - const {pipe} = renderToPipeableStream(<ServerApp />);
846 - pipe(writable);
847 - });
848 - expect(getMeaningfulChildren(document)).toEqual(
849 - <html>
850 - <head>
851 - <link rel="preload" as="script" href="foo" integrity="foo hash" />
852 - <link
853 - rel="preload"
854 - as="script"
855 - href="bar"
856 - crossorigin="use-credentials"
857 - integrity="bar hash"
858 - />
859 - <link rel="preload" as="script" href="baz" />
860 - <title>hi</title>
861 - </head>
862 - <body>foo</body>
863 - </html>,
864 - );
906 + await act(() => {
907 + resolveText('block again');
908 + });
909
866 - ReactDOMClient.hydrateRoot(document, <ClientApp />);
867 - expect(Scheduler).toFlushWithoutYielding();
910 + expect(getMeaningfulChildren(document)).toEqual(
911 + <html>
912 + <head>
913 + <link rel="stylesheet" href="one1" data-precedence="one" />
914 + <link rel="stylesheet" href="one2" data-precedence="one" />
915 + <style data-href="one3" data-precedence="one">
916 + {css}
917 + </style>
918 + <style data-href="two1" data-precedence="two">
919 + {css}
920 + </style>
921 + <link rel="stylesheet" href="two2" data-precedence="two" />
922 + <style data-href="two3" data-precedence="two">
923 + {css}
924 + </style>
925 + <link rel="stylesheet" href="three1" data-precedence="three" />
926 + <style data-href="three2" data-precedence="three">
927 + {css}
928 + </style>
929 + <link rel="stylesheet" href="three3" data-precedence="three" />
930 + <style data-href="four1" data-precedence="four">
931 + {css}
932 + </style>
933 + <style data-href="four2" data-precedence="four">
934 + {css}
935 + </style>
936 + <link rel="stylesheet" href="four3" data-precedence="four" />
937 + <link rel="stylesheet" href="five1" data-precedence="five" />
938 + <style data-href="six1" data-precedence="six">
939 + {css}
940 + </style>
941 + </head>
942 + <body>
943 + <link rel="preload" href="one2" as="style" />
944 + <link rel="preload" href="two2" as="style" />
945 + <link rel="preload" href="five1" as="style" />
946 + <link rel="preload" href="three3" as="style" />
947 + <link rel="preload" href="four3" as="style" />
948 + </body>
949 + </html>,
950 + );
951
869 - expect(getMeaningfulChildren(document)).toEqual(
870 - <html>
871 - <head>
872 - <link rel="preload" as="script" href="foo" integrity="foo hash" />
873 - <link
874 - rel="preload"
875 - as="script"
876 - href="bar"
877 - crossorigin="use-credentials"
878 - integrity="bar hash"
879 - />
880 - <link rel="preload" as="script" href="baz" />
881 - <title>hi</title>
882 - <link rel="preload" as="script" href="qux" />
883 - <link
884 - rel="preload"
885 - as="script"
886 - href="quux"
887 - crossorigin=""
888 - integrity="quux hash"
889 - />
890 - </head>
891 - <body>foo</body>
892 - </html>,
893 - );
894 - });
895 - });
952 + ReactDOMClient.hydrateRoot(
953 + document,
954 + <html>
955 + <body>
956 + <link rel="stylesheet" href="one4" precedence="one" />
957 + <style href="two4" precedence="two">
958 + {css}
959 + </style>
960 + <link rel="stylesheet" href="three4" precedence="three" />
961 + <style href="four4" precedence="four">
962 + {css}
963 + </style>
964 + <link rel="stylesheet" href="seven1" precedence="seven" />
965 + <style href="eight1" precedence="eight">
966 + {css}
967 + </style>
968 + </body>
969 + </html>,
970 + );
971 + expect(Scheduler).toFlushWithoutYielding();
972
897 - describe('ReactDOM.preinit as style', () => {
898 - // @gate enableFloat
899 - it('creates a style Resource when called during server rendering before first flush', async () => {
900 - function Component() {
901 - ReactDOM.preinit('foo', {as: 'style'});
902 - return 'foo';
903 - }
904 - await actIntoEmptyDocument(() => {
905 - const {pipe} = renderToPipeableStream(
906 - <html>
907 - <head />
908 - <body>
909 - <Component />
910 - </body>
911 - </html>,
912 - );
913 - pipe(writable);
914 - });
915 - expect(getMeaningfulChildren(document)).toEqual(
916 - <html>
917 - <head>
918 - <link rel="stylesheet" href="foo" data-precedence="default" />
919 - </head>
920 - <body>foo</body>
921 - </html>,
922 - );
923 - });
973 + expect(getMeaningfulChildren(document)).toEqual(
974 + <html>
975 + <head>
976 + <link rel="stylesheet" href="one1" data-precedence="one" />
977 + <link rel="stylesheet" href="one2" data-precedence="one" />
978 + <style data-href="one3" data-precedence="one">
979 + {css}
980 + </style>
981 + <link rel="stylesheet" href="one4" data-precedence="one" />
982 + <style data-href="two1" data-precedence="two">
983 + {css}
984 + </style>
985 + <link rel="stylesheet" href="two2" data-precedence="two" />
986 + <style data-href="two3" data-precedence="two">
987 + {css}
988 + </style>
989 + <style data-href="two4" data-precedence="two">
990 + {css}
991 + </style>
992 + <link rel="stylesheet" href="three1" data-precedence="three" />
993 + <style data-href="three2" data-precedence="three">
994 + {css}
995 + </style>
996 + <link rel="stylesheet" href="three3" data-precedence="three" />
997 + <link rel="stylesheet" href="three4" data-precedence="three" />
998 + <style data-href="four1" data-precedence="four">
999 + {css}
1000 + </style>
1001 + <style data-href="four2" data-precedence="four">
1002 + {css}
1003 + </style>
1004 + <link rel="stylesheet" href="four3" data-precedence="four" />
1005 + <style data-href="four4" data-precedence="four">
1006 + {css}
1007 + </style>
1008 + <link rel="stylesheet" href="five1" data-precedence="five" />
1009 + <style data-href="six1" data-precedence="six">
1010 + {css}
1011 + </style>
1012 + <link rel="stylesheet" href="seven1" data-precedence="seven" />
1013 + <style data-href="eight1" data-precedence="eight">
1014 + {css}
1015 + </style>
1016 + <link rel="preload" href="one4" as="style" />
1017 + <link rel="preload" href="three4" as="style" />
1018 + <link rel="preload" href="seven1" as="style" />
1019 + </head>
1020 + <body>
1021 + <link rel="preload" href="one2" as="style" />
1022 + <link rel="preload" href="two2" as="style" />
1023 + <link rel="preload" href="five1" as="style" />
1024 + <link rel="preload" href="three3" as="style" />
1025 + <link rel="preload" href="four3" as="style" />
1026 + </body>
1027 + </html>,
1028 + );
1029 + });
1030
925 - // @gate enableFloat
926 - it('creates a preload Resource when called during server rendering after first flush', async () => {
927 - function BlockedOn({text, children}) {
928 - readText(text);
929 - return children;
930 - }
931 - function Component() {
932 - ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
933 - return 'foo';
934 - }
935 - await actIntoEmptyDocument(() => {
936 - const {pipe} = renderToPipeableStream(
937 - <html>
938 - <head />
939 - <body>
940 - <Suspense fallback="loading...">
941 - <BlockedOn text="unblock">
942 - <Component />
943 - </BlockedOn>
944 - </Suspense>
945 - </body>
946 - </html>,
947 - );
948 - pipe(writable);
949 - });
950 - await act(() => {
951 - resolveText('unblock');
952 - });
953 - expect(getMeaningfulChildren(document)).toEqual(
1031 + // @gate enableFloat
1032 + it('client renders a boundary if a style Resource dependency fails to load', async () => {
1033 + function App() {
1034 + return (
1035 <html>
1036 <head />
1037 <body>
957 - foo
958 - <link rel="preload" as="style" href="foo" />
1038 + <Suspense fallback="loading...">
1039 + <BlockedOn value="unblock">
1040 + <link rel="stylesheet" href="foo" precedence="arbitrary" />
1041 + <link rel="stylesheet" href="bar" precedence="arbitrary" />
1042 + Hello
1043 + </BlockedOn>
1044 + </Suspense>
1045 </body>
960 - </html>,
1046 + </html>
1047 );
1048 + }
1049 + await actIntoEmptyDocument(() => {
1050 + const {pipe} = renderToPipeableStream(<App />);
1051 + pipe(writable);
1052 });
1053
964 - // @gate enableFloat
965 - it('inserts a style Resource into the document during render when called during client rendering', async () => {
966 - function Component() {
967 - ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'});
968 - return 'foo';
969 - }
970 - const root = ReactDOMClient.createRoot(container);
971 - root.render(<Component />);
972 - expect(Scheduler).toFlushWithoutYielding();
973 - expect(getMeaningfulChildren(document)).toEqual(
974 - <html>
975 - <head>
976 - <link rel="stylesheet" href="foo" data-precedence="foo" />
977 - </head>
978 - <body>
979 - <div id="container">foo</div>
980 - </body>
981 - </html>,
1054 + await act(() => {
1055 + resolveText('unblock');
1056 + });
1057 +
1058 + expect(getMeaningfulChildren(document)).toEqual(
1059 + <html>
1060 + <head>
1061 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1062 + <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1063 + </head>
1064 + <body>
1065 + loading...
1066 + <link rel="preload" href="foo" as="style" />
1067 + <link rel="preload" href="bar" as="style" />
1068 + </body>
1069 + </html>,
1070 + );
1071 +
1072 + await act(() => {
1073 + const barLink = document.querySelector(
1074 + 'link[rel="stylesheet"][href="bar"]',
1075 );
1076 + const event = document.createEvent('Events');
1077 + event.initEvent('error', true, true);
1078 + barLink.dispatchEvent(event);
1079 });
1080
985 - // @gate enableFloat
986 - it('inserts a preload resource into the document when called in an insertion effect, layout effect, or passive effect', async () => {
987 - function App() {
988 - React.useEffect(() => {
989 - ReactDOM.preinit('passive', {as: 'style', precedence: 'default'});
990 - }, []);
991 - React.useLayoutEffect(() => {
992 - ReactDOM.preinit('layout', {as: 'style', precedence: 'default'});
993 - });
994 - React.useInsertionEffect(() => {
995 - ReactDOM.preinit('insertion', {as: 'style', precedence: 'default'});
996 - });
997 - return 'foobar';
998 - }
999 - const root = ReactDOMClient.createRoot(container);
1000 - root.render(<App />);
1001 - expect(Scheduler).toFlushWithoutYielding();
1081 + const boundaryTemplateInstance = document.getElementById('B:0');
1082 + const suspenseInstance = boundaryTemplateInstance.previousSibling;
1083
1003 - expect(getMeaningfulChildren(document)).toEqual(
1084 + expect(suspenseInstance.data).toEqual('$!');
1085 + expect(boundaryTemplateInstance.dataset.dgst).toBe(
1086 + 'Resource failed to load',
1087 + );
1088 +
1089 + expect(getMeaningfulChildren(document)).toEqual(
1090 + <html>
1091 + <head>
1092 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1093 + <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1094 + </head>
1095 + <body>
1096 + loading...
1097 + <link rel="preload" href="foo" as="style" />
1098 + <link rel="preload" href="bar" as="style" />
1099 + </body>
1100 + </html>,
1101 + );
1102 +
1103 + const errors = [];
1104 + ReactDOMClient.hydrateRoot(document, <App />, {
1105 + onRecoverableError(err, errInfo) {
1106 + errors.push(err.message);
1107 + errors.push(err.digest);
1108 + },
1109 + });
1110 + expect(Scheduler).toFlushWithoutYielding();
1111 + expect(getMeaningfulChildren(document)).toEqual(
1112 + <html>
1113 + <head>
1114 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1115 + <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
1116 + </head>
1117 + <body>
1118 + <link rel="preload" href="foo" as="style" />
1119 + <link rel="preload" href="bar" as="style" />
1120 + Hello
1121 + </body>
1122 + </html>,
1123 + );
1124 + expect(errors).toEqual([
1125 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1126 + 'Resource failed to load',
1127 + ]);
1128 + });
1129 +
1130 + // @gate enableFloat
1131 + it('treats stylesheet links with a precedence as a resource', async () => {
1132 + await actIntoEmptyDocument(() => {
1133 + const {pipe} = renderToPipeableStream(
1134 <html>
1005 - <head>
1006 - <link rel="preload" as="style" href="insertion" />
1007 - <link rel="preload" as="style" href="layout" />
1008 - <link rel="preload" as="style" href="passive" />
1009 - </head>
1135 + <head />
1136 <body>
1011 - <div id="container">foobar</div>
1137 + <link rel="stylesheet" href="foo" precedence="arbitrary" />
1138 + Hello
1139 </body>
1140 </html>,
1141 );
1142 + pipe(writable);
1143 });
1144 + expect(getMeaningfulChildren(document)).toEqual(
1145 + <html>
1146 + <head>
1147 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1148 + </head>
1149 + <body>Hello</body>
1150 + </html>,
1151 + );
1152
1017 - // @gate enableFloat
1018 - it('inserts a preload resource when called in module scope', async () => {
1019 - // The requirement that a root be created has to do with bootstrapping the dispatcher.
1020 - // We are intentionally avoiding setting it to the default via import due to cycles and
1021 - // we are trying to avoid doing a mutable initailation in module scope.
1022 - ReactDOM.preinit('foo', {as: 'style'});
1023 - ReactDOMClient.hydrateRoot(container, null);
1024 - ReactDOM.preinit('bar', {as: 'style'});
1025 - // We need to use global.document because preload falls back
1026 - // to the window.document global when no other documents have been used
1027 - // The way the JSDOM runtim is created for these tests the local document
1028 - // global does not point to the global.document
1029 - expect(getMeaningfulChildren(global.document)).toEqual(
1030 - <html>
1031 - <head>
1032 - <link rel="preload" as="style" href="bar" />
1033 - </head>
1034 - <body />
1035 - </html>,
1036 - );
1037 - });
1153 + ReactDOMClient.hydrateRoot(
1154 + document,
1155 + <html>
1156 + <head />
1157 + <body>Hello</body>
1158 + </html>,
1159 + );
1160 + expect(Scheduler).toFlushWithoutYielding();
1161 + expect(getMeaningfulChildren(document)).toEqual(
1162 + <html>
1163 + <head>
1164 + <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
1165 + </head>
1166 + <body>Hello</body>
1167 + </html>,
1168 + );
1169 });
1170
1040 - describe('ReactDOM.preinit as script', () => {
1041 - // @gate enableFloat
1042 - it('can preinit a script', async () => {
1043 - function App({srcs}) {
1044 - srcs.forEach(src => ReactDOM.preinit(src, {as: 'script'}));
1045 - return (
1046 - <html>
1047 - <head>
1048 - <title>title</title>
1049 - </head>
1050 - <body>foo</body>
1051 - </html>
1052 - );
1053 - }
1054 - await actIntoEmptyDocument(() => {
1055 - const {pipe} = renderToPipeableStream(
1056 - <App srcs={['server', 'shared']} />,
1057 - );
1058 - pipe(writable);
1059 - });
1060 - expect(getMeaningfulChildren(document)).toEqual(
1061 - <html>
1062 - <head>
1063 - <script src="server" async="" />
1064 - <script src="shared" async="" />
1065 - <title>title</title>
1066 - </head>
1067 - <body>foo</body>
1068 - </html>,
1171 + // @gate enableFloat
1172 + it('inserts text separators following text when followed by an element that is converted to a resource and thus removed from the html inline', async () => {
1173 + // If you render many of these as siblings the values get emitted as a single text with no separator sometimes
1174 + // because the link gets elided as a resource
1175 + function AsyncTextWithResource({text, href, precedence}) {
1176 + const value = readText(text);
1177 + return (
1178 + <>
1179 + {value}
1180 + <link rel="stylesheet" href={href} precedence={precedence} />
1181 + </>
1182 );
1183 + }
1184
1071 - ReactDOMClient.hydrateRoot(document, <App srcs={['client', 'shared']} />);
1072 - expect(Scheduler).toFlushWithoutYielding();
1073 - expect(getMeaningfulChildren(document)).toEqual(
1185 + await actIntoEmptyDocument(() => {
1186 + const {pipe} = renderToPipeableStream(
1187 <html>
1075 - <head>
1076 - <script src="server" async="" />
1077 - <script src="shared" async="" />
1078 - <title>title</title>
1079 - <script src="client" async="" />
1080 - </head>
1081 - <body>foo</body>
1188 + <head />
1189 + <body>
1190 + <AsyncTextWithResource text="foo" href="foo" precedence="one" />
1191 + <AsyncTextWithResource text="bar" href="bar" precedence="two" />
1192 + <AsyncTextWithResource text="baz" href="baz" precedence="three" />
1193 + </body>
1194 </html>,
1195 );
1196 + pipe(writable);
1197 + resolveText('foo');
1198 + resolveText('bar');
1199 + resolveText('baz');
1200 });
1201 +
1202 + expect(getMeaningfulChildren(document)).toEqual(
1203 + <html>
1204 + <head>
1205 + <link rel="stylesheet" href="foo" data-precedence="one" />
1206 + <link rel="stylesheet" href="bar" data-precedence="two" />
1207 + <link rel="stylesheet" href="baz" data-precedence="three" />
1208 + </head>
1209 + <body>
1210 + {'foo'}
1211 + {'bar'}
1212 + {'baz'}
1213 + </body>
1214 + </html>,
1215 + );
1216 });
1217
1087 - describe('document encapsulation', () => {
1088 - // @gate enableFloat
1089 - it('can support styles inside portals to a shadowRoot', async () => {
1090 - const shadow = document.body.attachShadow({mode: 'open'});
1091 - const root = ReactDOMClient.createRoot(container);
1092 - root.render(
1093 - <>
1094 - <link rel="stylesheet" href="foo" precedence="default" />
1095 - {ReactDOM.createPortal(
1096 - <div>
1097 - <link
1098 - rel="stylesheet"
1099 - href="foo"
1100 - data-extra-prop="foo"
1101 - precedence="different"
1102 - />
1103 - shadow
1104 - </div>,
1105 - shadow,
1106 - )}
1107 - container
1108 - </>,
1109 - );
1110 - expect(Scheduler).toFlushWithoutYielding();
1111 - expect(getMeaningfulChildren(document)).toEqual(
1218 + // @gate enableFloat
1219 + it('hoists late stylesheets the correct precedence', async () => {
1220 + function PresetPrecedence() {
1221 + ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
1222 + }
1223 + await actIntoEmptyDocument(() => {
1224 + const {pipe} = renderToPipeableStream(
1225 <html>
1113 - <head>
1114 - <link rel="stylesheet" href="foo" data-precedence="default" />
1115 - <link rel="preload" href="foo" as="style" />
1116 - </head>
1226 + <head />
1227 <body>
1118 - <div id="container">container</div>
1119 - </body>
1120 - </html>,
1121 - );
1122 - expect(getMeaningfulChildren(shadow)).toEqual([
1123 - <link
1124 - rel="stylesheet"
1125 - href="foo"
1126 - data-precedence="different"
1127 - data-extra-prop="foo"
1128 - />,
1129 - <div>shadow</div>,
1130 - ]);
1131 - });
1132 - // @gate enableFloat
1133 - it('can support styles inside portals to an element in shadowRoots', async () => {
1134 - const template = document.createElement('template');
1135 - template.innerHTML =
1136 - "<div><div id='shadowcontainer1'></div><div id='shadowcontainer2'></div></div>";
1137 - const shadow = document.body.attachShadow({mode: 'open'});
1138 - shadow.appendChild(template.content);
1139 -
1140 - const shadowContainer1 = shadow.getElementById('shadowcontainer1');
1141 - const shadowContainer2 = shadow.getElementById('shadowcontainer2');
1142 - const root = ReactDOMClient.createRoot(container);
1143 - root.render(
1144 - <>
1145 - <link rel="stylesheet" href="foo" precedence="default" />
1146 - {ReactDOM.createPortal(
1228 + <link rel="stylesheet" href="initial" precedence="one" />
1229 + <PresetPrecedence />
1230 <div>
1148 - <link rel="stylesheet" href="foo" precedence="one" />
1149 - <link rel="stylesheet" href="bar" precedence="two" />1
1150 - </div>,
1151 - shadow,
1152 - )}
1153 - {ReactDOM.createPortal(
1231 + <Suspense fallback="loading foo bar...">
1232 + <div>foo</div>
1233 + <link rel="stylesheet" href="foo" precedence="one" />
1234 + <BlockedOn value="bar">
1235 + <div>bar</div>
1236 + <link rel="stylesheet" href="bar" precedence="default" />
1237 + </BlockedOn>
1238 + </Suspense>
1239 + </div>
1240 <div>
1155 - <link rel="stylesheet" href="foo" precedence="one" />
1156 - <link rel="stylesheet" href="baz" precedence="one" />2
1157 - </div>,
1158 - shadowContainer1,
1159 - )}
1160 - {ReactDOM.createPortal(
1241 + <Suspense fallback="loading bar baz qux...">
1242 + <BlockedOn value="bar">
1243 + <div>bar</div>
1244 + <link rel="stylesheet" href="bar" precedence="default" />
1245 + </BlockedOn>
1246 + <BlockedOn value="baz">
1247 + <div>baz</div>
1248 + <link rel="stylesheet" href="baz" precedence="two" />
1249 + </BlockedOn>
1250 + <BlockedOn value="qux">
1251 + <div>qux</div>
1252 + <link rel="stylesheet" href="qux" precedence="one" />
1253 + </BlockedOn>
1254 + </Suspense>
1255 + </div>
1256 <div>
1162 - <link rel="stylesheet" href="bar" precedence="two" />
1163 - <link rel="stylesheet" href="qux" precedence="three" />3
1164 - </div>,
1165 - shadowContainer2,
1166 - )}
1167 - container
1168 - </>,
1169 - );
1170 - expect(Scheduler).toFlushWithoutYielding();
1171 - expect(getMeaningfulChildren(document)).toEqual(
1172 - <html>
1173 - <head>
1174 - <link rel="stylesheet" href="foo" data-precedence="default" />
1175 - <link rel="preload" href="foo" as="style" />
1176 - <link rel="preload" href="bar" as="style" />
1177 - <link rel="preload" href="baz" as="style" />
1178 - <link rel="preload" href="qux" as="style" />
1179 - </head>
1180 - <body>
1181 - <div id="container">container</div>
1257 + <Suspense fallback="loading bar baz qux...">
1258 + <BlockedOn value="unblock">
1259 + <BlockedOn value="bar">
1260 + <div>bar</div>
1261 + <link rel="stylesheet" href="bar" precedence="default" />
1262 + </BlockedOn>
1263 + <BlockedOn value="baz">
1264 + <div>baz</div>
1265 + <link rel="stylesheet" href="baz" precedence="two" />
1266 + </BlockedOn>
1267 + <BlockedOn value="qux">
1268 + <div>qux</div>
1269 + <link rel="stylesheet" href="qux" precedence="one" />
1270 + </BlockedOn>
1271 + </BlockedOn>
1272 + </Suspense>
1273 + </div>
1274 </body>
1275 </html>,
1276 );
1185 - expect(getMeaningfulChildren(shadow)).toEqual([
1186 - <link rel="stylesheet" href="foo" data-precedence="one" />,
1187 - <link rel="stylesheet" href="baz" data-precedence="one" />,
1188 - <link rel="stylesheet" href="bar" data-precedence="two" />,
1189 - <link rel="stylesheet" href="qux" data-precedence="three" />,
1190 - <div>
1191 - <div id="shadowcontainer1">
1192 - <div>2</div>
1193 - </div>
1194 - <div id="shadowcontainer2">
1195 - <div>3</div>
1196 - </div>
1197 - </div>,
1198 - <div>1</div>,
1199 - ]);
1277 + pipe(writable);
1278 });
1201 - });
1202 -
1203 - describe('head resources', () => {
1204 - // @gate enableFloat
1205 - it('supports preconnects, prefetc-dns, and arbitrary other link types', async () => {
1206 - await actIntoEmptyDocument(() => {
1207 - const {pipe} = renderToPipeableStream(
1208 - <html>
1209 - <head />
1210 - <body>
1211 - <div>hello world</div>
1212 - </body>
1213 - <link rel="foo" href="bar" />
1214 - <link rel="preload" href="bar" />
1215 - <link rel="preload" href="bar" as="style" />
1216 - <link rel="stylesheet" href="bar" precedence="default" />
1217 - <link rel="preconnect" href="bar" />
1218 - <link rel="dns-prefetch" href="bar" />
1219 - <link rel="icon" href="bar" />
1220 - <link rel="icon" href="bar" sizes="1x1" />
1221 - <link rel="icon" href="bar" media="foo" />
1222 - <link rel="shortcut icon" href="bar" />
1223 - <link rel="apple-touch-icon" href="bar" />
1224 - </html>,
1225 - );
1226 - pipe(writable);
1227 - });
1228 - // "preconnect" and "dns-prefetch" get hoisted to the front.
1229 - // All other generic links (not styles, or typed preloads)
1230 - // get emitted after styles and other higher priority Resources
1231 - // Sizes and Media are part of generic link keys
1232 - expect(getMeaningfulChildren(document)).toEqual(
1233 - <html>
1234 - <head>
1235 - <link rel="preconnect" href="bar" />
1236 - <link rel="dns-prefetch" href="bar" />
1237 - <link rel="stylesheet" href="bar" data-precedence="default" />
1238 - <link rel="foo" href="bar" />
1239 - <link rel="preload" href="bar" />
1240 - <link rel="icon" href="bar" />
1241 - <link rel="icon" href="bar" sizes="1x1" />
1242 - <link rel="icon" href="bar" media="foo" />
1243 - <link rel="shortcut icon" href="bar" />
1244 - <link rel="apple-touch-icon" href="bar" />
1245 - </head>
1246 - <body>
1247 - <div>hello world</div>
1248 - </body>
1249 - </html>,
1250 - );
1279
1252 - const root = ReactDOMClient.hydrateRoot(
1253 - document,
1254 - <html>
1255 - <head />
1256 - <body>
1257 - <div>hello world</div>
1258 - </body>
1259 - <link rel="foo" href="bar" />
1260 - <link rel="preload" href="bar" />
1261 - <link rel="preload" href="bar" as="style" />
1262 - <link rel="stylesheet" href="bar" precedence="default" />
1263 - <link rel="preconnect" href="bar" />
1264 - <link rel="dns-prefetch" href="bar" />
1265 - <link rel="icon" href="bar" />
1266 - <link rel="icon" href="bar" sizes="1x1" />
1267 - <link rel="icon" href="bar" media="foo" />
1268 - <link rel="shortcut icon" href="bar" />
1269 - <link rel="apple-touch-icon" href="bar" />
1270 - </html>,
1271 - );
1272 - expect(Scheduler).toFlushWithoutYielding();
1273 - expect(getMeaningfulChildren(document)).toEqual(
1274 - <html>
1275 - <head>
1276 - <link rel="preconnect" href="bar" />
1277 - <link rel="dns-prefetch" href="bar" />
1278 - <link rel="stylesheet" href="bar" data-precedence="default" />
1279 - <link rel="foo" href="bar" />
1280 - <link rel="preload" href="bar" />
1281 - <link rel="icon" href="bar" />
1282 - <link rel="icon" href="bar" sizes="1x1" />
1283 - <link rel="icon" href="bar" media="foo" />
1284 - <link rel="shortcut icon" href="bar" />
1285 - <link rel="apple-touch-icon" href="bar" />
1286 - </head>
1287 - <body>
1288 - <div>hello world</div>
1289 - </body>
1290 - </html>,
1291 - );
1292 -
1293 - root.render(
1294 - <html>
1295 - <head />
1296 - <body>
1297 - <div>hello world</div>
1298 - </body>
1299 - </html>,
1300 - );
1301 - expect(Scheduler).toFlushWithoutYielding();
1302 - expect(getMeaningfulChildren(document)).toEqual(
1303 - <html>
1304 - <head>
1305 - <link rel="stylesheet" href="bar" data-precedence="default" />
1306 - <link rel="preload" href="bar" />
1307 - </head>
1308 - <body>
1309 - <div>hello world</div>
1310 - </body>
1311 - </html>,
1312 - );
1313 - });
1314 -
1315 - // @gate enableFloat
1316 - it('can render <base> as a Resource', async () => {
1317 - await actIntoEmptyDocument(() => {
1318 - const {pipe} = renderToPipeableStream(
1319 - <html>
1320 - <head />
1321 - <body>
1322 - <base target="_blank" />
1323 - <base href="foo" />
1324 - <base target="_self" href="bar" />
1325 - <div>hello world</div>
1326 - </body>
1327 - </html>,
1328 - );
1329 - pipe(writable);
1330 - });
1331 - expect(getMeaningfulChildren(document)).toEqual(
1332 - <html>
1333 - <head>
1334 - <base target="_blank" />
1335 - <base href="foo" />
1336 - <base target="_self" href="bar" />
1337 - </head>
1338 - <body>
1339 - <div>hello world</div>
1340 - </body>
1341 - </html>,
1342 - );
1343 -
1344 - ReactDOMClient.hydrateRoot(
1345 - document,
1346 - <html>
1347 - <head />
1348 - <body>
1349 - <base target="_blank" />
1350 - <base href="foo" />
1351 - <base target="_self" href="bar" />
1352 - <base target="_top" href="baz" />
1353 - <div>hello world</div>
1354 - </body>
1355 - </html>,
1356 - );
1357 - expect(Scheduler).toFlushWithoutYielding();
1358 - expect(getMeaningfulChildren(document)).toEqual(
1359 - <html>
1360 - <head>
1361 - <base target="_top" href="baz" />
1362 - <base target="_blank" />
1363 - <base href="foo" />
1364 - <base target="_self" href="bar" />
1365 - </head>
1366 - <body>
1367 - <div>hello world</div>
1368 - </body>
1369 - </html>,
1370 - );
1371 - });
1372 -
1373 - // @gate enableFloat
1374 - it('can render icons and apple-touch-icons as Resources', async () => {
1375 - await actIntoEmptyDocument(() => {
1376 - const {pipe} = renderToPipeableStream(
1377 - <>
1378 - <html>
1379 - <head />
1380 - <body>
1381 - <link rel="icon" href="foo" />
1382 - <div>hello world</div>
1383 - </body>
1384 - </html>
1385 - <link rel="apple-touch-icon" href="foo" />
1386 - </>,
1387 - );
1388 - pipe(writable);
1389 - });
1390 - expect(getMeaningfulChildren(document)).toEqual(
1391 - <html>
1392 - <head>
1393 - <link rel="icon" href="foo" />
1394 - <link rel="apple-touch-icon" href="foo" />
1395 - </head>
1396 - <body>
1397 - <div>hello world</div>
1398 - </body>
1399 - </html>,
1400 - );
1280 + expect(getMeaningfulChildren(document)).toEqual(
1281 + <html>
1282 + <head>
1283 + <link rel="stylesheet" href="initial" data-precedence="one" />
1284 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1285 + <link rel="preload" href="foo" as="style" />
1286 + </head>
1287 + <body>
1288 + <div>loading foo bar...</div>
1289 + <div>loading bar baz qux...</div>
1290 + <div>loading bar baz qux...</div>
1291 + </body>
1292 + </html>,
1293 + );
1294
1402 - ReactDOMClient.hydrateRoot(
1403 - document,
1404 - <html>
1405 - <link rel="apple-touch-icon" href="foo" />
1406 - <head />
1407 - <body>
1408 - <link rel="icon" href="foo" />
1409 - <div>hello world</div>
1410 - </body>
1411 - </html>,
1412 - );
1413 - expect(Scheduler).toFlushWithoutYielding();
1414 - expect(getMeaningfulChildren(document)).toEqual(
1415 - <html>
1416 - <head>
1417 - <link rel="icon" href="foo" />
1418 - <link rel="apple-touch-icon" href="foo" />
1419 - </head>
1420 - <body>
1421 - <div>hello world</div>
1422 - </body>
1423 - </html>,
1424 - );
1295 + await act(() => {
1296 + resolveText('foo');
1297 + resolveText('bar');
1298 });
1299
1427 - // @gate enableFloat
1428 - it('can hydrate the right instances for deeply nested structured metas', async () => {
1429 - await actIntoEmptyDocument(() => {
1430 - const {pipe} = renderToPipeableStream(
1431 - <>
1432 - <html>
1433 - <head />
1434 - <body>
1435 - <div>hello world</div>
1436 - </body>
1437 - </html>
1438 - <meta property="og:foo" content="one" />
1439 - <meta property="og:foo:bar" content="bar" />
1440 - <meta property="og:foo:bar:baz" content="baz" />
1441 - <meta property="og:foo" content="two" />
1442 - <meta property="og:foo:bar" content="bar" />
1443 - <meta property="og:foo:bar:baz" content="baz" />
1444 - </>,
1445 - );
1446 - pipe(writable);
1447 - });
1448 - expect(getMeaningfulChildren(document)).toEqual(
1449 - <html>
1450 - <head>
1451 - <meta property="og:foo" content="one" />
1452 - <meta property="og:foo:bar" content="bar" />
1453 - <meta property="og:foo:bar:baz" content="baz" />
1454 - <meta property="og:foo" content="two" />
1455 - <meta property="og:foo:bar" content="bar" />
1456 - <meta property="og:foo:bar:baz" content="baz" />
1457 - </head>
1458 - <body>
1459 - <div>hello world</div>
1460 - </body>
1461 - </html>,
1462 - );
1463 -
1464 - const root = ReactDOMClient.hydrateRoot(
1465 - document,
1466 - <>
1467 - <html>
1468 - <head />
1469 - <body>
1470 - <div>hello world</div>
1471 - </body>
1472 - </html>
1473 - <meta property="og:foo" content="one" />
1474 - <meta property="og:foo:bar" content="bar" />
1475 - <meta property="og:foo:bar:baz" content="baz" />
1476 - <meta property="og:foo" content="two" />
1477 - <meta property="og:foo:bar" content="bar" />
1478 - <meta property="og:foo:bar:baz" content="baz" />
1479 - </>,
1480 - );
1481 - expect(Scheduler).toFlushWithoutYielding();
1482 - expect(getMeaningfulChildren(document)).toEqual(
1483 - <html>
1484 - <head>
1485 - <meta property="og:foo" content="one" />
1486 - <meta property="og:foo:bar" content="bar" />
1487 - <meta property="og:foo:bar:baz" content="baz" />
1488 - <meta property="og:foo" content="two" />
1489 - <meta property="og:foo:bar" content="bar" />
1490 - <meta property="og:foo:bar:baz" content="baz" />
1491 - </head>
1492 - <body>
1493 - <div>hello world</div>
1494 - </body>
1495 - </html>,
1496 - );
1300 + expect(getMeaningfulChildren(document)).toEqual(
1301 + <html>
1302 + <head>
1303 + <link rel="stylesheet" href="initial" data-precedence="one" />
1304 + <link rel="stylesheet" href="foo" data-precedence="one" />
1305 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1306 + <link rel="stylesheet" href="bar" data-precedence="default" />
1307 + <link rel="preload" href="foo" as="style" />
1308 + </head>
1309 + <body>
1310 + <div>loading foo bar...</div>
1311 + <div>loading bar baz qux...</div>
1312 + <div>loading bar baz qux...</div>
1313 + <link rel="preload" href="bar" as="style" />
1314 + </body>
1315 + </html>,
1316 + );
1317
1498 - root.render(
1499 - <>
1500 - <html>
1501 - <head />
1502 - <body>
1503 - <div>hello world</div>
1504 - </body>
1505 - </html>
1506 - </>,
1507 - );
1508 - expect(Scheduler).toFlushWithoutYielding();
1509 - expect(getMeaningfulChildren(document)).toEqual(
1510 - <html>
1511 - <head />
1512 - <body>
1513 - <div>hello world</div>
1514 - </body>
1515 - </html>,
1516 - );
1318 + await act(() => {
1319 + const link = document.querySelector('link[rel="stylesheet"][href="foo"]');
1320 + const event = document.createEvent('Events');
1321 + event.initEvent('load', true, true);
1322 + link.dispatchEvent(event);
1323 });
1324
1519 - // @gate enableFloat
1520 - it('can insert meta tags in the expected location', async () => {
1521 - await actIntoEmptyDocument(() => {
1522 - const {pipe} = renderToPipeableStream(
1523 - <>
1524 - <html>
1525 - <head />
1526 - <body>
1527 - <div>hello world</div>
1528 - </body>
1529 - </html>
1530 - <meta charSet="utf-8" />
1531 - <meta name="google-site-verification" content="somehash1" />
1532 - <meta name="google-site-verification" content="somehash2" />
1533 - <meta
1534 - name="description"
1535 - property="og:description"
1536 - content="my site"
1537 - />
1538 - <meta property="og:image" content="foo" />
1539 - <meta property="og:image:width" content="100" />
1540 - <meta httpEquiv="refresh" content="dont actually" />
1541 - <meta property="og:image:height" content="100" />
1542 - <meta property="og:image" content="bar" />
1543 - <meta property="og:image:width" content="100" />
1544 - <meta itemProp="someprop" content="somevalue" />
1545 - <meta property="og:image:height" content="100" />
1546 - <meta property="og:description:foo" content="foo" />
1547 - </>,
1548 - );
1549 - pipe(writable);
1550 - });
1551 - expect(getMeaningfulChildren(document)).toEqual(
1552 - <html>
1553 - <head>
1554 - <meta charset="utf-8" />
1555 - <meta name="google-site-verification" content="somehash1" />
1556 - <meta name="google-site-verification" content="somehash2" />
1557 - <meta
1558 - name="description"
1559 - property="og:description"
1560 - content="my site"
1561 - />
1562 - <meta property="og:image" content="foo" />
1563 - <meta property="og:image:width" content="100" />
1564 - <meta http-equiv="refresh" content="dont actually" />
1565 - <meta property="og:image:height" content="100" />
1566 - <meta property="og:image" content="bar" />
1567 - <meta property="og:image:width" content="100" />
1568 - <meta itemprop="someprop" content="somevalue" />
1569 - <meta property="og:image:height" content="100" />
1570 - <meta property="og:description:foo" content="foo" />
1571 - </head>
1572 - <body>
1573 - <div>hello world</div>
1574 - </body>
1575 - </html>,
1576 - );
1577 -
1578 - const root = ReactDOMClient.hydrateRoot(
1579 - document,
1580 - <html>
1581 - <head>
1582 - <meta property="og:image" content="bar" />
1583 - <meta property="og:image:width" content="100" />
1584 - <meta property="og:image:height" content="100" />
1585 - </head>
1586 - <body>
1587 - <div>hello world</div>
1588 - </body>
1589 - </html>,
1590 - );
1591 - expect(Scheduler).toFlushWithoutYielding();
1592 - expect(getMeaningfulChildren(document)).toEqual(
1593 - <html>
1594 - <head>
1595 - <meta charset="utf-8" />
1596 - <meta name="google-site-verification" content="somehash1" />
1597 - <meta name="google-site-verification" content="somehash2" />
1598 - <meta
1599 - name="description"
1600 - property="og:description"
1601 - content="my site"
1602 - />
1603 - <meta property="og:image" content="foo" />
1604 - <meta property="og:image:width" content="100" />
1605 - <meta http-equiv="refresh" content="dont actually" />
1606 - <meta property="og:image:height" content="100" />
1607 - <meta property="og:image" content="bar" />
1608 - <meta property="og:image:width" content="100" />
1609 - <meta itemprop="someprop" content="somevalue" />
1610 - <meta property="og:image:height" content="100" />
1611 - <meta property="og:description:foo" content="foo" />
1612 - </head>
1613 - <body>
1614 - <div>hello world</div>
1615 - </body>
1616 - </html>,
1617 - );
1325 + expect(getMeaningfulChildren(document)).toEqual(
1326 + <html>
1327 + <head>
1328 + <link rel="stylesheet" href="initial" data-precedence="one" />
1329 + <link rel="stylesheet" href="foo" data-precedence="one" />
1330 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1331 + <link rel="stylesheet" href="bar" data-precedence="default" />
1332 + <link rel="preload" href="foo" as="style" />
1333 + </head>
1334 + <body>
1335 + <div>loading foo bar...</div>
1336 + <div>loading bar baz qux...</div>
1337 + <div>loading bar baz qux...</div>
1338 + <link rel="preload" href="bar" as="style" />
1339 + </body>
1340 + </html>,
1341 + );
1342
1619 - root.render(
1620 - <html>
1621 - <head>
1622 - <meta property="og:image" content="bar" />
1623 - <meta property="og:image:width" content="100" />
1624 - <meta property="og:image:height" content="100" />
1625 - <meta property="og:description" content="my site" />
1626 - <meta
1627 - itemProp="description bar"
1628 - property="og:description:bar"
1629 - content="bar"
1630 - />
1631 - </head>
1632 - <body>
1633 - <div>hello world</div>
1634 - </body>
1635 - </html>,
1636 - );
1637 - expect(Scheduler).toFlushWithoutYielding();
1638 - expect(getMeaningfulChildren(document)).toEqual(
1639 - <html>
1640 - <head>
1641 - <meta charset="utf-8" />
1642 - <meta name="google-site-verification" content="somehash1" />
1643 - <meta name="google-site-verification" content="somehash2" />
1644 - <meta
1645 - name="description"
1646 - property="og:description"
1647 - content="my site"
1648 - />
1649 - <meta
1650 - itemprop="description bar"
1651 - property="og:description:bar"
1652 - content="bar"
1653 - />
1654 - <meta property="og:image" content="foo" />
1655 - <meta property="og:image:width" content="100" />
1656 - <meta http-equiv="refresh" content="dont actually" />
1657 - <meta property="og:image:height" content="100" />
1658 - <meta property="og:image" content="bar" />
1659 - <meta property="og:image:width" content="100" />
1660 - <meta itemprop="someprop" content="somevalue" />
1661 - <meta property="og:image:height" content="100" />
1662 - <meta property="og:description:foo" content="foo" />
1663 - </head>
1664 - <body>
1665 - <div>hello world</div>
1666 - </body>
1667 - </html>,
1668 - );
1343 + await act(() => {
1344 + const link = document.querySelector('link[rel="stylesheet"][href="bar"]');
1345 + const event = document.createEvent('Events');
1346 + event.initEvent('load', true, true);
1347 + link.dispatchEvent(event);
1348 });
1349
1671 - // @gate enableFloat
1672 - it('can render meta tags with og properties with structured data', async () => {
1673 - await actIntoEmptyDocument(() => {
1674 - const {pipe} = renderToPipeableStream(
1675 - <>
1676 - <html>
1677 - <head />
1678 - <body>
1679 - <div>hello world</div>
1680 - </body>
1681 - </html>
1682 - <meta property="og:image" content="foo" />
1683 - <meta property="og:image:width" content="100" />
1684 - <meta property="og:image:height" content="100" />
1685 - <meta property="og:image" content="bar" />
1686 - <meta property="og:image:width" content="100" />
1687 - <meta property="og:image:height" content="100" />
1688 - </>,
1689 - );
1690 - pipe(writable);
1691 - });
1692 - expect(getMeaningfulChildren(document)).toEqual(
1693 - <html>
1694 - <head>
1695 - <meta property="og:image" content="foo" />
1696 - <meta property="og:image:width" content="100" />
1697 - <meta property="og:image:height" content="100" />
1698 - <meta property="og:image" content="bar" />
1699 - <meta property="og:image:width" content="100" />
1700 - <meta property="og:image:height" content="100" />
1701 - </head>
1702 - <body>
1703 - <div>hello world</div>
1704 - </body>
1705 - </html>,
1706 - );
1350 + expect(getMeaningfulChildren(document)).toEqual(
1351 + <html>
1352 + <head>
1353 + <link rel="stylesheet" href="initial" data-precedence="one" />
1354 + <link rel="stylesheet" href="foo" data-precedence="one" />
1355 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1356 + <link rel="stylesheet" href="bar" data-precedence="default" />
1357 + <link rel="preload" href="foo" as="style" />
1358 + </head>
1359 + <body>
1360 + <div>
1361 + <div>foo</div>
1362 + <div>bar</div>
1363 + </div>
1364 + <div>loading bar baz qux...</div>
1365 + <div>loading bar baz qux...</div>
1366 + <link rel="preload" href="bar" as="style" />
1367 + </body>
1368 + </html>,
1369 + );
1370
1708 - const root = ReactDOMClient.hydrateRoot(
1709 - document,
1710 - <html>
1711 - <head />
1712 - <body>
1713 - <meta property="og:image" content="foo" />
1714 - <meta property="og:image:width" content="100" />
1715 - <meta property="og:image:height" content="100" />
1716 - <meta property="og:image" content="bar" />
1717 - <meta property="og:image:width" content="100" />
1718 - <meta property="og:image:height" content="100" />
1719 - <div>hello world</div>
1720 - </body>
1721 - </html>,
1722 - );
1723 - expect(Scheduler).toFlushWithoutYielding();
1724 - expect(getMeaningfulChildren(document)).toEqual(
1725 - <html>
1726 - <head>
1727 - <meta property="og:image" content="foo" />
1728 - <meta property="og:image:width" content="100" />
1729 - <meta property="og:image:height" content="100" />
1730 - <meta property="og:image" content="bar" />
1731 - <meta property="og:image:width" content="100" />
1732 - <meta property="og:image:height" content="100" />
1733 - </head>
1734 - <body>
1735 - <div>hello world</div>
1736 - </body>
1737 - </html>,
1738 - );
1371 + await act(() => {
1372 + resolveText('baz');
1373 + });
1374
1740 - root.render(
1741 - <html>
1742 - <head />
1743 - <body>
1744 - <meta property="og:image" content="foo" />
1745 - <meta property="og:image:width" content="100" />
1746 - <meta property="og:image:height" content="100" />
1747 - <meta property="og:image" content="bar" />
1748 - <meta property="og:image:height" content="100" />
1749 - <div>hello world</div>
1750 - </body>
1751 - </html>,
1752 - );
1753 - expect(Scheduler).toFlushWithoutYielding();
1754 - expect(getMeaningfulChildren(document)).toEqual(
1755 - <html>
1756 - <head>
1757 - <meta property="og:image" content="foo" />
1758 - <meta property="og:image:width" content="100" />
1759 - <meta property="og:image:height" content="100" />
1760 - <meta property="og:image" content="bar" />
1761 - <meta property="og:image:height" content="100" />
1762 - </head>
1763 - <body>
1764 - <div>hello world</div>
1765 - </body>
1766 - </html>,
1767 - );
1375 + expect(getMeaningfulChildren(document)).toEqual(
1376 + <html>
1377 + <head>
1378 + <link rel="stylesheet" href="initial" data-precedence="one" />
1379 + <link rel="stylesheet" href="foo" data-precedence="one" />
1380 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1381 + <link rel="stylesheet" href="bar" data-precedence="default" />
1382 + <link rel="preload" href="foo" as="style" />
1383 + </head>
1384 + <body>
1385 + <div>
1386 + <div>foo</div>
1387 + <div>bar</div>
1388 + </div>
1389 + <div>loading bar baz qux...</div>
1390 + <div>loading bar baz qux...</div>
1391 + <link rel="preload" as="style" href="bar" />
1392 + <link rel="preload" as="style" href="baz" />
1393 + </body>
1394 + </html>,
1395 + );
1396
1769 - root.render(
1770 - <html>
1771 - <head />
1772 - <body>
1773 - <meta property="og:image" content="foo" />
1774 - <meta property="og:image:width" content="100" />
1775 - <meta property="og:image:height" content="100" />
1776 - <meta property="og:image:foo" content="foo" />
1777 - <meta property="og:image" content="bar" />
1778 - <meta property="og:image:height" content="100" />
1779 - <div>hello world</div>
1780 - </body>
1781 - </html>,
1782 - );
1783 - expect(Scheduler).toFlushWithoutYielding();
1784 - expect(getMeaningfulChildren(document)).toEqual(
1785 - <html>
1786 - <head>
1787 - <meta property="og:image" content="foo" />
1788 - <meta property="og:image:foo" content="foo" />
1789 - <meta property="og:image:width" content="100" />
1790 - <meta property="og:image:height" content="100" />
1791 - <meta property="og:image" content="bar" />
1792 - <meta property="og:image:height" content="100" />
1793 - </head>
1794 - <body>
1795 - <div>hello world</div>
1796 - </body>
1797 - </html>,
1798 - );
1397 + await act(() => {
1398 + resolveText('qux');
1399 + });
1400
1800 - root.render(
1801 - <html>
1802 - <head />
1803 - <body>
1804 - <meta property="og:image" content="foo" />
1805 - <meta property="og:image:width" content="100" />
1806 - <meta property="og:image:width:bar" content="bar" />
1807 - <meta property="og:image:height" content="100" />
1808 - <meta property="og:image:foo" content="foo" />
1809 - <meta property="og:image" content="bar" />
1810 - <meta property="og:image:height" content="100" />
1811 - <div>hello world</div>
1812 - </body>
1813 - </html>,
1401 + expect(getMeaningfulChildren(document)).toEqual(
1402 + <html>
1403 + <head>
1404 + <link rel="stylesheet" href="initial" data-precedence="one" />
1405 + <link rel="stylesheet" href="foo" data-precedence="one" />
1406 + <link rel="stylesheet" href="qux" data-precedence="one" />
1407 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1408 + <link rel="stylesheet" href="bar" data-precedence="default" />
1409 + <link rel="stylesheet" href="baz" data-precedence="two" />
1410 + <link rel="preload" href="foo" as="style" />
1411 + </head>
1412 + <body>
1413 + <div>
1414 + <div>foo</div>
1415 + <div>bar</div>
1416 + </div>
1417 + <div>loading bar baz qux...</div>
1418 + <div>loading bar baz qux...</div>
1419 + <link rel="preload" as="style" href="bar" />
1420 + <link rel="preload" as="style" href="baz" />
1421 + <link rel="preload" as="style" href="qux" />
1422 + </body>
1423 + </html>,
1424 + );
1425 +
1426 + await act(() => {
1427 + const bazlink = document.querySelector(
1428 + 'link[rel="stylesheet"][href="baz"]',
1429 );
1815 - expect(Scheduler).toFlushWithoutYielding();
1816 - expect(getMeaningfulChildren(document)).toEqual(
1817 - <html>
1818 - <head>
1819 - <meta property="og:image" content="foo" />
1820 - <meta property="og:image:foo" content="foo" />
1821 - <meta property="og:image:height" content="100" />
1822 - <meta property="og:image:width" content="100" />
1823 - <meta property="og:image:width:bar" content="bar" />
1824 - <meta property="og:image" content="bar" />
1825 - <meta property="og:image:height" content="100" />
1826 - </head>
1827 - <body>
1828 - <div>hello world</div>
1829 - </body>
1830 - </html>,
1430 + const quxlink = document.querySelector(
1431 + 'link[rel="stylesheet"][href="qux"]',
1432 );
1433 + const presetLink = document.querySelector(
1434 + 'link[rel="stylesheet"][href="preset"]',
1435 + );
1436 + const event = document.createEvent('Events');
1437 + event.initEvent('load', true, true);
1438 + bazlink.dispatchEvent(event);
1439 + quxlink.dispatchEvent(event);
1440 + presetLink.dispatchEvent(event);
1441 });
1442
1834 - // @gate enableFloat
1835 - it('can render meta tags as resources', async () => {
1836 - await actIntoEmptyDocument(() => {
1837 - const {pipe} = renderToPipeableStream(
1838 - <>
1839 - <html>
1840 - <head />
1841 - <body>
1842 - <div>hello world</div>
1843 - </body>
1844 - </html>
1845 - <meta name="robots" content="noindex" />
1846 - <meta httpEquiv="content-security-policy" content="foo" />
1847 - <meta itemProp="description" content="desc" />
1848 - <meta property="description" content="desc2" />
1849 - <meta charSet="utf-8" />
1850 - </>,
1851 - );
1852 - pipe(writable);
1853 - });
1854 - expect(getMeaningfulChildren(document)).toEqual(
1855 - <html>
1856 - <head>
1857 - <meta charset="utf-8" />
1858 - <meta name="robots" content="noindex" />
1859 - <meta http-equiv="content-security-policy" content="foo" />
1860 - <meta itemprop="description" content="desc" />
1861 - <meta property="description" content="desc2" />
1862 - </head>
1863 - <body>
1864 - <div>hello world</div>
1865 - </body>
1866 - </html>,
1867 - );
1443 + expect(getMeaningfulChildren(document)).toEqual(
1444 + <html>
1445 + <head>
1446 + <link rel="stylesheet" href="initial" data-precedence="one" />
1447 + <link rel="stylesheet" href="foo" data-precedence="one" />
1448 + <link rel="stylesheet" href="qux" data-precedence="one" />
1449 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1450 + <link rel="stylesheet" href="bar" data-precedence="default" />
1451 + <link rel="stylesheet" href="baz" data-precedence="two" />
1452 + <link rel="preload" href="foo" as="style" />
1453 + </head>
1454 + <body>
1455 + <div>
1456 + <div>foo</div>
1457 + <div>bar</div>
1458 + </div>
1459 + <div>
1460 + <div>bar</div>
1461 + <div>baz</div>
1462 + <div>qux</div>
1463 + </div>
1464 + <div>loading bar baz qux...</div>
1465 + <link rel="preload" as="style" href="bar" />
1466 + <link rel="preload" as="style" href="baz" />
1467 + <link rel="preload" as="style" href="qux" />
1468 + </body>
1469 + </html>,
1470 + );
1471
1869 - ReactDOMClient.hydrateRoot(
1870 - document,
1871 - <html>
1872 - <head>
1873 - <meta charSet="utf-8" />
1874 - </head>
1875 - <body>
1876 - <meta name="robots" content="noindex" />
1877 - <meta httpEquiv="content-security-policy" content="foo" />
1878 - <div>hello world</div>
1879 - </body>
1880 - </html>,
1881 - );
1882 - expect(Scheduler).toFlushWithoutYielding();
1883 - expect(getMeaningfulChildren(document)).toEqual(
1884 - <html>
1885 - <head>
1886 - <meta charset="utf-8" />
1887 - <meta name="robots" content="noindex" />
1888 - <meta http-equiv="content-security-policy" content="foo" />
1889 - <meta itemprop="description" content="desc" />
1890 - <meta property="description" content="desc2" />
1891 - </head>
1892 - <body>
1893 - <div>hello world</div>
1894 - </body>
1895 - </html>,
1896 - );
1472 + await act(() => {
1473 + resolveText('unblock');
1474 });
1475
1899 - // @gate enableFloat
1900 - it('can rendering title tags anywhere in the tree', async () => {
1901 - await actIntoEmptyDocument(() => {
1902 - const {pipe} = renderToPipeableStream(
1903 - <>
1904 - <title>before</title>
1905 - <>
1906 - <html>
1907 - <head>
1908 - <title>in head</title>
1909 - </head>
1910 - <body>
1911 - <div>
1912 - <title>during</title>
1913 - hello world
1914 - </div>
1915 - </body>
1916 - </html>
1917 - </>
1918 - <title>after</title>
1919 - </>,
1920 - );
1921 - pipe(writable);
1922 - });
1476 + expect(getMeaningfulChildren(document)).toEqual(
1477 + <html>
1478 + <head>
1479 + <link rel="stylesheet" href="initial" data-precedence="one" />
1480 + <link rel="stylesheet" href="foo" data-precedence="one" />
1481 + <link rel="stylesheet" href="qux" data-precedence="one" />
1482 + <link rel="stylesheet" href="preset" data-precedence="preset" />
1483 + <link rel="stylesheet" href="bar" data-precedence="default" />
1484 + <link rel="stylesheet" href="baz" data-precedence="two" />
1485 + <link rel="preload" href="foo" as="style" />
1486 + </head>
1487 + <body>
1488 + <div>
1489 + <div>foo</div>
1490 + <div>bar</div>
1491 + </div>
1492 + <div>
1493 + <div>bar</div>
1494 + <div>baz</div>
1495 + <div>qux</div>
1496 + </div>
1497 + <div>
1498 + <div>bar</div>
1499 + <div>baz</div>
1500 + <div>qux</div>
1501 + </div>
1502 + <link rel="preload" as="style" href="bar" />
1503 + <link rel="preload" as="style" href="baz" />
1504 + <link rel="preload" as="style" href="qux" />
1505 + </body>
1506 + </html>,
1507 + );
1508 + });
1509
1924 - expect(getMeaningfulChildren(document)).toEqual(
1510 + // @gate enableFloat
1511 + it('normalizes stylesheet resource precedence for all boundaries inlined as part of the shell flush', async () => {
1512 + await actIntoEmptyDocument(() => {
1513 + const {pipe} = renderToPipeableStream(
1514 <html>
1926 - <head>
1927 - <title>before</title>
1928 - <title>in head</title>
1929 - <title>during</title>
1930 - <title>after</title>
1931 - </head>
1515 + <head />
1516 <body>
1933 - <div>hello world</div>
1934 - </body>
1935 - </html>,
1936 - );
1937 -
1938 - ReactDOMClient.hydrateRoot(
1939 - document,
1940 - <>
1941 - <title>before</title>
1942 - <>
1943 - <html>
1944 - <head>
1945 - <title>in head</title>
1946 - </head>
1947 - <body>
1517 + <div>
1518 + outer
1519 + <link rel="stylesheet" href="1one" precedence="one" />
1520 + <link rel="stylesheet" href="1two" precedence="two" />
1521 + <link rel="stylesheet" href="1three" precedence="three" />
1522 + <link rel="stylesheet" href="1four" precedence="four" />
1523 + <Suspense fallback={null}>
1524 <div>
1949 - <title>during</title>
1950 - hello world
1525 + middle
1526 + <link rel="stylesheet" href="2one" precedence="one" />
1527 + <link rel="stylesheet" href="2two" precedence="two" />
1528 + <link rel="stylesheet" href="2three" precedence="three" />
1529 + <link rel="stylesheet" href="2four" precedence="four" />
1530 + <Suspense fallback={null}>
1531 + <div>
1532 + inner
1533 + <link rel="stylesheet" href="3five" precedence="five" />
1534 + <link rel="stylesheet" href="3one" precedence="one" />
1535 + <link rel="stylesheet" href="3two" precedence="two" />
1536 + <link rel="stylesheet" href="3three" precedence="three" />
1537 + <link rel="stylesheet" href="3four" precedence="four" />
1538 + </div>
1539 + </Suspense>
1540 </div>
1952 - </body>
1953 - </html>
1954 - </>
1955 - <title>after</title>
1956 - </>,
1957 - );
1958 - expect(Scheduler).toFlushWithoutYielding();
1959 - expect(getMeaningfulChildren(document)).toEqual(
1960 - <html>
1961 - <head>
1962 - <title>before</title>
1963 - <title>in head</title>
1964 - <title>during</title>
1965 - <title>after</title>
1966 - </head>
1967 - <body>
1968 - <div>hello world</div>
1541 + </Suspense>
1542 + <Suspense fallback={null}>
1543 + <div>middle</div>
1544 + <link rel="stylesheet" href="4one" precedence="one" />
1545 + <link rel="stylesheet" href="4two" precedence="two" />
1546 + <link rel="stylesheet" href="4three" precedence="three" />
1547 + <link rel="stylesheet" href="4four" precedence="four" />
1548 + </Suspense>
1549 + </div>
1550 </body>
1551 </html>,
1552 );
1553 + pipe(writable);
1554 });
1555
1974 - // @gate enableFloat
1975 - it('prepends new titles on the client so newer ones override older ones, including orphaned server rendered titles', async () => {
1976 - await actIntoEmptyDocument(() => {
1977 - const {pipe} = renderToPipeableStream(
1978 - <html>
1979 - <head>
1980 - <title>server</title>
1981 - </head>
1982 - <body>
1983 - <div>hello world</div>
1984 - </body>
1985 - </html>,
1986 - );
1987 - pipe(writable);
1988 - });
1556 + expect(getMeaningfulChildren(document)).toEqual(
1557 + <html>
1558 + <head>
1559 + <link rel="stylesheet" href="1one" data-precedence="one" />
1560 + <link rel="stylesheet" href="2one" data-precedence="one" />
1561 + <link rel="stylesheet" href="3one" data-precedence="one" />
1562 + <link rel="stylesheet" href="4one" data-precedence="one" />
1563
1990 - expect(getMeaningfulChildren(document)).toEqual(
1991 - <html>
1992 - <head>
1993 - <title>server</title>
1994 - </head>
1995 - <body>
1996 - <div>hello world</div>
1997 - </body>
1998 - </html>,
1999 - );
1564 + <link rel="stylesheet" href="1two" data-precedence="two" />
1565 + <link rel="stylesheet" href="2two" data-precedence="two" />
1566 + <link rel="stylesheet" href="3two" data-precedence="two" />
1567 + <link rel="stylesheet" href="4two" data-precedence="two" />
1568
2001 - ReactDOMClient.hydrateRoot(
2002 - document,
2003 - <html>
2004 - <title>html</title>
2005 - <head>
2006 - <title>head</title>
2007 - </head>
2008 - <body>
2009 - <title>body</title>
2010 - <div>hello world</div>
2011 - </body>
2012 - </html>,
2013 - );
2014 - expect(Scheduler).toFlushWithoutYielding();
2015 - expect(getMeaningfulChildren(document)).toEqual(
1569 + <link rel="stylesheet" href="1three" data-precedence="three" />
1570 + <link rel="stylesheet" href="2three" data-precedence="three" />
1571 + <link rel="stylesheet" href="3three" data-precedence="three" />
1572 + <link rel="stylesheet" href="4three" data-precedence="three" />
1573 +
1574 + <link rel="stylesheet" href="1four" data-precedence="four" />
1575 + <link rel="stylesheet" href="2four" data-precedence="four" />
1576 + <link rel="stylesheet" href="3four" data-precedence="four" />
1577 + <link rel="stylesheet" href="4four" data-precedence="four" />
1578 +
1579 + <link rel="stylesheet" href="3five" data-precedence="five" />
1580 + </head>
1581 + <body>
1582 + <div>
1583 + outer
1584 + <div>
1585 + middle<div>inner</div>
1586 + </div>
1587 + <div>middle</div>
1588 + </div>
1589 + </body>
1590 + </html>,
1591 + );
1592 + });
1593 +
1594 + // @gate enableFloat
1595 + it('stylesheet resources are inserted according to precedence order on the client', async () => {
1596 + await actIntoEmptyDocument(() => {
1597 + const {pipe} = renderToPipeableStream(
1598 <html>
2017 - <head>
2018 - <title>body</title>
2019 - <title>head</title>
2020 - <title>html</title>
2021 - <title>server</title>
2022 - </head>
1599 + <head />
1600 <body>
2024 - <div>hello world</div>
1601 + <div>
1602 + <link rel="stylesheet" href="foo" precedence="one" />
1603 + <link rel="stylesheet" href="bar" precedence="two" />
1604 + Hello
1605 + </div>
1606 </body>
1607 </html>,
1608 );
1609 + pipe(writable);
1610 });
1611
2030 - // @gate enableFloat
2031 - it('keys titles on text children and only removes them when no more instances refer to that title', async () => {
2032 - const root = ReactDOMClient.createRoot(container);
2033 - root.render(
2034 - <div>
2035 - <title>{[2]}</title>hello world<title>2</title>
2036 - </div>,
2037 - );
2038 - expect(Scheduler).toFlushWithoutYielding();
2039 - expect(getMeaningfulChildren(document)).toEqual(
2040 - <html>
2041 - <head>
2042 - <title>2</title>
2043 - </head>
2044 - <body>
2045 - <div id="container">
2046 - <div>hello world</div>
2047 - </div>
2048 - </body>
2049 - </html>,
2050 - );
1612 + expect(getMeaningfulChildren(document)).toEqual(
1613 + <html>
1614 + <head>
1615 + <link rel="stylesheet" href="foo" data-precedence="one" />
1616 + <link rel="stylesheet" href="bar" data-precedence="two" />
1617 + </head>
1618 + <body>
1619 + <div>Hello</div>
1620 + </body>
1621 + </html>,
1622 + );
1623 +
1624 + const root = ReactDOMClient.hydrateRoot(
1625 + document,
1626 + <html>
1627 + <head />
1628 + <body>
1629 + <div>
1630 + <link rel="stylesheet" href="foo" precedence="one" />
1631 + <link rel="stylesheet" href="bar" precedence="two" />
1632 + Hello
1633 + </div>
1634 + </body>
1635 + </html>,
1636 + );
1637 + expect(Scheduler).toFlushWithoutYielding();
1638 + expect(getMeaningfulChildren(document)).toEqual(
1639 + <html>
1640 + <head>
1641 + <link rel="stylesheet" href="foo" data-precedence="one" />
1642 + <link rel="stylesheet" href="bar" data-precedence="two" />
1643 + </head>
1644 + <body>
1645 + <div>Hello</div>
1646 + </body>
1647 + </html>,
1648 + );
1649 +
1650 + root.render(
1651 + <html>
1652 + <head />
1653 + <body>
1654 + <div>Hello</div>
1655 + <link rel="stylesheet" href="baz" precedence="one" />
1656 + </body>
1657 + </html>,
1658 + );
1659 + expect(Scheduler).toFlushWithoutYielding();
1660 + expect(getMeaningfulChildren(document)).toEqual(
1661 + <html>
1662 + <head>
1663 + <link rel="stylesheet" href="foo" data-precedence="one" />
1664 + <link rel="stylesheet" href="baz" data-precedence="one" />
1665 + <link rel="stylesheet" href="bar" data-precedence="two" />
1666 + <link rel="preload" as="style" href="baz" />
1667 + </head>
1668 + <body>
1669 + <div>Hello</div>
1670 + </body>
1671 + </html>,
1672 + );
1673 + });
1674 +
1675 + // @gate enableFloat
1676 + it('inserts preloads in render phase eagerly', async () => {
1677 + function Throw() {
1678 + throw new Error('Uh oh!');
1679 + }
1680 + class ErrorBoundary extends React.Component {
1681 + state = {hasError: false, error: null};
1682 + static getDerivedStateFromError(error) {
1683 + return {
1684 + hasError: true,
1685 + error,
1686 + };
1687 + }
1688 + render() {
1689 + if (this.state.hasError) {
1690 + return this.state.error.message;
1691 + }
1692 + return this.props.children;
1693 + }
1694 + }
1695
2052 - root.render(
2053 - <div>
2054 - {null}hello world<title>2</title>
2055 - </div>,
2056 - );
2057 - expect(Scheduler).toFlushWithoutYielding();
2058 - expect(getMeaningfulChildren(document)).toEqual(
2059 - <html>
2060 - <head>
2061 - <title>2</title>
2062 - </head>
2063 - <body>
2064 - <div id="container">
2065 - <div>hello world</div>
2066 - </div>
2067 - </body>
2068 - </html>,
2069 - );
2070 - root.render(
2071 - <div>
2072 - {null}hello world{null}
2073 - </div>,
2074 - );
2075 - expect(Scheduler).toFlushWithoutYielding();
2076 - expect(getMeaningfulChildren(document)).toEqual(
1696 + const root = ReactDOMClient.createRoot(container);
1697 + root.render(
1698 + <ErrorBoundary>
1699 + <link rel="stylesheet" href="foo" precedence="default" />
1700 + <div>foo</div>
1701 + <Throw />
1702 + </ErrorBoundary>,
1703 + );
1704 + expect(Scheduler).toFlushWithoutYielding();
1705 + expect(getMeaningfulChildren(document)).toEqual(
1706 + <html>
1707 + <head>
1708 + <link rel="preload" href="foo" as="style" />
1709 + </head>
1710 + <body>
1711 + <div id="container">Uh oh!</div>
1712 + </body>
1713 + </html>,
1714 + );
1715 + });
1716 +
1717 + // @gate enableFloat
1718 + it('will include child boundary stylesheet resources in the boundary reveal instruction', async () => {
1719 + await actIntoEmptyDocument(() => {
1720 + const {pipe} = renderToPipeableStream(
1721 <html>
1722 <head />
1723 <body>
2080 - <div id="container">
2081 - <div>hello world</div>
1724 + <div>
1725 + <Suspense fallback="loading foo...">
1726 + <BlockedOn value="foo">
1727 + <div>foo</div>
1728 + <link rel="stylesheet" href="foo" precedence="default" />
1729 + <Suspense fallback="loading bar...">
1730 + <BlockedOn value="bar">
1731 + <div>bar</div>
1732 + <link rel="stylesheet" href="bar" precedence="default" />
1733 + <Suspense fallback="loading baz...">
1734 + <BlockedOn value="baz">
1735 + <div>baz</div>
1736 + <link
1737 + rel="stylesheet"
1738 + href="baz"
1739 + precedence="default"
1740 + />
1741 + </BlockedOn>
1742 + </Suspense>
1743 + </BlockedOn>
1744 + </Suspense>
1745 + </BlockedOn>
1746 + </Suspense>
1747 </div>
1748 </body>
1749 </html>,
1750 );
1751 + pipe(writable);
1752 });
1753
2088 - // @gate enableFloat && enableHostSingletons && (enableClientRenderFallbackOnTextMismatch || !__DEV__)
2089 - it('can render a title before a singleton even if that singleton clears its contents', async () => {
2090 - await actIntoEmptyDocument(() => {
2091 - const {pipe} = renderToPipeableStream(
2092 - <>
2093 - <title>foo</title>
2094 - <html>
2095 - <head />
2096 - <body>
2097 - <div>server</div>
2098 - </body>
2099 - </html>
2100 - </>,
2101 - );
2102 - pipe(writable);
2103 - });
1754 + expect(getMeaningfulChildren(document)).toEqual(
1755 + <html>
1756 + <head />
1757 + <body>
1758 + <div>loading foo...</div>
1759 + </body>
1760 + </html>,
1761 + );
1762
2105 - const errors = [];
2106 - ReactDOMClient.hydrateRoot(
2107 - document,
2108 - <>
2109 - <title>foo</title>
2110 - <html>
2111 - <head />
2112 - <body>
2113 - <div>client</div>
2114 - </body>
2115 - </html>
2116 - </>,
2117 - {
2118 - onRecoverableError(err) {
2119 - errors.push(err.message);
2120 - },
2121 - },
2122 - );
2123 - try {
2124 - expect(() => {
2125 - expect(Scheduler).toFlushWithoutYielding();
2126 - }).toErrorDev(
2127 - [
2128 - 'Warning: Text content did not match. Server: "server" Client: "client"',
2129 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
2130 - ],
2131 - {withoutStack: 1},
2132 - );
2133 - } catch (e) {
2134 - // When gates are false this test fails on a DOMException if you don't clear the scheduler after catching.
2135 - // When gates are true this branch should not be hit
2136 - expect(Scheduler).toFlushWithoutYielding();
2137 - throw e;
2138 - }
2139 - expect(getMeaningfulChildren(document)).toEqual(
2140 - <html>
2141 - <head>
2142 - <title>foo</title>
2143 - </head>
2144 - <body>
2145 - <div>client</div>
2146 - </body>
2147 - </html>,
2148 - );
1763 + await act(() => {
1764 + resolveText('bar');
1765 });
2150 - });
1766 + expect(getMeaningfulChildren(document)).toEqual(
1767 + <html>
1768 + <head />
1769 + <body>
1770 + <div>loading foo...</div>
1771 + </body>
1772 + </html>,
1773 + );
1774
2152 - describe('style resources', () => {
2153 - // @gate enableFloat
2154 - it('treats link rel stylesheet elements as a style resource when it includes a precedence when server rendering', async () => {
2155 - await actIntoEmptyDocument(() => {
2156 - const {pipe} = renderToPipeableStream(
2157 - <html>
2158 - <head />
2159 - <body>
2160 - <link rel="stylesheet" href="aresource" precedence="foo" />
2161 - <div>hello world</div>
2162 - </body>
2163 - </html>,
2164 - );
2165 - pipe(writable);
2166 - });
1775 + await act(() => {
1776 + resolveText('baz');
1777 + });
1778 + expect(getMeaningfulChildren(document)).toEqual(
1779 + <html>
1780 + <head />
1781 + <body>
1782 + <div>loading foo...</div>
1783 + </body>
1784 + </html>,
1785 + );
1786
2168 - expect(getMeaningfulChildren(document)).toEqual(
2169 - <html>
2170 - <head>
2171 - <link rel="stylesheet" href="aresource" data-precedence="foo" />
2172 - </head>
2173 - <body>
2174 - <div>hello world</div>
2175 - </body>
2176 - </html>,
1787 + await act(() => {
1788 + resolveText('foo');
1789 + });
1790 + expect(getMeaningfulChildren(document)).toEqual(
1791 + <html>
1792 + <head>
1793 + <link rel="stylesheet" href="foo" data-precedence="default" />
1794 + <link rel="stylesheet" href="bar" data-precedence="default" />
1795 + <link rel="stylesheet" href="baz" data-precedence="default" />
1796 + </head>
1797 + <body>
1798 + <div>loading foo...</div>
1799 + <link rel="preload" href="foo" as="style" />
1800 + <link rel="preload" href="bar" as="style" />
1801 + <link rel="preload" href="baz" as="style" />
1802 + </body>
1803 + </html>,
1804 + );
1805 +
1806 + await act(() => {
1807 + const event = document.createEvent('Events');
1808 + event.initEvent('load', true, true);
1809 + Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
1810 + el => {
1811 + el.dispatchEvent(event);
1812 + },
1813 );
1814 });
1815 + expect(getMeaningfulChildren(document)).toEqual(
1816 + <html>
1817 + <head>
1818 + <link rel="stylesheet" href="foo" data-precedence="default" />
1819 + <link rel="stylesheet" href="bar" data-precedence="default" />
1820 + <link rel="stylesheet" href="baz" data-precedence="default" />
1821 + </head>
1822 + <body>
1823 + <div>
1824 + <div>foo</div>
1825 + <div>bar</div>
1826 + <div>baz</div>
1827 + </div>
1828 + <link rel="preload" href="foo" as="style" />
1829 + <link rel="preload" href="bar" as="style" />
1830 + <link rel="preload" href="baz" as="style" />
1831 + </body>
1832 + </html>,
1833 + );
1834 + });
1835
2180 - // @gate enableFloat
2181 - it('treats link rel stylesheet elements as a style resource when it includes a precedence when client rendering', async () => {
2182 - const root = ReactDOMClient.createRoot(document);
2183 - root.render(
1836 + // @gate enableFloat
1837 + it('will hoist resources of child boundaries emitted as part of a partial boundary to the parent boundary', async () => {
1838 + await actIntoEmptyDocument(() => {
1839 + const {pipe} = renderToPipeableStream(
1840 <html>
1841 <head />
1842 <body>
2187 - <link rel="stylesheet" href="aresource" precedence="foo" />
2188 - <div>hello world</div>
1843 + <div>
1844 + <Suspense fallback="loading...">
1845 + <div>
1846 + <BlockedOn value="foo">
1847 + <div>foo</div>
1848 + <link rel="stylesheet" href="foo" precedence="default" />
1849 + <Suspense fallback="loading bar...">
1850 + <BlockedOn value="bar">
1851 + <div>bar</div>
1852 + <link
1853 + rel="stylesheet"
1854 + href="bar"
1855 + precedence="default"
1856 + />
1857 + <Suspense fallback="loading baz...">
1858 + <div>
1859 + <BlockedOn value="baz">
1860 + <div>baz</div>
1861 + <link
1862 + rel="stylesheet"
1863 + href="baz"
1864 + precedence="default"
1865 + />
1866 + </BlockedOn>
1867 + </div>
1868 + </Suspense>
1869 + </BlockedOn>
1870 + </Suspense>
1871 + </BlockedOn>
1872 + <BlockedOn value="qux">
1873 + <div>qux</div>
1874 + <link rel="stylesheet" href="qux" precedence="default" />
1875 + </BlockedOn>
1876 + </div>
1877 + </Suspense>
1878 + </div>
1879 </body>
1880 </html>,
1881 );
2192 - expect(Scheduler).toFlushWithoutYielding();
1882 + pipe(writable);
1883 + });
1884
2194 - expect(getMeaningfulChildren(document)).toEqual(
2195 - <html>
2196 - <head>
2197 - <link rel="stylesheet" href="aresource" data-precedence="foo" />
2198 - </head>
2199 - <body>
2200 - <div>hello world</div>
2201 - </body>
2202 - </html>,
2203 - );
1885 + expect(getMeaningfulChildren(document)).toEqual(
1886 + <html>
1887 + <head />
1888 + <body>
1889 + <div>loading...</div>
1890 + </body>
1891 + </html>,
1892 + );
1893 +
1894 + // This will enqueue a stylesheet resource in a deep blocked boundary (loading baz...).
1895 + await act(() => {
1896 + resolveText('baz');
1897 });
1898 + expect(getMeaningfulChildren(document)).toEqual(
1899 + <html>
1900 + <head />
1901 + <body>
1902 + <div>loading...</div>
1903 + </body>
1904 + </html>,
1905 + );
1906
2206 - // @gate enableFloat
2207 - it('treats link rel stylesheet elements as a style resource when it includes a precedence when hydrating', async () => {
2208 - await actIntoEmptyDocument(() => {
2209 - const {pipe} = renderToPipeableStream(
2210 - <html>
2211 - <head />
2212 - <body>
2213 - <link rel="stylesheet" href="aresource" precedence="foo" />
2214 - <div>hello world</div>
2215 - </body>
2216 - </html>,
2217 - );
2218 - pipe(writable);
2219 - });
2220 - ReactDOMClient.hydrateRoot(
2221 - document,
2222 - <html>
2223 - <head />
2224 - <body>
2225 - <link rel="stylesheet" href="aresource" precedence="foo" />
2226 - <div>hello world</div>
2227 - </body>
2228 - </html>,
1907 + // This will enqueue a stylesheet resource in the intermediate blocked boundary (loading bar...).
1908 + await act(() => {
1909 + resolveText('bar');
1910 + });
1911 + expect(getMeaningfulChildren(document)).toEqual(
1912 + <html>
1913 + <head />
1914 + <body>
1915 + <div>loading...</div>
1916 + </body>
1917 + </html>,
1918 + );
1919 +
1920 + // This will complete a segment in the top level boundary that is still blocked on another segment.
1921 + // It will flush the completed segment however the inner boundaries should not emit their style dependencies
1922 + // because they are not going to be revealed yet. instead their dependencies are hoisted to the blocked
1923 + // boundary (top level).
1924 + await act(() => {
1925 + resolveText('foo');
1926 + });
1927 + expect(getMeaningfulChildren(document)).toEqual(
1928 + <html>
1929 + <head />
1930 + <body>
1931 + <div>loading...</div>
1932 + <link rel="preload" href="foo" as="style" />
1933 + <link rel="preload" href="bar" as="style" />
1934 + <link rel="preload" href="baz" as="style" />
1935 + </body>
1936 + </html>,
1937 + );
1938 +
1939 + // This resolves the last blocked segment on the top level boundary so we see all dependencies of the
1940 + // nested boundaries emitted at this level
1941 + await act(() => {
1942 + resolveText('qux');
1943 + });
1944 + expect(getMeaningfulChildren(document)).toEqual(
1945 + <html>
1946 + <head>
1947 + <link rel="stylesheet" href="foo" data-precedence="default" />
1948 + <link rel="stylesheet" href="bar" data-precedence="default" />
1949 + <link rel="stylesheet" href="baz" data-precedence="default" />
1950 + <link rel="stylesheet" href="qux" data-precedence="default" />
1951 + </head>
1952 + <body>
1953 + <div>loading...</div>
1954 + <link rel="preload" href="foo" as="style" />
1955 + <link rel="preload" href="bar" as="style" />
1956 + <link rel="preload" href="baz" as="style" />
1957 + <link rel="preload" href="qux" as="style" />
1958 + </body>
1959 + </html>,
1960 + );
1961 +
1962 + // We load all stylesheets and confirm the content is revealed
1963 + await act(() => {
1964 + const event = document.createEvent('Events');
1965 + event.initEvent('load', true, true);
1966 + Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(
1967 + el => {
1968 + el.dispatchEvent(event);
1969 + },
1970 );
2230 - expect(Scheduler).toFlushWithoutYielding();
1971 + });
1972 + expect(getMeaningfulChildren(document)).toEqual(
1973 + <html>
1974 + <head>
1975 + <link rel="stylesheet" href="foo" data-precedence="default" />
1976 + <link rel="stylesheet" href="bar" data-precedence="default" />
1977 + <link rel="stylesheet" href="baz" data-precedence="default" />
1978 + <link rel="stylesheet" href="qux" data-precedence="default" />
1979 + </head>
1980 + <body>
1981 + <div>
1982 + <div>
1983 + <div>foo</div>
1984 + <div>bar</div>
1985 + <div>
1986 + <div>baz</div>
1987 + </div>
1988 + <div>qux</div>
1989 + </div>
1990 + </div>
1991 + <link rel="preload" href="foo" as="style" />
1992 + <link rel="preload" href="bar" as="style" />
1993 + <link rel="preload" href="baz" as="style" />
1994 + <link rel="preload" href="qux" as="style" />
1995 + </body>
1996 + </html>,
1997 + );
1998 + });
1999
2232 - expect(getMeaningfulChildren(document)).toEqual(
2000 + // @gate enableFloat
2001 + it('encodes attributes consistently whether resources are flushed in shell or in late boundaries', async () => {
2002 + function App() {
2003 + return (
2004 <html>
2234 - <head>
2235 - <link rel="stylesheet" href="aresource" data-precedence="foo" />
2236 - </head>
2005 + <head />
2006 <body>
2238 - <div>hello world</div>
2007 + <div>
2008 + <link
2009 + // This preload is explicit so it can flush with a lot of potential attrs
2010 + // We will duplicate this as a style that flushes after the shell
2011 + rel="stylesheet"
2012 + href="foo"
2013 + // precedence is not a special attribute for preloads so this will just flush as is
2014 + precedence="default"
2015 + // Some standard link props
2016 + crossOrigin="anonymous"
2017 + media="all"
2018 + integrity="somehash"
2019 + referrerPolicy="origin"
2020 + // data and non starndard attributes that should flush
2021 + data-foo={'"quoted"'}
2022 + nonStandardAttr="attr"
2023 + properlyformattednonstandardattr="attr"
2024 + // attributes that should be filtered out for violating certain rules
2025 + onSomething="this should be removed b/c event handler"
2026 + shouldnotincludefunctions={() => {}}
2027 + norsymbols={Symbol('foo')}
2028 + />
2029 + <Suspense fallback={'loading...'}>
2030 + <BlockedOn value="unblock">
2031 + <link
2032 + // This preload is explicit so it can flush with a lot of potential attrs
2033 + // We will duplicate this as a style that flushes after the shell
2034 + rel="stylesheet"
2035 + href="bar"
2036 + // opt-in property to get this treated as a resource
2037 + precedence="default"
2038 + // Some standard link props
2039 + crossOrigin="anonymous"
2040 + media="all"
2041 + integrity="somehash"
2042 + referrerPolicy="origin"
2043 + // data and non starndard attributes that should flush
2044 + data-foo={'"quoted"'}
2045 + nonStandardAttr="attr"
2046 + properlyformattednonstandardattr="attr"
2047 + // attributes that should be filtered out for violating certain rules
2048 + onSomething="this should be removed b/c event handler"
2049 + shouldnotincludefunctions={() => {}}
2050 + norsymbols={Symbol('foo')}
2051 + />
2052 + </BlockedOn>
2053 + </Suspense>
2054 + </div>
2055 </body>
2240 - </html>,
2056 + </html>
2057 );
2242 - });
2243 -
2244 - // @gate enableFloat
2245 - it('preloads stylesheets without a precedence prop when server rendering', async () => {
2058 + }
2059 + await expect(async () => {
2060 await actIntoEmptyDocument(() => {
2247 - const {pipe} = renderToPipeableStream(
2248 - <html>
2249 - <head />
2250 - <body>
2251 - <link rel="stylesheet" href="notaresource" />
2252 - <div>hello world</div>
2253 - </body>
2254 - </html>,
2255 - );
2061 + const {pipe} = renderToPipeableStream(<App />);
2062 pipe(writable);
2063 });
2258 -
2064 expect(getMeaningfulChildren(document)).toEqual(
2065 <html>
2066 <head>
2262 - <link rel="preload" as="style" href="notaresource" />
2067 + <link
2068 + rel="stylesheet"
2069 + href="foo"
2070 + data-precedence="default"
2071 + crossorigin="anonymous"
2072 + media="all"
2073 + integrity="somehash"
2074 + referrerpolicy="origin"
2075 + data-foo={'"quoted"'}
2076 + nonstandardattr="attr"
2077 + properlyformattednonstandardattr="attr"
2078 + />
2079 </head>
2080 <body>
2265 - <link rel="stylesheet" href="notaresource" />
2266 - <div>hello world</div>
2081 + <div>loading...</div>
2082 </body>
2083 </html>,
2084 );
2085 + }).toErrorDev([
2086 + 'React does not recognize the `nonStandardAttr` prop on a DOM element.' +
2087 + ' If you intentionally want it to appear in the DOM as a custom attribute,' +
2088 + ' spell it as lowercase `nonstandardattr` instead. If you accidentally passed it from a' +
2089 + ' parent component, remove it from the DOM element.',
2090 + 'Invalid values for props `shouldnotincludefunctions`, `norsymbols` on <link> tag. Either remove them from' +
2091 + ' the element, or pass a string or number value to keep them in the DOM. For' +
2092 + ' details, see https://reactjs.org/link/attribute-behavior',
2093 + ]);
2094 +
2095 + // Now we flush the stylesheet with the boundary
2096 + await act(() => {
2097 + resolveText('unblock');
2098 });
2099
2272 - // @gate enableFloat
2273 - it('hoists style resources to the correct precedence', async () => {
2274 - await actIntoEmptyDocument(() => {
2275 - const {pipe} = renderToPipeableStream(
2276 - <html>
2277 - <head />
2278 - <body>
2279 - <link rel="stylesheet" href="foo1" precedence="foo" />
2280 - <link rel="stylesheet" href="default1" precedence="default" />
2281 - <link rel="stylesheet" href="foo2" precedence="foo" />
2282 - <div>hello world</div>
2283 - </body>
2284 - </html>,
2285 - );
2286 - pipe(writable);
2287 - });
2288 - expect(getMeaningfulChildren(document)).toEqual(
2289 - <html>
2290 - <head>
2291 - <link rel="stylesheet" href="foo1" data-precedence="foo" />
2292 - <link rel="stylesheet" href="foo2" data-precedence="foo" />
2293 - <link rel="stylesheet" href="default1" data-precedence="default" />
2294 - </head>
2295 - <body>
2296 - <div>hello world</div>
2297 - </body>
2298 - </html>,
2299 - );
2100 + expect(getMeaningfulChildren(document)).toEqual(
2101 + <html>
2102 + <head>
2103 + <link
2104 + rel="stylesheet"
2105 + href="foo"
2106 + data-precedence="default"
2107 + crossorigin="anonymous"
2108 + media="all"
2109 + integrity="somehash"
2110 + referrerpolicy="origin"
2111 + data-foo={'"quoted"'}
2112 + nonstandardattr="attr"
2113 + properlyformattednonstandardattr="attr"
2114 + />
2115 + <link
2116 + rel="stylesheet"
2117 + href="bar"
2118 + data-precedence="default"
2119 + crossorigin="anonymous"
2120 + media="all"
2121 + integrity="somehash"
2122 + referrerpolicy="origin"
2123 + data-foo={'"quoted"'}
2124 + nonstandardattr="attr"
2125 + properlyformattednonstandardattr="attr"
2126 + />
2127 + </head>
2128 + <body>
2129 + <div>loading...</div>
2130 + <link
2131 + rel="preload"
2132 + as="style"
2133 + href="bar"
2134 + crossorigin="anonymous"
2135 + media="all"
2136 + integrity="somehash"
2137 + referrerpolicy="origin"
2138 + />
2139 + </body>
2140 + </html>,
2141 + );
2142 + });
2143
2301 - ReactDOMClient.hydrateRoot(
2302 - document,
2144 + // @gate enableFloat
2145 + it('boundary stylesheet resource dependencies hoist to a parent boundary when flushed inline', async () => {
2146 + await actIntoEmptyDocument(() => {
2147 + const {pipe} = renderToPipeableStream(
2148 <html>
2149 <head />
2150 <body>
2306 - <link rel="stylesheet" href="bar1" precedence="bar" />
2307 - <link rel="stylesheet" href="foo3" precedence="foo" />
2308 - <link rel="stylesheet" href="default2" precedence="default" />
2309 - <div>hello world</div>
2310 - </body>
2311 - </html>,
2312 - );
2313 - expect(Scheduler).toFlushWithoutYielding();
2314 - expect(getMeaningfulChildren(document)).toEqual(
2315 - <html>
2316 - <head>
2317 - <link rel="stylesheet" href="foo1" data-precedence="foo" />
2318 - <link rel="stylesheet" href="foo2" data-precedence="foo" />
2319 - <link rel="stylesheet" href="foo3" data-precedence="foo" />
2320 - <link rel="stylesheet" href="default1" data-precedence="default" />
2321 - <link rel="stylesheet" href="default2" data-precedence="default" />
2322 - <link rel="stylesheet" href="bar1" data-precedence="bar" />
2323 - <link rel="preload" as="style" href="bar1" />
2324 - <link rel="preload" as="style" href="foo3" />
2325 - <link rel="preload" as="style" href="default2" />
2326 - </head>
2327 - <body>
2328 - <div>hello world</div>
2151 + <div>
2152 + <Suspense fallback="loading A...">
2153 + <BlockedOn value="unblock">
2154 + <AsyncText text="A" />
2155 + <link rel="stylesheet" href="A" precedence="A" />
2156 + <Suspense fallback="loading AA...">
2157 + <AsyncText text="AA" />
2158 + <link rel="stylesheet" href="AA" precedence="AA" />
2159 + <Suspense fallback="loading AAA...">
2160 + <AsyncText text="AAA" />
2161 + <link rel="stylesheet" href="AAA" precedence="AAA" />
2162 + <Suspense fallback="loading AAAA...">
2163 + <AsyncText text="AAAA" />
2164 + <link rel="stylesheet" href="AAAA" precedence="AAAA" />
2165 + </Suspense>
2166 + </Suspense>
2167 + </Suspense>
2168 + </BlockedOn>
2169 + </Suspense>
2170 + </div>
2171 </body>
2172 </html>,
2173 );
2174 + pipe(writable);
2175 });
2176 + expect(getMeaningfulChildren(document)).toEqual(
2177 + <html>
2178 + <head />
2179 + <body>
2180 + <div>loading A...</div>
2181 + </body>
2182 + </html>,
2183 + );
2184
2334 - // @gate enableFloat
2335 - it('retains styles even after the last referring Resource unmounts', async () => {
2336 - // This test is true until a future update where there is some form of garbage collection.
2337 - const root = ReactDOMClient.createRoot(document);
2185 + await act(() => {
2186 + resolveText('unblock');
2187 + resolveText('AAAA');
2188 + resolveText('AA');
2189 + });
2190 + expect(getMeaningfulChildren(document)).toEqual(
2191 + <html>
2192 + <head />
2193 + <body>
2194 + <div>loading A...</div>
2195 + <link rel="preload" as="style" href="A" />
2196 + <link rel="preload" as="style" href="AA" />
2197 + <link rel="preload" as="style" href="AAA" />
2198 + <link rel="preload" as="style" href="AAAA" />
2199 + </body>
2200 + </html>,
2201 + );
2202
2339 - root.render(
2340 - <html>
2341 - <head />
2342 - <body>
2343 - hello world
2344 - <link rel="stylesheet" href="foo" precedence="foo" />
2345 - </body>
2346 - </html>,
2347 - );
2348 - expect(Scheduler).toFlushWithoutYielding();
2203 + await act(() => {
2204 + resolveText('A');
2205 + });
2206 + await act(() => {
2207 + document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2208 + const event = document.createEvent('Events');
2209 + event.initEvent('load', true, true);
2210 + l.dispatchEvent(event);
2211 + });
2212 + });
2213 + expect(getMeaningfulChildren(document)).toEqual(
2214 + <html>
2215 + <head>
2216 + <link rel="stylesheet" href="A" data-precedence="A" />
2217 + <link rel="stylesheet" href="AA" data-precedence="AA" />
2218 + </head>
2219 + <body>
2220 + <div>
2221 + {'A'}
2222 + {'AA'}
2223 + {'loading AAA...'}
2224 + </div>
2225 + <link rel="preload" as="style" href="A" />
2226 + <link rel="preload" as="style" href="AA" />
2227 + <link rel="preload" as="style" href="AAA" />
2228 + <link rel="preload" as="style" href="AAAA" />
2229 + </body>
2230 + </html>,
2231 + );
2232
2350 - root.render(
2351 - <html>
2352 - <head />
2353 - <body>hello world</body>
2354 - </html>,
2355 - );
2356 - expect(Scheduler).toFlushWithoutYielding();
2357 - expect(getMeaningfulChildren(document)).toEqual(
2233 + await act(() => {
2234 + resolveText('AAA');
2235 + });
2236 + await act(() => {
2237 + document.querySelectorAll('link[rel="stylesheet"]').forEach(l => {
2238 + const event = document.createEvent('Events');
2239 + event.initEvent('load', true, true);
2240 + l.dispatchEvent(event);
2241 + });
2242 + });
2243 + expect(getMeaningfulChildren(document)).toEqual(
2244 + <html>
2245 + <head>
2246 + <link rel="stylesheet" href="A" data-precedence="A" />
2247 + <link rel="stylesheet" href="AA" data-precedence="AA" />
2248 + <link rel="stylesheet" href="AAA" data-precedence="AAA" />
2249 + <link rel="stylesheet" href="AAAA" data-precedence="AAAA" />
2250 + </head>
2251 + <body>
2252 + <div>
2253 + {'A'}
2254 + {'AA'}
2255 + {'AAA'}
2256 + {'AAAA'}
2257 + </div>
2258 + <link rel="preload" as="style" href="A" />
2259 + <link rel="preload" as="style" href="AA" />
2260 + <link rel="preload" as="style" href="AAA" />
2261 + <link rel="preload" as="style" href="AAAA" />
2262 + </body>
2263 + </html>,
2264 + );
2265 + });
2266 +
2267 + // @gate enableFloat
2268 + it('always enforces crossOrigin "anonymous" for font preloads', async () => {
2269 + function App() {
2270 + ReactDOM.preload('foo', {as: 'font'});
2271 + ReactDOM.preload('bar', {as: 'font', crossOrigin: 'foo'});
2272 + ReactDOM.preload('baz', {as: 'font', crossOrigin: 'use-credentials'});
2273 + ReactDOM.preload('qux', {as: 'font', crossOrigin: 'anonymous'});
2274 + return (
2275 <html>
2359 - <head>
2360 - <link rel="stylesheet" href="foo" data-precedence="foo" />
2361 - </head>
2362 - <body>hello world</body>
2363 - </html>,
2276 + <head />
2277 + <body />
2278 + </html>
2279 );
2280 + }
2281 + await actIntoEmptyDocument(() => {
2282 + const {pipe} = renderToPipeableStream(<App />);
2283 + pipe(writable);
2284 });
2285 + expect(getMeaningfulChildren(document)).toEqual(
2286 + <html>
2287 + <head>
2288 + <link rel="preload" as="font" href="foo" crossorigin="" />
2289 + <link rel="preload" as="font" href="bar" crossorigin="" />
2290 + <link rel="preload" as="font" href="baz" crossorigin="" />
2291 + <link rel="preload" as="font" href="qux" crossorigin="" />
2292 + </head>
2293 + <body />
2294 + </html>,
2295 + );
2296 + });
2297
2367 - // @gate enableFloat && enableHostSingletons && enableClientRenderFallbackOnTextMismatch
2368 - it('retains styles even when a new html, head, and/body mount', async () => {
2369 - await actIntoEmptyDocument(() => {
2370 - const {pipe} = renderToPipeableStream(
2298 + describe('ReactDOM.preload(href, { as: ... })', () => {
2299 + // @gate enableFloat
2300 + it('creates a preload resource when called', async () => {
2301 + function App() {
2302 + ReactDOM.preload('foo', {as: 'style'});
2303 + return (
2304 <html>
2372 - <head />
2305 <body>
2374 - <link rel="stylesheet" href="foo" precedence="foo" />
2375 - <link rel="stylesheet" href="bar" precedence="bar" />
2376 - server
2306 + <Suspense fallback="loading...">
2307 + <BlockedOn value="blocked">
2308 + <Component />
2309 + </BlockedOn>
2310 + </Suspense>
2311 </body>
2378 - </html>,
2312 + </html>
2313 );
2314 + }
2315 + function Component() {
2316 + ReactDOM.preload('bar', {as: 'script'});
2317 + return <div>hello</div>;
2318 + }
2319 +
2320 + await actIntoEmptyDocument(() => {
2321 + const {pipe} = renderToPipeableStream(<App />);
2322 pipe(writable);
2323 });
2382 - const errors = [];
2383 - ReactDOMClient.hydrateRoot(
2384 - document,
2324 + expect(getMeaningfulChildren(document)).toEqual(
2325 <html>
2326 <head>
2387 - <link rel="stylesheet" href="qux" precedence="qux" />
2388 - <link rel="stylesheet" href="foo" precedence="foo" />
2327 + <link rel="preload" as="style" href="foo" />
2328 </head>
2390 - <body>client</body>
2329 + <body>loading...</body>
2330 </html>,
2392 - {
2393 - onRecoverableError(error) {
2394 - errors.push(error.message);
2395 - },
2396 - },
2397 - );
2398 - expect(() => {
2399 - expect(Scheduler).toFlushWithoutYielding();
2400 - }).toErrorDev(
2401 - [
2402 - 'Warning: Text content did not match. Server: "server" Client: "client"',
2403 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
2404 - ],
2405 - {withoutStack: 1},
2331 );
2332 +
2333 + await act(() => {
2334 + resolveText('blocked');
2335 + });
2336 expect(getMeaningfulChildren(document)).toEqual(
2337 <html>
2338 <head>
2410 - <link rel="stylesheet" href="foo" data-precedence="foo" />
2411 - <link rel="stylesheet" href="bar" data-precedence="bar" />
2412 - <link rel="stylesheet" href="qux" data-precedence="qux" />
2339 + <link rel="preload" as="style" href="foo" />
2340 </head>
2414 - <body>client</body>
2341 + <body>
2342 + <div>hello</div>
2343 + <link rel="preload" as="script" href="bar" />
2344 + </body>
2345 </html>,
2346 );
2417 - });
2347
2419 - // @gate enableFloat && !enableHostSingletons
2420 - it('retains styles even when a new html, head, and/body mount - without HostSingleton', async () => {
2421 - await actIntoEmptyDocument(() => {
2422 - const {pipe} = renderToPipeableStream(
2348 + function ClientApp() {
2349 + ReactDOM.preload('foo', {as: 'style'});
2350 + React.useInsertionEffect(() => ReactDOM.preload('bar', {as: 'script'}));
2351 + React.useLayoutEffect(() => ReactDOM.preload('baz', {as: 'font'}));
2352 + React.useEffect(() => ReactDOM.preload('qux', {as: 'style'}));
2353 + return (
2354 <html>
2424 - <head />
2355 <body>
2426 - <link rel="stylesheet" href="foo" precedence="foo" />
2427 - <link rel="stylesheet" href="bar" precedence="bar" />
2428 - server
2356 + <Suspense fallback="loading...">
2357 + <div>hello</div>
2358 + </Suspense>
2359 </body>
2430 - </html>,
2360 + </html>
2361 );
2432 - pipe(writable);
2433 - });
2434 - const errors = [];
2435 - ReactDOMClient.hydrateRoot(
2436 - document,
2437 - <html>
2438 - <head>
2439 - <link rel="stylesheet" href="qux" precedence="qux" />
2440 - <link rel="stylesheet" href="foo" precedence="foo" />
2441 - </head>
2442 - <body>client</body>
2443 - </html>,
2444 - {
2445 - onRecoverableError(error) {
2446 - errors.push(error.message);
2447 - },
2448 - },
2449 - );
2450 - expect(() => {
2451 - expect(Scheduler).toFlushWithoutYielding();
2452 - }).toErrorDev(
2453 - [
2454 - 'Warning: Text content did not match. Server: "server" Client: "client"',
2455 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
2456 - ],
2457 - {withoutStack: 1},
2458 - );
2362 + }
2363 + ReactDOMClient.hydrateRoot(document, <ClientApp />);
2364 + expect(Scheduler).toFlushWithoutYielding();
2365 expect(getMeaningfulChildren(document)).toEqual(
2366 <html>
2367 <head>
2462 - <link rel="stylesheet" href="qux" data-precedence="qux" />
2463 - <link rel="stylesheet" href="foo" data-precedence="foo" />
2368 + <link rel="preload" as="style" href="foo" />
2369 + <link rel="preload" as="font" href="baz" crossorigin="" />
2370 + <link rel="preload" as="style" href="qux" />
2371 </head>
2465 - <body>client</body>
2466 - </html>,
2467 - );
2468 - });
2469 -
2470 - // @gate enableFloat && enableHostSingletons
2471 - it('retains styles in head through head remounts', async () => {
2472 - const root = ReactDOMClient.createRoot(document);
2473 - root.render(
2474 - <html>
2475 - <head key={1} />
2372 <body>
2477 - <link rel="stylesheet" href="foo" precedence="foo" />
2478 - <link rel="stylesheet" href="bar" precedence="bar" />
2479 - {null}
2480 - hello
2373 + <div>hello</div>
2374 + <link rel="preload" as="script" href="bar" />
2375 </body>
2376 </html>,
2377 );
2484 - expect(Scheduler).toFlushWithoutYielding();
2378 + });
2379 +
2380 + // @gate enableFloat
2381 + it('can seed connection props for stylesheet and script resources', async () => {
2382 + function App() {
2383 + ReactDOM.preload('foo', {
2384 + as: 'style',
2385 + crossOrigin: 'use-credentials',
2386 + integrity: 'some hash',
2387 + });
2388 + return (
2389 + <html>
2390 + <body>
2391 + <div>hello</div>
2392 + <link rel="stylesheet" href="foo" precedence="default" />
2393 + </body>
2394 + </html>
2395 + );
2396 + }
2397 +
2398 + await actIntoEmptyDocument(() => {
2399 + const {pipe} = renderToPipeableStream(<App />);
2400 + pipe(writable);
2401 + });
2402 +
2403 expect(getMeaningfulChildren(document)).toEqual(
2404 <html>
2405 <head>
2488 - <link rel="stylesheet" href="foo" data-precedence="foo" />
2489 - <link rel="stylesheet" href="bar" data-precedence="bar" />
2406 + <link
2407 + rel="stylesheet"
2408 + href="foo"
2409 + data-precedence="default"
2410 + crossorigin="use-credentials"
2411 + integrity="some hash"
2412 + />
2413 </head>
2491 - <body>hello</body>
2492 - </html>,
2493 - );
2494 -
2495 - root.render(
2496 - <html>
2497 - <head key={2} />
2414 <body>
2499 - <link rel="stylesheet" href="foo" precedence="foo" />
2500 - {null}
2501 - <link rel="stylesheet" href="baz" precedence="baz" />
2502 - hello
2415 + <div>hello</div>
2416 </body>
2417 </html>,
2418 );
2506 - expect(Scheduler).toFlushWithoutYielding();
2507 - // The reason we do not see preloads in the head is they are inserted synchronously
2508 - // during render and then when the new singleton mounts it resets it's content, retaining only styles
2509 - expect(getMeaningfulChildren(document)).toEqual(
2510 - <html>
2511 - <head>
2512 - <link rel="stylesheet" href="foo" data-precedence="foo" />
2513 - <link rel="stylesheet" href="bar" data-precedence="bar" />
2514 - <link rel="stylesheet" href="baz" data-precedence="baz" />
2515 - <link rel="preload" href="baz" as="style" />
2516 - </head>
2517 - <body>hello</body>
2518 - </html>,
2519 - );
2419 + });
2420 +
2421 + // @gate enableFloat
2422 + it('warns if you do not pass in a valid href argument or options argument', async () => {
2423 + function App() {
2424 + ReactDOM.preload();
2425 + ReactDOM.preload('');
2426 + ReactDOM.preload('foo', null);
2427 + ReactDOM.preload('foo', {});
2428 + ReactDOM.preload('foo', {as: 'foo'});
2429 + return <div>foo</div>;
2430 + }
2431 +
2432 + await expect(async () => {
2433 + await act(() => {
2434 + renderToPipeableStream(<App />).pipe(writable);
2435 + });
2436 + }).toErrorDev([
2437 + 'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered `undefined` instead.',
2438 + 'ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered an empty string instead.',
2439 + 'ReactDOM.preload(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preloaded but encountered `null` instead.',
2440 + 'ReactDOM.preload(): Expected the `as` property in the `options` argument (second) to contain a string value describing the type of resource to be preloaded but encountered `undefined` instead. Values that are valid in for the `as` attribute of a `<link rel="preload" as="..." />` tag are valid here.',
2441 + ]);
2442 + });
2443 +
2444 + // @gate enableFloat
2445 + it('warns if you pass incompatible options to two `ReactDOM.preload(...)` with the same href', async () => {
2446 + function Component() {
2447 + ReactDOM.preload('foo', {
2448 + as: 'font',
2449 + crossOrigin: 'use-credentials',
2450 + });
2451 + ReactDOM.preload('foo', {
2452 + as: 'font',
2453 + integrity: 'some hash',
2454 + crossOrigin: 'anonymous',
2455 + });
2456 + ReactDOM.preload('foo', {
2457 + as: 'font',
2458 + extra: 'ignored',
2459 + });
2460 + }
2461 +
2462 + await expect(async () => {
2463 + await actIntoEmptyDocument(() => {
2464 + renderToPipeableStream(
2465 + <html>
2466 + <body>
2467 + <Component />
2468 + </body>
2469 + </html>,
2470 + );
2471 + });
2472 + }).toErrorDev([
2473 + 'Warning: ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("foo", { as: "font", ...})`. React will always use the options it first encounters when preloading a resource for a given `href` and `as` type, and any later options will be ignored if different. Try updating all calls to `ReactDOM.preload()` with the same `href` and `as` type to use the same options, or eliminate one of the calls.\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
2474 + 'Warning: ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("foo", { as: "font", ...})`. React will always use the options it first encounters when preloading a resource for a given `href` and `as` type, and any later options will be ignored if different. Try updating all calls to `ReactDOM.preload()` with the same `href` and `as` type to use the same options, or eliminate one of the calls.\n "crossOrigin" missing from options, original option value: "use-credentials"',
2475 + ]);
2476 + });
2477 +
2478 + // @gate enableFloat
2479 + it('warns if you pass incompatible options to two `ReactDOM.preload(...)` when an implicit preload already exists with the same href', async () => {
2480 + function Component() {
2481 + ReactDOM.preload('foo', {
2482 + as: 'style',
2483 + crossOrigin: 'use-credentials',
2484 + });
2485 + }
2486 +
2487 + await expect(async () => {
2488 + await actIntoEmptyDocument(() => {
2489 + renderToPipeableStream(
2490 + <html>
2491 + <body>
2492 + <link
2493 + rel="stylesheet"
2494 + href="foo"
2495 + integrity="some hash"
2496 + media="print"
2497 + />
2498 + <Component />
2499 + </body>
2500 + </html>,
2501 + );
2502 + });
2503 + }).toErrorDev([
2504 + 'ReactDOM.preload(): For `href` "foo", The options provided conflict with props on a matching <link rel="stylesheet" ... /> element. When the preload options disagree with the underlying resource it usually means the browser will not be able to use the preload when the resource is fetched, negating any benefit the preload would provide. React will preload the resource using props derived from the resource instead and ignore the options provided to the `ReactDOM.preload()` call. In general, preloading is useful when you expect to render a resource soon but have not yet done so. In this case since the underlying resource was already rendered the preload call may be extraneous. Try removing the call, otherwise try adjusting both the props on the <link rel="stylesheet" ... /> and the options passed to `ReactDOM.preload()` to agree.\n "integrity" missing from options, underlying prop value: "some hash"\n "media" missing from options, underlying prop value: "print"\n "crossOrigin" option value: "use-credentials", missing from underlying props',
2505 + ]);
2506 });
2507 });
2508
2523 - describe('script resources', () => {
2509 + describe('ReactDOM.preinit(href, { as: ... })', () => {
2510 // @gate enableFloat
2525 - it('treats async scripts without onLoad or onError as Resources', async () => {
2526 - await actIntoEmptyDocument(() => {
2527 - const {pipe} = renderToPipeableStream(
2511 + it('creates a stylesheet resource when ReactDOM.preinit(..., {as: "style" }) is called', async () => {
2512 + function App() {
2513 + ReactDOM.preinit('foo', {as: 'style'});
2514 + return (
2515 <html>
2529 - <head />
2516 <body>
2531 - <script src="foo" async={true} />
2532 - <script src="bar" async={true} onLoad={() => {}} />
2533 - <script src="baz" data-meaningful="" />
2534 - hello world
2517 + <Suspense fallback="loading...">
2518 + <BlockedOn value="bar">
2519 + <Component />
2520 + </BlockedOn>
2521 + </Suspense>
2522 </body>
2536 - </html>,
2523 + </html>
2524 );
2525 + }
2526 +
2527 + function Component() {
2528 + ReactDOM.preinit('bar', {as: 'style'});
2529 + return <div>hello</div>;
2530 + }
2531 +
2532 + await actIntoEmptyDocument(() => {
2533 + const {pipe} = renderToPipeableStream(<App />);
2534 pipe(writable);
2535 });
2540 - // The plain async script is converted to a resource and emitted as part of the shell
2541 - // The async script with onLoad is preloaded in the shell but is expecting to be added
2542 - // during hydration. This is novel, the script is NOT a HostResource but it also will
2543 - // never hydrate
2544 - // The regular script is just a normal html that should hydrate with a HostComponent
2536 expect(getMeaningfulChildren(document)).toEqual(
2537 <html>
2538 <head>
2548 - <script src="foo" async="" />
2549 - <link rel="preload" href="bar" as="script" />
2539 + <link rel="stylesheet" href="foo" data-precedence="default" />
2540 </head>
2551 - <body>
2552 - <script src="baz" data-meaningful="" />
2553 - hello world
2554 - </body>
2541 + <body>loading...</body>
2542 </html>,
2543 );
2544
2558 - ReactDOMClient.hydrateRoot(
2559 - document,
2545 + await act(() => {
2546 + resolveText('bar');
2547 + });
2548 + // The reason we do not see the "bar" stylesheet here is that ReactDOM.preinit is not about
2549 + // encoding a resource dependency but is a hint that a resource will be used in the near future.
2550 + // If we call preinit on the server after the shell has flushed the best we can do is emit a preload
2551 + // because any flushing suspense boundaries are not actually dependent on that resource and we don't
2552 + // want to delay reveal based on when that resource loads.
2553 + expect(getMeaningfulChildren(document)).toEqual(
2554 <html>
2561 - <head />
2555 + <head>
2556 + <link rel="stylesheet" href="foo" data-precedence="default" />
2557 + </head>
2558 <body>
2563 - <script src="foo" async={true} />
2564 - <script src="bar" async={true} onLoad={() => {}} />
2565 - <script src="baz" data-meaningful="" />
2566 - hello world
2559 + <div>hello</div>
2560 + <link rel="preload" href="bar" as="style" />
2561 </body>
2562 </html>,
2563 );
2564 +
2565 + function ClientApp() {
2566 + ReactDOM.preinit('bar', {as: 'style'});
2567 + return (
2568 + <html>
2569 + <body>
2570 + <Suspense fallback="loading...">
2571 + <div>hello</div>
2572 + </Suspense>
2573 + </body>
2574 + </html>
2575 + );
2576 + }
2577 +
2578 + ReactDOMClient.hydrateRoot(document, <ClientApp />);
2579 expect(Scheduler).toFlushWithoutYielding();
2571 - // The async script with onLoad is inserted in the right place but does not cause the hydration
2572 - // to fail.
2580 expect(getMeaningfulChildren(document)).toEqual(
2581 <html>
2582 <head>
2576 - <script src="foo" async="" />
2577 - <link rel="preload" href="bar" as="script" />
2583 + <link rel="stylesheet" href="foo" data-precedence="default" />
2584 + <link rel="stylesheet" href="bar" data-precedence="default" />
2585 </head>
2586 <body>
2580 - <script src="bar" async="" />
2581 - <script src="baz" data-meaningful="" />
2582 - hello world
2587 + <div>hello</div>
2588 + <link rel="preload" href="bar" as="style" />
2589 </body>
2590 </html>,
2591 );
2592 });
2587 - });
2593
2589 - // @gate enableFloat
2590 - it('client renders a boundary if a style Resource dependency fails to load', async () => {
2591 - function BlockedOn({text, children}) {
2592 - readText(text);
2593 - return children;
2594 - }
2595 - function App() {
2596 - return (
2594 + // @gate enableFloat
2595 + it('creates a preload resource when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
2596 + function App() {
2597 + React.useEffect(() => {
2598 + ReactDOM.preinit('foo', {as: 'style'});
2599 + }, []);
2600 + return (
2601 + <html>
2602 + <body>foo</body>
2603 + </html>
2604 + );
2605 + }
2606 +
2607 + const root = ReactDOMClient.createRoot(document);
2608 + root.render(<App />);
2609 + expect(Scheduler).toFlushWithoutYielding();
2610 + expect(getMeaningfulChildren(document)).toEqual(
2611 <html>
2598 - <head />
2599 - <body>
2600 - <Suspense fallback="loading...">
2601 - <BlockedOn text="unblock">
2602 - <link rel="stylesheet" href="foo" precedence="arbitrary" />
2603 - <link rel="stylesheet" href="bar" precedence="arbitrary" />
2604 - Hello
2605 - </BlockedOn>
2606 - </Suspense>
2607 - </body>
2608 - </html>
2612 + <head>
2613 + <link rel="preload" href="foo" as="style" />
2614 + </head>
2615 + <body>foo</body>
2616 + </html>,
2617 );
2610 - }
2611 - await actIntoEmptyDocument(() => {
2612 - const {pipe} = renderToPipeableStream(<App />);
2613 - pipe(writable);
2618 });
2619
2616 - await act(() => {
2617 - resolveText('unblock');
2618 - });
2620 + // @gate enableFloat
2621 + it('creates a script resource when ReactDOM.preinit(..., {as: "script" }) is called', async () => {
2622 + function App() {
2623 + ReactDOM.preinit('foo', {as: 'script'});
2624 + return (
2625 + <html>
2626 + <body>
2627 + <Suspense fallback="loading...">
2628 + <BlockedOn value="bar">
2629 + <Component />
2630 + </BlockedOn>
2631 + </Suspense>
2632 + </body>
2633 + </html>
2634 + );
2635 + }
2636
2620 - expect(getMeaningfulChildren(document)).toEqual(
2621 - <html>
2622 - <head>
2623 - <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2624 - <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
2625 - </head>
2626 - <body>
2627 - loading...
2628 - <link rel="preload" href="foo" as="style" />
2629 - <link rel="preload" href="bar" as="style" />
2630 - </body>
2631 - </html>,
2632 - );
2637 + function Component() {
2638 + ReactDOM.preinit('bar', {as: 'script'});
2639 + return <div>hello</div>;
2640 + }
2641
2634 - await act(() => {
2635 - const barLink = document.querySelector(
2636 - 'link[rel="stylesheet"][href="bar"]',
2642 + await actIntoEmptyDocument(() => {
2643 + const {pipe} = renderToPipeableStream(<App />);
2644 + pipe(writable);
2645 + });
2646 + expect(getMeaningfulChildren(document)).toEqual(
2647 + <html>
2648 + <head>
2649 + <script async="" src="foo" />
2650 + </head>
2651 + <body>loading...</body>
2652 + </html>,
2653 );
2638 - const event = document.createEvent('Events');
2639 - event.initEvent('error', true, true);
2640 - barLink.dispatchEvent(event);
2641 - });
2642 -
2643 - const boundaryTemplateInstance = document.getElementById('B:0');
2644 - const suspenseInstance = boundaryTemplateInstance.previousSibling;
2645 -
2646 - expect(suspenseInstance.data).toEqual('$!');
2647 - expect(boundaryTemplateInstance.dataset.dgst).toBe(
2648 - 'Resource failed to load',
2649 - );
2650 -
2651 - expect(getMeaningfulChildren(document)).toEqual(
2652 - <html>
2653 - <head>
2654 - <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2655 - <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
2656 - </head>
2657 - <body>
2658 - loading...
2659 - <link rel="preload" href="foo" as="style" />
2660 - <link rel="preload" href="bar" as="style" />
2661 - </body>
2662 - </html>,
2663 - );
2664 -
2665 - const errors = [];
2666 - ReactDOMClient.hydrateRoot(document, <App />, {
2667 - onRecoverableError(err, errInfo) {
2668 - errors.push(err.message);
2669 - errors.push(err.digest);
2670 - },
2671 - });
2672 - expect(Scheduler).toFlushWithoutYielding();
2673 - expect(getMeaningfulChildren(document)).toEqual(
2674 - <html>
2675 - <head>
2676 - <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2677 - <link rel="stylesheet" href="bar" data-precedence="arbitrary" />
2678 - </head>
2679 - <body>
2680 - <link rel="preload" href="foo" as="style" />
2681 - <link rel="preload" href="bar" as="style" />
2682 - Hello
2683 - </body>
2684 - </html>,
2685 - );
2686 - expect(errors).toEqual([
2687 - 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
2688 - 'Resource failed to load',
2689 - ]);
2690 - });
2654
2692 - // @gate enableFloat
2693 - it('treats stylesheet links with a precedence as a resource', async () => {
2694 - await actIntoEmptyDocument(() => {
2695 - const {pipe} = renderToPipeableStream(
2655 + await act(() => {
2656 + resolveText('bar');
2657 + });
2658 + expect(getMeaningfulChildren(document)).toEqual(
2659 <html>
2697 - <head />
2660 + <head>
2661 + <script async="" src="foo" />
2662 + </head>
2663 <body>
2699 - <link rel="stylesheet" href="foo" precedence="arbitrary" />
2700 - Hello
2664 + <div>hello</div>
2665 + <script async="" src="bar" />
2666 </body>
2702 - </html>,
2703 - );
2704 - pipe(writable);
2705 - });
2706 - expect(getMeaningfulChildren(document)).toEqual(
2707 - <html>
2708 - <head>
2709 - <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2710 - </head>
2711 - <body>Hello</body>
2712 - </html>,
2713 - );
2714 -
2715 - ReactDOMClient.hydrateRoot(
2716 - document,
2717 - <html>
2718 - <head />
2719 - <body>Hello</body>
2720 - </html>,
2721 - );
2722 - expect(Scheduler).toFlushWithoutYielding();
2723 - expect(getMeaningfulChildren(document)).toEqual(
2724 - <html>
2725 - <head>
2726 - <link rel="stylesheet" href="foo" data-precedence="arbitrary" />
2727 - </head>
2728 - <body>Hello</body>
2729 - </html>,
2730 - );
2731 - });
2732 -
2733 - // @gate enableFloat
2734 - it('inserts text separators following text when followed by an element that is converted to a resource and thus removed from the html inline', async () => {
2735 - // If you render many of these as siblings the values get emitted as a single text with no separator sometimes
2736 - // because the link gets elided as a resource
2737 - function AsyncTextWithResource({text, href, precedence}) {
2738 - const value = readText(text);
2739 - return (
2740 - <>
2741 - {value}
2742 - <link rel="stylesheet" href={href} precedence={precedence} />
2743 - </>
2667 + </html>,
2668 );
2745 - }
2669
2747 - await actIntoEmptyDocument(() => {
2748 - const {pipe} = renderToPipeableStream(
2670 + function ClientApp() {
2671 + ReactDOM.preinit('bar', {as: 'script'});
2672 + return (
2673 + <html>
2674 + <body>
2675 + <Suspense fallback="loading...">
2676 + <div>hello</div>
2677 + </Suspense>
2678 + </body>
2679 + </html>
2680 + );
2681 + }
2682 +
2683 + ReactDOMClient.hydrateRoot(document, <ClientApp />);
2684 + expect(Scheduler).toFlushWithoutYielding();
2685 + expect(getMeaningfulChildren(document)).toEqual(
2686 <html>
2750 - <head />
2687 + <head>
2688 + <script async="" src="foo" />
2689 + </head>
2690 <body>
2752 - <AsyncTextWithResource text="foo" href="foo" precedence="one" />
2753 - <AsyncTextWithResource text="bar" href="bar" precedence="two" />
2754 - <AsyncTextWithResource text="baz" href="baz" precedence="three" />
2691 + <div>hello</div>
2692 + <script async="" src="bar" />
2693 </body>
2694 </html>,
2695 );
2758 - pipe(writable);
2759 - resolveText('foo');
2760 - resolveText('bar');
2761 - resolveText('baz');
2696 });
2697
2764 - expect(getMeaningfulChildren(document)).toEqual(
2765 - <html>
2766 - <head>
2767 - <link rel="stylesheet" href="foo" data-precedence="one" />
2768 - <link rel="stylesheet" href="bar" data-precedence="two" />
2769 - <link rel="stylesheet" href="baz" data-precedence="three" />
2770 - </head>
2771 - <body>
2772 - {'foo'}
2773 - {'bar'}
2774 - {'baz'}
2775 - </body>
2776 - </html>,
2777 - );
2778 - });
2698 + // @gate enableFloat
2699 + it('creates a preload resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => {
2700 + function App() {
2701 + React.useEffect(() => {
2702 + ReactDOM.preinit('foo', {as: 'script'});
2703 + }, []);
2704 + return (
2705 + <html>
2706 + <body>foo</body>
2707 + </html>
2708 + );
2709 + }
2710
2780 - // @gate enableFloat
2781 - it('hoists late stylesheets the correct precedence', async () => {
2782 - function AsyncListItemWithResource({text, href, precedence, ...rest}) {
2783 - const value = readText(text);
2784 - return (
2785 - <li>
2786 - <link
2787 - rel="stylesheet"
2788 - href={href}
2789 - precedence={precedence}
2790 - {...rest}
2791 - />
2792 - {value}
2793 - </li>
2794 - );
2795 - }
2796 - function BlockingChildren({text, children}) {
2797 - readText(text);
2798 - return children;
2799 - }
2800 - function PresetPrecedence() {
2801 - ReactDOM.preinit('preset', {as: 'style', precedence: 'preset'});
2802 - }
2803 - await actIntoEmptyDocument(() => {
2804 - const {pipe} = renderToPipeableStream(
2711 + const root = ReactDOMClient.createRoot(document);
2712 + root.render(<App />);
2713 + expect(Scheduler).toFlushWithoutYielding();
2714 + expect(getMeaningfulChildren(document)).toEqual(
2715 <html>
2806 - <head />
2807 - <body>
2808 - <link rel="stylesheet" href="initial" precedence="one" />
2809 - <PresetPrecedence />
2810 - <div>
2811 - <Suspense fallback="loading foo bar...">
2812 - <link rel="stylesheet" href="foo" precedence="one" />
2813 - <ul>
2814 - <li>
2815 - <AsyncText text="foo" />
2816 - </li>
2817 - <AsyncListItemWithResource
2818 - text="bar"
2819 - href="bar"
2820 - precedence="default"
2821 - data-foo="foo"
2822 - crossOrigin="anonymous"
2823 - />
2824 - </ul>
2825 - </Suspense>
2826 - </div>
2827 - <div>
2828 - <Suspense fallback="loading bar baz qux...">
2829 - <ul>
2830 - <AsyncListItemWithResource
2831 - text="bar"
2832 - href="bar"
2833 - precedence="default"
2834 - />
2835 - <AsyncListItemWithResource
2836 - text="baz"
2837 - href="baz"
2838 - precedence="two"
2839 - />
2840 - <AsyncListItemWithResource
2841 - text="qux"
2842 - href="qux"
2843 - precedence="one"
2844 - />
2845 - </ul>
2846 - </Suspense>
2847 - </div>
2848 - <div>
2849 - <Suspense fallback="loading bar baz qux...">
2850 - <BlockingChildren text="unblock">
2851 - <ul>
2852 - <AsyncListItemWithResource
2853 - text="bar"
2854 - href="bar"
2855 - precedence="default"
2856 - />
2857 - <AsyncListItemWithResource
2858 - text="baz"
2859 - href="baz"
2860 - precedence="two"
2861 - />
2862 - <AsyncListItemWithResource
2863 - text="qux"
2864 - href="qux"
2865 - precedence="one"
2866 - />
2867 - </ul>
2868 - </BlockingChildren>
2869 - </Suspense>
2870 - </div>
2871 - </body>
2716 + <head>
2717 + <link rel="preload" href="foo" as="script" />
2718 + </head>
2719 + <body>foo</body>
2720 </html>,
2721 );
2874 - pipe(writable);
2722 });
2723
2877 - expect(getMeaningfulChildren(document)).toEqual(
2878 - <html>
2879 - <head>
2880 - <link rel="stylesheet" href="initial" data-precedence="one" />
2881 - <link rel="stylesheet" href="preset" data-precedence="preset" />
2882 - <link rel="preload" href="foo" as="style" />
2883 - </head>
2884 - <body>
2885 - <div>loading foo bar...</div>
2886 - <div>loading bar baz qux...</div>
2887 - <div>loading bar baz qux...</div>
2888 - </body>
2889 - </html>,
2890 - );
2724 + // @gate enableFloat
2725 + it('warns if you do not pass in a valid href argument or options argument', async () => {
2726 + function App() {
2727 + ReactDOM.preinit();
2728 + ReactDOM.preinit('');
2729 + ReactDOM.preinit('foo', null);
2730 + ReactDOM.preinit('foo', {});
2731 + ReactDOM.preinit('foo', {as: 'foo'});
2732 + return <div>foo</div>;
2733 + }
2734
2892 - await act(() => {
2893 - resolveText('foo');
2894 - resolveText('bar');
2735 + await expect(async () => {
2736 + await act(() => {
2737 + renderToPipeableStream(<App />).pipe(writable);
2738 + });
2739 + }).toErrorDev([
2740 + 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered `undefined` instead',
2741 + 'ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered an empty string instead',
2742 + 'ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered `null` instead',
2743 + 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered `undefined` instead. Valid values for `as` are "style" and "script".',
2744 + 'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered "foo" instead. Valid values for `as` are "style" and "script".',
2745 + ]);
2746 });
2747
2897 - expect(getMeaningfulChildren(document)).toEqual(
2898 - <html>
2899 - <head>
2900 - <link rel="stylesheet" href="initial" data-precedence="one" />
2901 - <link rel="stylesheet" href="foo" data-precedence="one" />
2902 - <link rel="stylesheet" href="preset" data-precedence="preset" />
2903 - <link
2904 - rel="stylesheet"
2905 - href="bar"
2906 - data-precedence="default"
2907 - data-foo="foo"
2908 - crossorigin="anonymous"
2909 - />
2910 - <link rel="preload" href="foo" as="style" />
2911 - </head>
2912 - <body>
2913 - <div>loading foo bar...</div>
2914 - <div>loading bar baz qux...</div>
2915 - <div>loading bar baz qux...</div>
2916 - <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
2917 - </body>
2918 - </html>,
2919 - );
2748 + // @gate enableFloat
2749 + it('warns if you pass options to `ReactDOM.preinit(..., { as: "style", ... })` incompatible with props from an existing <link rel="stylesheet" .../>', async () => {
2750 + function Component() {
2751 + ReactDOM.preinit('foo', {
2752 + as: 'style',
2753 + integrity: 'some hash',
2754 + crossOrigin: 'use-credentials',
2755 + });
2756 + }
2757
2921 - await act(() => {
2922 - const link = document.querySelector('link[rel="stylesheet"][href="foo"]');
2923 - const event = document.createEvent('Events');
2924 - event.initEvent('load', true, true);
2925 - link.dispatchEvent(event);
2758 + await expect(async () => {
2759 + await actIntoEmptyDocument(() => {
2760 + renderToPipeableStream(
2761 + <html>
2762 + <body>
2763 + <link
2764 + rel="stylesheet"
2765 + href="foo"
2766 + precedence="foo"
2767 + crossOrigin="anonymous"
2768 + />
2769 + <Component />
2770 + </body>
2771 + </html>,
2772 + );
2773 + });
2774 + }).toErrorDev([
2775 + 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with props found on a <link rel="stylesheet" precedence="foo" href="foo" .../> that was already rendered. React will always use the props or options it first encounters for a hoistable stylesheet for a given `href` and any later props or options will be ignored if different. Generally, ReactDOM.preinit() is useful when you are not yet rendering a stylesheet but you anticipate it will be used soon. In this case the stylesheet was already rendered so preinitializing it does not provide any additional benefit. To resolve, try making the props and options agree between the <link rel="stylesheet" .../> and the `ReactDOM.preinit()` call or remove the `ReactDOM.preinit()` call.\n "precedence" missing from options, prop value: "foo"\n "integrity" option value: "some hash", missing from props\n "crossOrigin" option value: "use-credentials", prop value: "anonymous"',
2776 + ]);
2777 });
2778
2928 - expect(getMeaningfulChildren(document)).toEqual(
2929 - <html>
2930 - <head>
2931 - <link rel="stylesheet" href="initial" data-precedence="one" />
2932 - <link rel="stylesheet" href="foo" data-precedence="one" />
2933 - <link rel="stylesheet" href="preset" data-precedence="preset" />
2934 - <link
2935 - rel="stylesheet"
2936 - href="bar"
2937 - data-precedence="default"
2938 - data-foo="foo"
2939 - crossorigin="anonymous"
2940 - />
2941 - <link rel="preload" href="foo" as="style" />
2942 - </head>
2943 - <body>
2944 - <div>loading foo bar...</div>
2945 - <div>loading bar baz qux...</div>
2946 - <div>loading bar baz qux...</div>
2947 - <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
2948 - </body>
2949 - </html>,
2950 - );
2779 + // @gate enableFloat
2780 + it('warns if you pass incompatible options to two `ReactDOM.preinit(..., { as: "style", ... })` with the same href', async () => {
2781 + function Component() {
2782 + ReactDOM.preinit('foo', {
2783 + as: 'style',
2784 + precedence: 'foo',
2785 + crossOrigin: 'use-credentials',
2786 + });
2787 + ReactDOM.preinit('foo', {
2788 + as: 'style',
2789 + integrity: 'some hash',
2790 + crossOrigin: 'anonymous',
2791 + });
2792 + }
2793
2952 - await act(() => {
2953 - const link = document.querySelector('link[rel="stylesheet"][href="bar"]');
2954 - const event = document.createEvent('Events');
2955 - event.initEvent('load', true, true);
2956 - link.dispatchEvent(event);
2794 + await expect(async () => {
2795 + await actIntoEmptyDocument(() => {
2796 + renderToPipeableStream(
2797 + <html>
2798 + <body>
2799 + <Component />
2800 + </body>
2801 + </html>,
2802 + );
2803 + });
2804 + }).toErrorDev([
2805 + 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with another call to `ReactDOM.preinit("foo", { as: "style", ... })`. React will always use the options it first encounters when preinitializing a hoistable stylesheet for a given `href` and any later options will be ignored if different. Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.\n "precedence" missing from options, original option value: "foo"\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
2806 + ]);
2807 });
2808
2959 - expect(getMeaningfulChildren(document)).toEqual(
2960 - <html>
2961 - <head>
2962 - <link rel="stylesheet" href="initial" data-precedence="one" />
2963 - <link rel="stylesheet" href="foo" data-precedence="one" />
2964 - <link rel="stylesheet" href="preset" data-precedence="preset" />
2965 - <link
2966 - rel="stylesheet"
2967 - href="bar"
2968 - data-precedence="default"
2969 - data-foo="foo"
2970 - crossorigin="anonymous"
2971 - />
2972 - <link rel="preload" href="foo" as="style" />
2973 - </head>
2974 - <body>
2975 - <div>
2976 - <ul>
2977 - <li>foo</li>
2978 - <li>bar</li>
2979 - </ul>
2980 - </div>
2981 - <div>loading bar baz qux...</div>
2982 - <div>loading bar baz qux...</div>
2983 - <link rel="preload" href="bar" as="style" crossorigin="anonymous" />
2984 - </body>
2985 - </html>,
2986 - );
2809 + // @gate enableFloat
2810 + it('warns if you pass options to `ReactDOM.preinit(..., { as: "script", ... })` incompatible with props from an existing <script async={true} .../>', async () => {
2811 + function Component() {
2812 + ReactDOM.preinit('foo', {
2813 + as: 'script',
2814 + integrity: 'some hash',
2815 + crossOrigin: 'use-credentials',
2816 + });
2817 + }
2818 +
2819 + await expect(async () => {
2820 + await actIntoEmptyDocument(() => {
2821 + renderToPipeableStream(
2822 + <html>
2823 + <body>
2824 + <script async={true} src="foo" crossOrigin="anonymous" />
2825 + <Component />
2826 + </body>
2827 + </html>,
2828 + );
2829 + });
2830 + }).toErrorDev([
2831 + 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with props found on a <script async={true} src="foo" .../> that was already rendered. React will always use the props or options it first encounters for a hoistable script for a given `href` and any later props or options will be ignored if different. Generally, ReactDOM.preinit() is useful when you are not yet rendering a script but you anticipate it will be used soon and want to go beyond preloading it and have it execute early. In this case the script was already rendered so preinitializing it does not provide any additional benefit. To resolve, try making the props and options agree between the <script .../> and the `ReactDOM.preinit()` call or remove the `ReactDOM.preinit()` call.\n "integrity" option value: "some hash", missing from props\n "crossOrigin" option value: "use-credentials", prop value: "anonymous"',
2832 + ]);
2833 + });
2834
2988 - await act(() => {
2989 - resolveText('baz');
2835 + // @gate enableFloat
2836 + it('warns if you pass incompatible options to two `ReactDOM.preinit(..., { as: "script", ... })` with the same href', async () => {
2837 + function Component() {
2838 + ReactDOM.preinit('foo', {
2839 + as: 'script',
2840 + crossOrigin: 'use-credentials',
2841 + });
2842 + ReactDOM.preinit('foo', {
2843 + as: 'script',
2844 + integrity: 'some hash',
2845 + crossOrigin: 'anonymous',
2846 + });
2847 + }
2848 +
2849 + await expect(async () => {
2850 + await actIntoEmptyDocument(() => {
2851 + renderToPipeableStream(
2852 + <html>
2853 + <body>
2854 + <Component />
2855 + </body>
2856 + </html>,
2857 + );
2858 + });
2859 + }).toErrorDev([
2860 + 'ReactDOM.preinit(): For `href` "foo", the options provided conflict with another call to `ReactDOM.preinit("foo", { as: "script", ... })`. React will always use the options it first encounters when preinitializing a hoistable script for a given `href` and any later options will be ignored if different. Try updating all calls to `ReactDOM.preinit()` for a given `href` to use the same options, or only call `ReactDOM.preinit()` once per `href`.\n "integrity" option value: "some hash", missing from original options\n "crossOrigin" option value: "anonymous", original option value: "use-credentials"',
2861 + ]);
2862 });
2863 + });
2864
2992 - expect(getMeaningfulChildren(document)).toEqual(
2993 - <html>
2994 - <head>
2995 - <link rel="stylesheet" href="initial" data-precedence="one" />
2996 - <link rel="stylesheet" href="foo" data-precedence="one" />
2997 - <link rel="stylesheet" href="preset" data-precedence="preset" />
2998 - <link
2999 - rel="stylesheet"
3000 - href="bar"
3001 - data-precedence="default"
3002 - data-foo="foo"
3003 - crossorigin="anonymous"
3004 - />
3005 - <link rel="preload" href="foo" as="style" />
3006 - </head>
3007 - <body>
3008 - <div>
3009 - <ul>
3010 - <li>foo</li>
3011 - <li>bar</li>
3012 - </ul>
3013 - </div>
3014 - <div>loading bar baz qux...</div>
3015 - <div>loading bar baz qux...</div>
3016 - <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3017 - <link rel="preload" as="style" href="baz" />
3018 - </body>
3019 - </html>,
3020 - );
2865 + describe('Stylesheet Resources', () => {
2866 + // @gate enableFloat
2867 + it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when server rendering', async () => {
2868 + await actIntoEmptyDocument(() => {
2869 + const {pipe} = renderToPipeableStream(
2870 + <html>
2871 + <head />
2872 + <body>
2873 + <link rel="stylesheet" href="aresource" precedence="foo" />
2874 + <div>hello world</div>
2875 + </body>
2876 + </html>,
2877 + );
2878 + pipe(writable);
2879 + });
2880
3022 - await act(() => {
3023 - resolveText('qux');
2881 + expect(getMeaningfulChildren(document)).toEqual(
2882 + <html>
2883 + <head>
2884 + <link rel="stylesheet" href="aresource" data-precedence="foo" />
2885 + </head>
2886 + <body>
2887 + <div>hello world</div>
2888 + </body>
2889 + </html>,
2890 + );
2891 });
2892
3026 - expect(getMeaningfulChildren(document)).toEqual(
3027 - <html>
3028 - <head>
3029 - <link rel="stylesheet" href="initial" data-precedence="one" />
3030 - <link rel="stylesheet" href="foo" data-precedence="one" />
3031 - <link rel="stylesheet" href="qux" data-precedence="one" />
3032 - <link rel="stylesheet" href="preset" data-precedence="preset" />
3033 - <link
3034 - rel="stylesheet"
3035 - href="bar"
3036 - data-precedence="default"
3037 - data-foo="foo"
3038 - crossorigin="anonymous"
3039 - />
3040 - <link rel="stylesheet" href="baz" data-precedence="two" />
3041 - <link rel="preload" href="foo" as="style" />
3042 - </head>
3043 - <body>
3044 - <div>
3045 - <ul>
3046 - <li>foo</li>
3047 - <li>bar</li>
3048 - </ul>
3049 - </div>
3050 - <div>loading bar baz qux...</div>
3051 - <div>loading bar baz qux...</div>
3052 - <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3053 - <link rel="preload" as="style" href="baz" />
3054 - <link rel="preload" as="style" href="qux" />
3055 - </body>
3056 - </html>,
3057 - );
2893 + // @gate enableFloat
2894 + it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when client rendering', async () => {
2895 + const root = ReactDOMClient.createRoot(document);
2896 + root.render(
2897 + <html>
2898 + <head />
2899 + <body>
2900 + <link rel="stylesheet" href="aresource" precedence="foo" />
2901 + <div>hello world</div>
2902 + </body>
2903 + </html>,
2904 + );
2905 + expect(Scheduler).toFlushWithoutYielding();
2906
3059 - await act(() => {
3060 - const bazlink = document.querySelector(
3061 - 'link[rel="stylesheet"][href="baz"]',
2907 + expect(getMeaningfulChildren(document)).toEqual(
2908 + <html>
2909 + <head>
2910 + <link rel="stylesheet" href="aresource" data-precedence="foo" />
2911 + </head>
2912 + <body>
2913 + <div>hello world</div>
2914 + </body>
2915 + </html>,
2916 );
3063 - const quxlink = document.querySelector(
3064 - 'link[rel="stylesheet"][href="qux"]',
2917 + });
2918 +
2919 + // @gate enableFloat
2920 + it('treats link rel stylesheet elements as a stylesheet resource when it includes a precedence when hydrating', async () => {
2921 + await actIntoEmptyDocument(() => {
2922 + const {pipe} = renderToPipeableStream(
2923 + <html>
2924 + <head />
2925 + <body>
2926 + <link rel="stylesheet" href="aresource" precedence="foo" />
2927 + <div>hello world</div>
2928 + </body>
2929 + </html>,
2930 + );
2931 + pipe(writable);
2932 + });
2933 + ReactDOMClient.hydrateRoot(
2934 + document,
2935 + <html>
2936 + <head />
2937 + <body>
2938 + <link rel="stylesheet" href="aresource" precedence="foo" />
2939 + <div>hello world</div>
2940 + </body>
2941 + </html>,
2942 );
3066 - const presetLink = document.querySelector(
3067 - 'link[rel="stylesheet"][href="preset"]',
2943 + expect(Scheduler).toFlushWithoutYielding();
2944 +
2945 + expect(getMeaningfulChildren(document)).toEqual(
2946 + <html>
2947 + <head>
2948 + <link rel="stylesheet" href="aresource" data-precedence="foo" />
2949 + </head>
2950 + <body>
2951 + <div>hello world</div>
2952 + </body>
2953 + </html>,
2954 );
3069 - const event = document.createEvent('Events');
3070 - event.initEvent('load', true, true);
3071 - bazlink.dispatchEvent(event);
3072 - quxlink.dispatchEvent(event);
3073 - presetLink.dispatchEvent(event);
2955 });
2956
3076 - expect(getMeaningfulChildren(document)).toEqual(
3077 - <html>
3078 - <head>
3079 - <link rel="stylesheet" href="initial" data-precedence="one" />
3080 - <link rel="stylesheet" href="foo" data-precedence="one" />
3081 - <link rel="stylesheet" href="qux" data-precedence="one" />
3082 - <link rel="stylesheet" href="preset" data-precedence="preset" />
3083 - <link
3084 - rel="stylesheet"
3085 - href="bar"
3086 - data-precedence="default"
3087 - data-foo="foo"
3088 - crossorigin="anonymous"
3089 - />
3090 - <link rel="stylesheet" href="baz" data-precedence="two" />
3091 - <link rel="preload" href="foo" as="style" />
3092 - </head>
3093 - <body>
3094 - <div>
3095 - <ul>
3096 - <li>foo</li>
3097 - <li>bar</li>
3098 - </ul>
3099 - </div>
3100 - <div>
3101 - <ul>
3102 - <li>bar</li>
3103 - <li>baz</li>
3104 - <li>qux</li>
3105 - </ul>
3106 - </div>
3107 - <div>loading bar baz qux...</div>
3108 - <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3109 - <link rel="preload" as="style" href="baz" />
3110 - <link rel="preload" as="style" href="qux" />
3111 - </body>
3112 - </html>,
3113 - );
2957 + // @gate enableFloat
2958 + it('preloads stylesheets without a precedence prop when server rendering', async () => {
2959 + await actIntoEmptyDocument(() => {
2960 + const {pipe} = renderToPipeableStream(
2961 + <html>
2962 + <head />
2963 + <body>
2964 + <link rel="stylesheet" href="notaresource" />
2965 + <div>hello world</div>
2966 + </body>
2967 + </html>,
2968 + );
2969 + pipe(writable);
2970 + });
2971
3115 - await act(() => {
3116 - resolveText('unblock');
2972 + expect(getMeaningfulChildren(document)).toEqual(
2973 + <html>
2974 + <head>
2975 + <link rel="preload" as="style" href="notaresource" />
2976 + </head>
2977 + <body>
2978 + <link rel="stylesheet" href="notaresource" />
2979 + <div>hello world</div>
2980 + </body>
2981 + </html>,
2982 + );
2983 });
2984
3119 - expect(getMeaningfulChildren(document)).toEqual(
3120 - <html>
3121 - <head>
3122 - <link rel="stylesheet" href="initial" data-precedence="one" />
3123 - <link rel="stylesheet" href="foo" data-precedence="one" />
3124 - <link rel="stylesheet" href="qux" data-precedence="one" />
3125 - <link rel="stylesheet" href="preset" data-precedence="preset" />
3126 - <link
3127 - rel="stylesheet"
3128 - href="bar"
3129 - data-precedence="default"
3130 - data-foo="foo"
3131 - crossorigin="anonymous"
3132 - />
3133 - <link rel="stylesheet" href="baz" data-precedence="two" />
3134 - <link rel="preload" href="foo" as="style" />
3135 - </head>
3136 - <body>
3137 - <div>
3138 - <ul>
3139 - <li>foo</li>
3140 - <li>bar</li>
3141 - </ul>
3142 - </div>
3143 - <div>
3144 - <ul>
3145 - <li>bar</li>
3146 - <li>baz</li>
3147 - <li>qux</li>
3148 - </ul>
3149 - </div>
3150 - <div>
3151 - <ul>
3152 - <li>bar</li>
3153 - <li>baz</li>
3154 - <li>qux</li>
3155 - </ul>
3156 - </div>
3157 - <link rel="preload" as="style" href="bar" crossorigin="anonymous" />
3158 - <link rel="preload" as="style" href="baz" />
3159 - <link rel="preload" as="style" href="qux" />
3160 - </body>
3161 - </html>,
3162 - );
3163 - });
2985 + // @gate enableFloat
2986 + it('hoists stylesheet resources to the correct precedence', async () => {
2987 + await actIntoEmptyDocument(() => {
2988 + const {pipe} = renderToPipeableStream(
2989 + <html>
2990 + <head />
2991 + <body>
2992 + <link rel="stylesheet" href="foo1" precedence="foo" />
2993 + <link rel="stylesheet" href="default1" precedence="default" />
2994 + <link rel="stylesheet" href="foo2" precedence="foo" />
2995 + <div>hello world</div>
2996 + </body>
2997 + </html>,
2998 + );
2999 + pipe(writable);
3000 + });
3001 + expect(getMeaningfulChildren(document)).toEqual(
3002 + <html>
3003 + <head>
3004 + <link rel="stylesheet" href="foo1" data-precedence="foo" />
3005 + <link rel="stylesheet" href="foo2" data-precedence="foo" />
3006 + <link rel="stylesheet" href="default1" data-precedence="default" />
3007 + </head>
3008 + <body>
3009 + <div>hello world</div>
3010 + </body>
3011 + </html>,
3012 + );
3013
3165 - // @gate enableFloat
3166 - it('normalizes style resource precedence for all boundaries inlined as part of the shell flush', async () => {
3167 - await actIntoEmptyDocument(() => {
3168 - const {pipe} = renderToPipeableStream(
3014 + ReactDOMClient.hydrateRoot(
3015 + document,
3016 <html>
3017 <head />
3018 <body>
3172 - <div>
3173 - outer
3174 - <link rel="stylesheet" href="1one" precedence="one" />
3175 - <link rel="stylesheet" href="1two" precedence="two" />
3176 - <link rel="stylesheet" href="1three" precedence="three" />
3177 - <link rel="stylesheet" href="1four" precedence="four" />
3178 - <Suspense fallback={null}>
3179 - <div>
3180 - middle
3181 - <link rel="stylesheet" href="2one" precedence="one" />
3182 - <link rel="stylesheet" href="2two" precedence="two" />
3183 - <link rel="stylesheet" href="2three" precedence="three" />
3184 - <link rel="stylesheet" href="2four" precedence="four" />
3185 - <Suspense fallback={null}>
3186 - <div>
3187 - inner
3188 - <link rel="stylesheet" href="3five" precedence="five" />
3189 - <link rel="stylesheet" href="3one" precedence="one" />
3190 - <link rel="stylesheet" href="3two" precedence="two" />
3191 - <link rel="stylesheet" href="3three" precedence="three" />
3192 - <link rel="stylesheet" href="3four" precedence="four" />
3193 - </div>
3194 - </Suspense>
3195 - </div>
3196 - </Suspense>
3197 - <Suspense fallback={null}>
3198 - <div>middle</div>
3199 - <link rel="stylesheet" href="4one" precedence="one" />
3200 - <link rel="stylesheet" href="4two" precedence="two" />
3201 - <link rel="stylesheet" href="4three" precedence="three" />
3202 - <link rel="stylesheet" href="4four" precedence="four" />
3203 - </Suspense>
3204 - </div>
3019 + <link rel="stylesheet" href="bar1" precedence="bar" />
3020 + <link rel="stylesheet" href="foo3" precedence="foo" />
3021 + <link rel="stylesheet" href="default2" precedence="default" />
3022 + <div>hello world</div>
3023 + </body>
3024 + </html>,
3025 + );
3026 + expect(Scheduler).toFlushWithoutYielding();
3027 + expect(getMeaningfulChildren(document)).toEqual(
3028 + <html>
3029 + <head>
3030 + <link rel="stylesheet" href="foo1" data-precedence="foo" />
3031 + <link rel="stylesheet" href="foo2" data-precedence="foo" />
3032 + <link rel="stylesheet" href="foo3" data-precedence="foo" />
3033 + <link rel="stylesheet" href="default1" data-precedence="default" />
3034 + <link rel="stylesheet" href="default2" data-precedence="default" />
3035 + <link rel="stylesheet" href="bar1" data-precedence="bar" />
3036 + <link rel="preload" as="style" href="bar1" />
3037 + <link rel="preload" as="style" href="foo3" />
3038 + <link rel="preload" as="style" href="default2" />
3039 + </head>
3040 + <body>
3041 + <div>hello world</div>
3042 </body>
3043 </html>,
3044 );
3208 - pipe(writable);
3045 });
3046
3211 - // The reason the href's aren't ordered linearly is that when boundaries complete their resources
3212 - // get hoisted to the shell directly so they can flush in the head. If a boundary doesn't suspend then
3213 - // child boundaries will complete before the parent boundary and thus have their resources hoist
3214 - // early. The reason precedences are still ordered correctly between child and parent is because
3215 - // the precedence ordering is determined upon first discovernig a resource rather than on hoist and
3216 - // so it follows render order
3217 - expect(getMeaningfulChildren(document)).toEqual(
3218 - <html>
3219 - <head>
3220 - <link rel="stylesheet" href="1one" data-precedence="one" />
3221 - <link rel="stylesheet" href="3one" data-precedence="one" />
3222 - <link rel="stylesheet" href="2one" data-precedence="one" />
3223 - <link rel="stylesheet" href="4one" data-precedence="one" />
3047 + // @gate enableFloat
3048 + it('retains styles even after the last referring Resource unmounts', async () => {
3049 + // This test is true until a future update where there is some form of garbage collection.
3050 + const root = ReactDOMClient.createRoot(document);
3051
3225 - <link rel="stylesheet" href="1two" data-precedence="two" />
3226 - <link rel="stylesheet" href="3two" data-precedence="two" />
3227 - <link rel="stylesheet" href="2two" data-precedence="two" />
3228 - <link rel="stylesheet" href="4two" data-precedence="two" />
3052 + root.render(
3053 + <html>
3054 + <head />
3055 + <body>
3056 + hello world
3057 + <link rel="stylesheet" href="foo" precedence="foo" />
3058 + </body>
3059 + </html>,
3060 + );
3061 + expect(Scheduler).toFlushWithoutYielding();
3062
3230 - <link rel="stylesheet" href="1three" data-precedence="three" />
3231 - <link rel="stylesheet" href="3three" data-precedence="three" />
3232 - <link rel="stylesheet" href="2three" data-precedence="three" />
3233 - <link rel="stylesheet" href="4three" data-precedence="three" />
3063 + root.render(
3064 + <html>
3065 + <head />
3066 + <body>hello world</body>
3067 + </html>,
3068 + );
3069 + expect(Scheduler).toFlushWithoutYielding();
3070 + expect(getMeaningfulChildren(document)).toEqual(
3071 + <html>
3072 + <head>
3073 + <link rel="stylesheet" href="foo" data-precedence="foo" />
3074 + </head>
3075 + <body>hello world</body>
3076 + </html>,
3077 + );
3078 + });
3079
3235 - <link rel="stylesheet" href="1four" data-precedence="four" />
3236 - <link rel="stylesheet" href="3four" data-precedence="four" />
3237 - <link rel="stylesheet" href="2four" data-precedence="four" />
3238 - <link rel="stylesheet" href="4four" data-precedence="four" />
3080 + // @gate enableFloat && enableHostSingletons && enableClientRenderFallbackOnTextMismatch
3081 + it('retains styles even when a new html, head, and/body mount', async () => {
3082 + await actIntoEmptyDocument(() => {
3083 + const {pipe} = renderToPipeableStream(
3084 + <html>
3085 + <head />
3086 + <body>
3087 + <link rel="stylesheet" href="foo" precedence="foo" />
3088 + <link rel="stylesheet" href="bar" precedence="bar" />
3089 + server
3090 + </body>
3091 + </html>,
3092 + );
3093 + pipe(writable);
3094 + });
3095 + const errors = [];
3096 + ReactDOMClient.hydrateRoot(
3097 + document,
3098 + <html>
3099 + <head>
3100 + <link rel="stylesheet" href="qux" precedence="qux" />
3101 + <link rel="stylesheet" href="foo" precedence="foo" />
3102 + </head>
3103 + <body>client</body>
3104 + </html>,
3105 + {
3106 + onRecoverableError(error) {
3107 + errors.push(error.message);
3108 + },
3109 + },
3110 + );
3111 + expect(() => {
3112 + expect(Scheduler).toFlushWithoutYielding();
3113 + }).toErrorDev(
3114 + [
3115 + 'Warning: Text content did not match. Server: "server" Client: "client"',

This file is too large to show in full.

packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+14 -9
@@ -342,16 +342,21 @@ describe('ReactDOMRoot', () => {
342 });
343
344 it('warns if creating a root on the document.body', async () => {
345 - expect(() => {
345 + if (gate(flags => flags.enableFloat)) {
346 + // we no longer expect an error for this if float is enabled
347 ReactDOMClient.createRoot(document.body);
347 - }).toErrorDev(
348 - 'createRoot(): Creating roots directly with document.body is ' +
349 - 'discouraged, since its children are often manipulated by third-party ' +
350 - 'scripts and browser extensions. This may lead to subtle ' +
351 - 'reconciliation issues. Try using a container element created ' +
352 - 'for your app.',
353 - {withoutStack: true},
354 - );
348 + } else {
349 + expect(() => {
350 + ReactDOMClient.createRoot(document.body);
351 + }).toErrorDev(
352 + 'createRoot(): Creating roots directly with document.body is ' +
353 + 'discouraged, since its children are often manipulated by third-party ' +
354 + 'scripts and browser extensions. This may lead to subtle ' +
355 + 'reconciliation issues. Try using a container element created ' +
356 + 'for your app.',
357 + {withoutStack: true},
358 + );
359 + }
360 });
361
362 it('warns if updating a root that has had its contents removed', async () => {
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
+2 -8
@@ -333,6 +333,7 @@ describe('ReactDOM HostSingleton', () => {
333 <link rel="stylesheet" href="3rdparty" />
334 <link rel="stylesheet" href="3rdparty2" />
335 <title>a client title</title>
336 + <meta />
337 </head>
338 <body data-client-baz="baz">
339 <style>
@@ -828,14 +829,7 @@ describe('ReactDOM HostSingleton', () => {
829 });
830 container = document.body;
831
831 - let root;
832 - // Given our new capabilities to render "safely" into the body we should consider removing this warning
833 - expect(() => {
834 - root = ReactDOMClient.createRoot(container);
835 - }).toErrorDev(
836 - 'Warning: createRoot(): Creating roots directly with document.body is discouraged, since its children are often manipulated by third-party scripts and browser extensions. This may lead to subtle reconciliation issues. Try using a container element created for your app.',
837 - {withoutStack: true},
838 - );
832 + const root = ReactDOMClient.createRoot(container);
833 root.render(<div>something new</div>);
834 expect(Scheduler).toFlushWithoutYielding();
835 expect(getVisibleChildren(document)).toEqual(
packages/react-dom/src/__tests__/react-dom-server-rendering-stub-test.js
+1 -1
@@ -49,7 +49,7 @@ describe('react-dom-server-rendering-stub', () => {
49 }
50 const html = ReactDOMFizzServer.renderToString(<App />);
51 expect(html).toEqual(
52 - '<link rel="stylesheet" href="bar" data-precedence="default"/><link href="foo" rel="preload" as="style"/><div>foo</div>',
52 + '<link rel="stylesheet" href="bar" data-precedence="default"/><link rel="preload" as="style" href="foo"/><div>foo</div>',
53 );
54 });
55
packages/react-dom/src/client/ReactDOMRoot.js
+1
@@ -385,6 +385,7 @@ export function isValidContainerLegacy(node: any): boolean {
385 function warnIfReactDOMContainerInDEV(container: any) {
386 if (__DEV__) {
387 if (
388 + !enableHostSingletons &&
389 container.nodeType === ELEMENT_NODE &&
390 ((container: any): Element).tagName &&
391 ((container: any): Element).tagName.toUpperCase() === 'BODY'
packages/react-dom/src/test-utils/ReactTestUtils.js
+2 -2
@@ -15,7 +15,7 @@ import {
15 ClassComponent,
16 FunctionComponent,
17 HostComponent,
18 - HostResource,
18 + HostHoistable,
19 HostSingleton,
20 HostText,
21 } from 'react-reconciler/src/ReactWorkTags';
@@ -65,7 +65,7 @@ function findAllInRenderedFiberTreeInternal(fiber, test) {
65 node.tag === HostText ||
66 node.tag === ClassComponent ||
67 node.tag === FunctionComponent ||
68 - (enableFloat ? node.tag === HostResource : false) ||
68 + (enableFloat ? node.tag === HostHoistable : false) ||
69 (enableHostSingletons ? node.tag === HostSingleton : false)
70 ) {
71 const publicInst = node.stateNode;
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+19 -10
@@ -140,9 +140,9 @@ export function pushTextInstance(
140
141 export function pushStartInstance(
142 target: Array<Chunk | PrecomputedChunk>,
143 - preamble: Array<Chunk | PrecomputedChunk>,
143 type: string,
144 props: Object,
145 + resources: Resources,
146 responseState: ResponseState,
147 formatContext: FormatContext,
148 textEmbedded: boolean,
@@ -158,9 +158,10 @@ export function pushStartInstance(
158
159 export function pushEndInstance(
160 target: Array<Chunk | PrecomputedChunk>,
161 - postamble: Array<Chunk | PrecomputedChunk>,
161 type: string,
162 props: Object,
163 + responseState: ResponseState,
164 + formatContext: FormatContext,
165 ): void {
166 target.push(END);
167 }
@@ -315,22 +316,23 @@ export function writeClientRenderBoundaryInstruction(
316 return writeChunkAndReturn(destination, formatID(boundaryID));
317 }
318
318 -export function writeInitialResources(
319 +export function writePreamble(
320 destination: Destination,
321 resources: Resources,
322 responseState: ResponseState,
323 willFlushAllSegments: boolean,
323 -): boolean {
324 - return true;
325 -}
324 +) {}
325
327 -export function writeImmediateResources(
326 +export function writeHoistables(
327 destination: Destination,
328 resources: Resources,
329 responseState: ResponseState,
331 -): boolean {
332 - return true;
333 -}
330 +) {}
331 +
332 +export function writePostamble(
333 + destination: Destination,
334 + responseState: ResponseState,
335 +) {}
336
337 export function hoistResources(
338 resources: Resources,
@@ -350,3 +352,10 @@ export function setCurrentlyRenderingBoundaryResourcesTarget(
352 resources: Resources,
353 boundaryResources: ?BoundaryResources,
354 ) {}
355 +
356 +export function writeResourcesForBoundary(
357 + destination: Destination,
358 + boundaryResources: BoundaryResources,
359 +): boolean {
360 + return true;
361 +}
packages/react-noop-renderer/src/ReactNoopServer.js
+3 -4
@@ -116,7 +116,6 @@ const ReactNoopServer = ReactFizzServer({
116 },
117 pushStartInstance(
118 target: Array<Uint8Array>,
119 - preamble: Array<Uint8Array>,
119 type: string,
120 props: Object,
121 ): ReactNodeList {
@@ -132,7 +131,6 @@ const ReactNoopServer = ReactFizzServer({
131
132 pushEndInstance(
133 target: Array<Uint8Array>,
135 - postamble: Array<Uint8Array>,
134 type: string,
135 props: Object,
136 ): void {
@@ -267,8 +265,9 @@ const ReactNoopServer = ReactFizzServer({
265 boundary.status = 'client-render';
266 },
267
270 - writeInitialResources() {},
271 - writeImmediateResources() {},
268 + writePreamble() {},
269 + writeHoistables() {},
270 + writePostamble() {},
271
272 createResources(): Resources {
273 return null;
packages/react-reconciler/src/ReactFiber.js
+6 -6
@@ -24,7 +24,7 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
24 import {
25 supportsResources,
26 supportsSingletons,
27 - isHostResourceType,
27 + isHostHoistableType,
28 isHostSingletonType,
29 } from './ReactFiberHostConfig';
30 import {
@@ -49,7 +49,7 @@ import {
49 HostComponent,
50 HostText,
51 HostPortal,
52 - HostResource,
52 + HostHoistable,
53 HostSingleton,
54 ForwardRef,
55 Fragment,
@@ -511,15 +511,15 @@ export function createFiberFromTypeAndProps(
511 supportsSingletons
512 ) {
513 const hostContext = getHostContext();
514 - fiberTag = isHostResourceType(type, pendingProps, hostContext)
515 - ? HostResource
514 + fiberTag = isHostHoistableType(type, pendingProps, hostContext)
515 + ? HostHoistable
516 : isHostSingletonType(type)
517 ? HostSingleton
518 : HostComponent;
519 } else if (enableFloat && supportsResources) {
520 const hostContext = getHostContext();
521 - fiberTag = isHostResourceType(type, pendingProps, hostContext)
522 - ? HostResource
521 + fiberTag = isHostHoistableType(type, pendingProps, hostContext)
522 + ? HostHoistable
523 : HostComponent;
524 } else if (enableHostSingletons && supportsSingletons) {
525 fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent;
packages/react-reconciler/src/ReactFiberBeginWork.js
+25 -10
@@ -51,7 +51,7 @@ import {
51 ClassComponent,
52 HostRoot,
53 HostComponent,
54 - HostResource,
54 + HostHoistable,
55 HostSingleton,
56 HostText,
57 HostPortal,
@@ -167,10 +167,15 @@ import {
167 supportsSingletons,
168 isPrimaryRenderer,
169 getResource,
170 + createHoistableInstance,
171 } from './ReactFiberHostConfig';
172 import type {SuspenseInstance} from './ReactFiberHostConfig';
173 import {shouldError, shouldSuspend} from './ReactFiberReconciler';
173 -import {pushHostContext, pushHostContainer} from './ReactFiberHostContext';
174 +import {
175 + pushHostContext,
176 + pushHostContainer,
177 + getRootHostContainer,
178 +} from './ReactFiberHostContext';
179 import {
180 suspenseStackCursor,
181 pushSuspenseListContext,
@@ -1624,19 +1629,30 @@ function updateHostComponent(
1629 return workInProgress.child;
1630 }
1631
1627 -function updateHostResource(
1632 +function updateHostHoistable(
1633 current: null | Fiber,
1634 workInProgress: Fiber,
1635 renderLanes: Lanes,
1636 ) {
1632 - pushHostContext(workInProgress);
1637 markRef(current, workInProgress);
1638 const currentProps = current === null ? null : current.memoizedProps;
1635 - workInProgress.memoizedState = getResource(
1639 + const resource = (workInProgress.memoizedState = getResource(
1640 workInProgress.type,
1637 - workInProgress.pendingProps,
1641 currentProps,
1639 - );
1642 + workInProgress.pendingProps,
1643 + ));
1644 + if (current === null) {
1645 + if (!getIsHydrating() && resource === null) {
1646 + // This is not a Resource Hoistable and we aren't hydrating so we construct the instance.
1647 + workInProgress.stateNode = createHoistableInstance(
1648 + workInProgress.type,
1649 + workInProgress.pendingProps,
1650 + getRootHostContainer(),
1651 + workInProgress,
1652 + );
1653 + }
1654 + }
1655 +
1656 // Resources never have reconciler managed children. It is possible for
1657 // the host implementation of getResource to consider children in the
1658 // resource construction but they will otherwise be discarded. In practice
@@ -3742,7 +3758,6 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3758 }
3759 resetHydrationState();
3760 break;
3745 - case HostResource:
3761 case HostSingleton:
3762 case HostComponent:
3763 pushHostContext(workInProgress);
@@ -4078,9 +4093,9 @@ function beginWork(
4093 }
4094 case HostRoot:
4095 return updateHostRoot(current, workInProgress, renderLanes);
4081 - case HostResource:
4096 + case HostHoistable:
4097 if (enableFloat && supportsResources) {
4083 - return updateHostResource(current, workInProgress, renderLanes);
4098 + return updateHostHoistable(current, workInProgress, renderLanes);
4099 }
4100 // eslint-disable-next-line no-fallthrough
4101 case HostSingleton:
packages/react-reconciler/src/ReactFiberCommitWork.js
+119 -22
@@ -14,6 +14,7 @@ import type {
14 Container,
15 ChildSet,
16 UpdatePayload,
17 + HoistableRoot,
18 } from './ReactFiberHostConfig';
19 import type {Fiber, FiberRoot} from './ReactInternalTypes';
20 import type {Lanes} from './ReactFiberLane';
@@ -60,7 +61,7 @@ import {
61 ClassComponent,
62 HostRoot,
63 HostComponent,
63 - HostResource,
64 + HostHoistable,
65 HostSingleton,
66 HostText,
67 HostPortal,
@@ -147,11 +148,15 @@ import {
148 prepareForCommit,
149 beforeActiveInstanceBlur,
150 detachDeletedInstance,
150 - acquireResource,
151 - releaseResource,
151 clearSingleton,
152 acquireSingletonInstance,
153 releaseSingletonInstance,
154 + getHoistableRoot,
155 + acquireResource,
156 + releaseResource,
157 + hydrateHoistable,
158 + mountHoistable,
159 + unmountHoistable,
160 } from './ReactFiberHostConfig';
161 import {
162 captureCommitPhaseError,
@@ -536,7 +541,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
541 break;
542 }
543 case HostComponent:
539 - case HostResource:
544 + case HostHoistable:
545 case HostSingleton:
546 case HostText:
547 case HostPortal:
@@ -1109,7 +1114,7 @@ function commitLayoutEffectOnFiber(
1114 }
1115 break;
1116 }
1112 - case HostResource: {
1117 + case HostHoistable: {
1118 if (enableFloat && supportsResources) {
1119 recursivelyTraverseLayoutEffects(
1120 finishedRoot,
@@ -1515,7 +1520,7 @@ function hideOrUnhideAllChildren(finishedWork: Fiber, isHidden: boolean) {
1520 if (
1521 node.tag === HostComponent ||
1522 (enableFloat && supportsResources
1518 - ? node.tag === HostResource
1523 + ? node.tag === HostHoistable
1524 : false) ||
1525 (enableHostSingletons && supportsSingletons
1526 ? node.tag === HostSingleton
@@ -1592,7 +1597,7 @@ function commitAttachRef(finishedWork: Fiber) {
1597 const instance = finishedWork.stateNode;
1598 let instanceToUse;
1599 switch (finishedWork.tag) {
1595 - case HostResource:
1600 + case HostHoistable:
1601 case HostSingleton:
1602 case HostComponent:
1603 instanceToUse = getPublicInstance(instance);
@@ -1736,7 +1741,7 @@ function isHostParent(fiber: Fiber): boolean {
1741 return (
1742 fiber.tag === HostComponent ||
1743 fiber.tag === HostRoot ||
1739 - (enableFloat && supportsResources ? fiber.tag === HostResource : false) ||
1744 + (enableFloat && supportsResources ? fiber.tag === HostHoistable : false) ||
1745 (enableHostSingletons && supportsSingletons
1746 ? fiber.tag === HostSingleton
1747 : false) ||
@@ -2018,7 +2023,7 @@ function commitDeletionEffectsOnFiber(
2023 // into their subtree. There are simpler cases in the inner switch
2024 // that don't modify the stack.
2025 switch (deletedFiber.tag) {
2021 - case HostResource: {
2026 + case HostHoistable: {
2027 if (enableFloat && supportsResources) {
2028 if (!offscreenSubtreeWasHidden) {
2029 safelyDetachRef(deletedFiber, nearestMountedAncestor);
@@ -2030,6 +2035,8 @@ function commitDeletionEffectsOnFiber(
2035 );
2036 if (deletedFiber.memoizedState) {
2037 releaseResource(deletedFiber.memoizedState);
2038 + } else if (deletedFiber.stateNode) {
2039 + unmountHoistable(deletedFiber.stateNode);
2040 }
2041 return;
2042 }
@@ -2520,6 +2527,8 @@ function recursivelyTraverseMutationEffects(
2527 setCurrentDebugFiberInDEV(prevDebugFiber);
2528 }
2529
2530 +let currentHoistableRoot: HoistableRoot | null = null;
2531 +
2532 function commitMutationEffectsOnFiber(
2533 finishedWork: Fiber,
2534 root: FiberRoot,
@@ -2603,8 +2612,11 @@ function commitMutationEffectsOnFiber(
2612 }
2613 return;
2614 }
2606 - case HostResource: {
2615 + case HostHoistable: {
2616 if (enableFloat && supportsResources) {
2617 + // We cast because we always set the root at the React root and so it cannot be
2618 + // null while we are processing mutation effects
2619 + const hoistableRoot: HoistableRoot = (currentHoistableRoot: any);
2620 recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2621 commitReconciliationEffects(finishedWork);
2622
@@ -2615,16 +2627,81 @@ function commitMutationEffectsOnFiber(
2627 }
2628
2629 if (flags & Update) {
2630 + const currentResource =
2631 + current !== null ? current.memoizedState : null;
2632 const newResource = finishedWork.memoizedState;
2619 - if (current !== null) {
2620 - const currentResource = current.memoizedState;
2621 - if (currentResource !== newResource) {
2633 + if (current === null) {
2634 + // We are mounting a new HostHoistable Fiber. We fork the mount
2635 + // behavior based on whether this instance is a Hoistable Instance
2636 + // or a Hoistable Resource
2637 + if (newResource === null) {
2638 + if (finishedWork.stateNode === null) {
2639 + finishedWork.stateNode = hydrateHoistable(
2640 + hoistableRoot,
2641 + finishedWork.type,
2642 + finishedWork.memoizedProps,
2643 + finishedWork,
2644 + );
2645 + } else {
2646 + mountHoistable(
2647 + hoistableRoot,
2648 + finishedWork.type,
2649 + finishedWork.stateNode,
2650 + );
2651 + }
2652 + } else {
2653 + finishedWork.stateNode = acquireResource(
2654 + hoistableRoot,
2655 + newResource,
2656 + finishedWork.memoizedProps,
2657 + );
2658 + }
2659 + } else if (currentResource !== newResource) {
2660 + // We are moving to or from Hoistable Resource, or between different Hoistable Resources
2661 + if (currentResource === null) {
2662 + if (current.stateNode !== null) {
2663 + unmountHoistable(current.stateNode);
2664 + }
2665 + } else {
2666 releaseResource(currentResource);
2667 }
2668 + if (newResource === null) {
2669 + mountHoistable(
2670 + hoistableRoot,
2671 + finishedWork.type,
2672 + finishedWork.stateNode,
2673 + );
2674 + } else {
2675 + acquireResource(
2676 + hoistableRoot,
2677 + newResource,
2678 + finishedWork.memoizedProps,
2679 + );
2680 + }
2681 + } else if (newResource === null && finishedWork.stateNode !== null) {
2682 + // We may have an update on a Hoistable element
2683 + const updatePayload: null | UpdatePayload =
2684 + (finishedWork.updateQueue: any);
2685 + finishedWork.updateQueue = null;
2686 + if (updatePayload !== null) {
2687 + try {
2688 + commitUpdate(
2689 + finishedWork.stateNode,
2690 + updatePayload,
2691 + finishedWork.type,
2692 + current.memoizedProps,
2693 + finishedWork.memoizedProps,
2694 + finishedWork,
2695 + );
2696 + } catch (error) {
2697 + captureCommitPhaseError(
2698 + finishedWork,
2699 + finishedWork.return,
2700 + error,
2701 + );
2702 + }
2703 + }
2704 }
2625 - finishedWork.stateNode = newResource
2626 - ? acquireResource(newResource)
2627 - : null;
2705 }
2706 return;
2707 }
@@ -2744,8 +2821,18 @@ function commitMutationEffectsOnFiber(
2821 return;
2822 }
2823 case HostRoot: {
2747 - recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2748 - commitReconciliationEffects(finishedWork);
2824 + if (enableFloat && supportsResources) {
2825 + const previousHoistableRoot = currentHoistableRoot;
2826 + currentHoistableRoot = getHoistableRoot(root.containerInfo);
2827 +
2828 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2829 + currentHoistableRoot = previousHoistableRoot;
2830 +
2831 + commitReconciliationEffects(finishedWork);
2832 + } else {
2833 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2834 + commitReconciliationEffects(finishedWork);
2835 + }
2836
2837 if (flags & Update) {
2838 if (supportsMutation && supportsHydration) {
@@ -2777,8 +2864,18 @@ function commitMutationEffectsOnFiber(
2864 return;
2865 }
2866 case HostPortal: {
2780 - recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2781 - commitReconciliationEffects(finishedWork);
2867 + if (enableFloat && supportsResources) {
2868 + const previousHoistableRoot = currentHoistableRoot;
2869 + currentHoistableRoot = getHoistableRoot(
2870 + finishedWork.stateNode.containerInfo,
2871 + );
2872 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2873 + commitReconciliationEffects(finishedWork);
2874 + currentHoistableRoot = previousHoistableRoot;
2875 + } else {
2876 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2877 + commitReconciliationEffects(finishedWork);
2878 + }
2879
2880 if (flags & Update) {
2881 if (supportsPersistence) {
@@ -3059,7 +3156,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
3156 recursivelyTraverseDisappearLayoutEffects(finishedWork);
3157 break;
3158 }
3062 - case HostResource:
3159 + case HostHoistable:
3160 case HostSingleton:
3161 case HostComponent: {
3162 // TODO (Offscreen) Check: flags & RefStatic
@@ -3161,7 +3258,7 @@ export function reappearLayoutEffects(
3258 // case HostRoot: {
3259 // ...
3260 // }
3164 - case HostResource:
3261 + case HostHoistable:
3262 case HostSingleton:
3263 case HostComponent: {
3264 recursivelyTraverseReappearLayoutEffects(
packages/react-reconciler/src/ReactFiberCompleteWork.js
+16 -5
@@ -51,7 +51,7 @@ import {
51 ClassComponent,
52 HostRoot,
53 HostComponent,
54 - HostResource,
54 + HostHoistable,
55 HostSingleton,
56 HostText,
57 HostPortal,
@@ -989,20 +989,31 @@ function completeWork(
989 }
990 return null;
991 }
992 - case HostResource: {
992 + case HostHoistable: {
993 if (enableFloat && supportsResources) {
994 - popHostContext(workInProgress);
994 const currentRef = current ? current.ref : null;
995 if (currentRef !== workInProgress.ref) {
996 markRef(workInProgress);
997 }
998 +
999 if (
1000 + // We are mounting and must Update this Hoistable in this commit
1001 current === null ||
1002 + // We are transitioning to, from, or between Hoistable Resources
1003 + // and require an update
1004 current.memoizedState !== workInProgress.memoizedState
1005 ) {
1003 - // The workInProgress resource is different than the current one or the current
1004 - // one does not exist
1006 markUpdate(workInProgress);
1007 + } else if (workInProgress.memoizedState === null) {
1008 + // We may have props to update on the Hoistable instance. We use the
1009 + // updateHostComponent path becuase it produces the update queue
1010 + // we need for Hoistables
1011 + updateHostComponent(
1012 + current,
1013 + workInProgress,
1014 + workInProgress.type,
1015 + workInProgress.pendingProps,
1016 + );
1017 }
1018 bubbleProperties(workInProgress);
1019 return null;
packages/react-reconciler/src/ReactFiberComponentStack.js
+2 -2
@@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes';
11
12 import {
13 HostComponent,
14 - HostResource,
14 + HostHoistable,
15 HostSingleton,
16 LazyComponent,
17 SuspenseComponent,
@@ -36,7 +36,7 @@ function describeFiber(fiber: Fiber): string {
36 : null;
37 const source = __DEV__ ? fiber._debugSource : null;
38 switch (fiber.tag) {
39 - case HostResource:
39 + case HostHoistable:
40 case HostSingleton:
41 case HostComponent:
42 return describeBuiltInComponentFrame(fiber.type, source, owner);
packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js
+8 -1
@@ -18,9 +18,16 @@ function shim(...args: any): empty {
18 );
19 }
20
21 +export type HoistableRoot = mixed;
22 +
23 // Resources (when unsupported)
24 export const supportsResources = false;
23 -export const isHostResourceType = shim;
25 +export const isHostHoistableType = shim;
26 +export const getHoistableRoot = shim;
27 export const getResource = shim;
28 export const acquireResource = shim;
29 export const releaseResource = shim;
30 +export const hydrateHoistable = shim;
31 +export const mountHoistable = shim;
32 +export const unmountHoistable = shim;
33 +export const createHoistableInstance = shim;
packages/react-reconciler/src/ReactFiberHotReloading.js
+2 -2
@@ -29,7 +29,7 @@ import {
29 FunctionComponent,
30 ForwardRef,
31 HostComponent,
32 - HostResource,
32 + HostHoistable,
33 HostSingleton,
34 HostPortal,
35 HostRoot,
@@ -468,7 +468,7 @@ function findChildHostInstancesForFiberShallowly(
468 while (true) {
469 if (
470 node.tag === HostComponent ||
471 - (enableFloat ? node.tag === HostResource : false) ||
471 + (enableFloat ? node.tag === HostHoistable : false) ||
472 (enableHostSingletons && supportsSingletons
473 ? node.tag === HostSingleton
474 : false)
packages/react-reconciler/src/ReactFiberTreeReflection.js
+3 -3
@@ -17,7 +17,7 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom
17 import {
18 ClassComponent,
19 HostComponent,
20 - HostResource,
20 + HostHoistable,
21 HostSingleton,
22 HostRoot,
23 HostPortal,
@@ -280,7 +280,7 @@ function findCurrentHostFiberImpl(node: Fiber): Fiber | null {
280 const tag = node.tag;
281 if (
282 tag === HostComponent ||
283 - (enableFloat ? tag === HostResource : false) ||
283 + (enableFloat ? tag === HostHoistable : false) ||
284 (enableHostSingletons ? tag === HostSingleton : false) ||
285 tag === HostText
286 ) {
@@ -311,7 +311,7 @@ function findCurrentHostFiberWithNoPortalsImpl(node: Fiber): Fiber | null {
311 const tag = node.tag;
312 if (
313 tag === HostComponent ||
314 - (enableFloat ? tag === HostResource : false) ||
314 + (enableFloat ? tag === HostHoistable : false) ||
315 (enableHostSingletons ? tag === HostSingleton : false) ||
316 tag === HostText
317 ) {
packages/react-reconciler/src/ReactFiberUnwindWork.js
+3 -3
@@ -19,7 +19,7 @@ import {
19 ClassComponent,
20 HostRoot,
21 HostComponent,
22 - HostResource,
22 + HostHoistable,
23 HostSingleton,
24 HostPortal,
25 ContextProvider,
@@ -117,7 +117,7 @@ function unwindWork(
117 // We unwound to the root without completing it. Exit.
118 return null;
119 }
120 - case HostResource:
120 + case HostHoistable:
121 case HostSingleton:
122 case HostComponent: {
123 // TODO: popHydrationState
@@ -237,7 +237,7 @@ function unwindInterruptedWork(
237 resetMutableSourceWorkInProgressVersions();
238 break;
239 }
240 - case HostResource:
240 + case HostHoistable:
241 case HostSingleton:
242 case HostComponent: {
243 popHostContext(interruptedWork);
packages/react-reconciler/src/ReactTestSelectors.js
+9 -9
@@ -12,7 +12,7 @@ import type {Instance} from './ReactFiberHostConfig';
12
13 import {
14 HostComponent,
15 - HostResource,
15 + HostHoistable,
16 HostSingleton,
17 HostText,
18 } from 'react-reconciler/src/ReactWorkTags';
@@ -158,7 +158,7 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
158 case ROLE_TYPE:
159 if (
160 tag === HostComponent ||
161 - tag === HostResource ||
161 + tag === HostHoistable ||
162 tag === HostSingleton
163 ) {
164 const node = fiber.stateNode;
@@ -173,7 +173,7 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
173 if (
174 tag === HostComponent ||
175 tag === HostText ||
176 - tag === HostResource ||
176 + tag === HostHoistable ||
177 tag === HostSingleton
178 ) {
179 const textContent = getTextContent(fiber);
@@ -188,7 +188,7 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
188 case TEST_NAME_TYPE:
189 if (
190 tag === HostComponent ||
191 - tag === HostResource ||
191 + tag === HostHoistable ||
192 tag === HostSingleton
193 ) {
194 const dataTestID = fiber.memoizedProps['data-testname'];
@@ -239,7 +239,7 @@ function findPaths(root: Fiber, selectors: Array<Selector>): Array<Fiber> {
239
240 if (
241 (tag === HostComponent ||
242 - tag === HostResource ||
242 + tag === HostHoistable ||
243 tag === HostSingleton) &&
244 isHiddenSubtree(fiber)
245 ) {
@@ -277,7 +277,7 @@ function hasMatchingPaths(root: Fiber, selectors: Array<Selector>): boolean {
277
278 if (
279 (tag === HostComponent ||
280 - tag === HostResource ||
280 + tag === HostHoistable ||
281 tag === HostSingleton) &&
282 isHiddenSubtree(fiber)
283 ) {
@@ -323,7 +323,7 @@ export function findAllNodes(
323 const tag = node.tag;
324 if (
325 tag === HostComponent ||
326 - tag === HostResource ||
326 + tag === HostHoistable ||
327 tag === HostSingleton
328 ) {
329 if (isHiddenSubtree(node)) {
@@ -366,7 +366,7 @@ export function getFindAllNodesFailureDescription(
366
367 if (
368 (tag === HostComponent ||
369 - tag === HostResource ||
369 + tag === HostHoistable ||
370 tag === HostSingleton) &&
371 isHiddenSubtree(fiber)
372 ) {
@@ -524,7 +524,7 @@ export function focusWithin(
524 }
525 if (
526 tag === HostComponent ||
527 - tag === HostResource ||
527 + tag === HostHoistable ||
528 tag === HostSingleton
529 ) {
530 const node = fiber.stateNode;
packages/react-reconciler/src/ReactWorkTags.js
+1 -1
@@ -62,5 +62,5 @@ export const OffscreenComponent = 22;
62 export const LegacyHiddenComponent = 23;
63 export const CacheComponent = 24;
64 export const TracingMarkerComponent = 25;
65 -export const HostResource = 26;
65 +export const HostHoistable = 26;
66 export const HostSingleton = 27;
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+7 -1
@@ -196,10 +196,16 @@ export const errorHydratingContainer = $$$hostConfig.errorHydratingContainer;
196 // (optional)
197 // -------------------
198 export const supportsResources = $$$hostConfig.supportsResources;
199 -export const isHostResourceType = $$$hostConfig.isHostResourceType;
199 +export const isHostHoistableType = $$$hostConfig.isHostHoistableType;
200 +export const getHoistableRoot = $$$hostConfig.getHoistableRoot;
201 export const getResource = $$$hostConfig.getResource;
202 export const acquireResource = $$$hostConfig.acquireResource;
203 export const releaseResource = $$$hostConfig.releaseResource;
204 +export const hydrateHoistable = $$$hostConfig.hydrateHoistable;
205 +export const mountHoistable = $$$hostConfig.mountHoistable;
206 +export const unmountHoistable = $$$hostConfig.unmountHoistable;
207 +export const createHoistableInstance = $$$hostConfig.createHoistableInstance;
208 +export type HoistableRoot = mixed;
209
210 // -------------------
211 // Singletons
packages/react-reconciler/src/getComponentNameFromFiber.js
+2 -2
@@ -19,7 +19,7 @@ import {
19 HostRoot,
20 HostPortal,
21 HostComponent,
22 - HostResource,
22 + HostHoistable,
23 HostSingleton,
24 HostText,
25 Fragment,
@@ -79,7 +79,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
79 return getWrappedName(type, type.render, 'ForwardRef');
80 case Fragment:
81 return 'Fragment';
82 - case HostResource:
82 + case HostHoistable:
83 case HostSingleton:
84 case HostComponent:
85 // Host component type is the display name (e.g. "div", "View")
packages/react-server/src/ReactFizzServer.js
+32 -49
@@ -65,8 +65,10 @@ import {
65 UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
66 assignSuspenseBoundaryID,
67 getChildFormatContext,
68 - writeInitialResources,
69 - writeImmediateResources,
68 + writeResourcesForBoundary,
69 + writePreamble,
70 + writeHoistables,
71 + writePostamble,
72 hoistResources,
73 hoistResourcesToRoot,
74 prepareToRender,
@@ -221,8 +223,6 @@ export opaque type Request = {
223 clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.
224 completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
225 partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.
224 - +preamble: Array<Chunk | PrecomputedChunk>, // Chunks that need to be emitted before any segment chunks.
225 - +postamble: Array<Chunk | PrecomputedChunk>, // Chunks that need to be emitted after segments, waiting for all pending root tasks to finish
226 // onError is called when an error happens anywhere in the tree. It might recover.
227 // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.
228 // Returning null/undefined will cause a defualt error message in production
@@ -294,11 +294,9 @@ export function createRequest(
294 completedRootSegment: null,
295 abortableTasks: abortSet,
296 pingedTasks: pingedTasks,
297 - clientRenderedBoundaries: [],
298 - completedBoundaries: [],
299 - partialBoundaries: [],
300 - preamble: [],
301 - postamble: [],
297 + clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
298 + completedBoundaries: ([]: Array<SuspenseBoundary>),
299 + partialBoundaries: ([]: Array<SuspenseBoundary>),
300 onError: onError === undefined ? defaultErrorHandler : onError,
301 onAllReady: onAllReady === undefined ? noop : onAllReady,
302 onShellReady: onShellReady === undefined ? noop : onShellReady,
@@ -695,9 +693,9 @@ function renderHostElement(
693
694 const children = pushStartInstance(
695 segment.chunks,
698 - request.preamble,
696 type,
697 props,
698 + request.resources,
699 request.responseState,
700 segment.formatContext,
701 segment.lastPushedText,
@@ -713,7 +711,13 @@ function renderHostElement(
711 // We expect that errors will fatal the whole task and that we don't need
712 // the correct context. Therefore this is not in a finally.
713 segment.formatContext = prevContext;
716 - pushEndInstance(segment.chunks, request.postamble, type, props);
714 + pushEndInstance(
715 + segment.chunks,
716 + type,
717 + props,
718 + request.responseState,
719 + prevContext,
720 + );
721 segment.lastPushedText = false;
722 popComponentStackInDEV(task);
723 }
@@ -2123,31 +2127,6 @@ function flushSegment(
2127 }
2128 }
2129
2126 -function flushInitialResources(
2127 - destination: Destination,
2128 - resources: Resources,
2129 - responseState: ResponseState,
2130 - willFlushAllSegments: boolean,
2131 -): void {
2132 - writeInitialResources(
2133 - destination,
2134 - resources,
2135 - responseState,
2136 - willFlushAllSegments,
2137 - );
2138 -}
2139 -
2140 -function flushImmediateResources(
2141 - destination: Destination,
2142 - request: Request,
2143 -): void {
2144 - writeImmediateResources(
2145 - destination,
2146 - request.resources,
2147 - request.responseState,
2148 - );
2149 -}
2150 -
2130 function flushClientRenderedBoundary(
2131 request: Request,
2132 destination: Destination,
@@ -2197,6 +2176,10 @@ function flushCompletedBoundary(
2176 }
2177 completedSegments.length = 0;
2178
2179 + if (enableFloat) {
2180 + writeResourcesForBoundary(destination, boundary.resources);
2181 + }
2182 +
2183 return writeCompletedBoundaryInstruction(
2184 destination,
2185 request.responseState,
@@ -2232,7 +2215,16 @@ function flushPartialBoundary(
2215 }
2216 }
2217 completedSegments.splice(0, i);
2235 - return true;
2218 +
2219 + if (enableFloat) {
2220 + // The way this is structured we only write resources for partial boundaries
2221 + // if there is no backpressure. Later before we complete the boundary we
2222 + // will write resources regardless of backpressure before we emit the
2223 + // completion instruction
2224 + return writeResourcesForBoundary(destination, boundary.resources);
2225 + } else {
2226 + return true;
2227 + }
2228 }
2229
2230 function flushPartiallyCompletedSegment(
@@ -2285,13 +2277,7 @@ function flushCompletedQueues(
2277 if (completedRootSegment !== null) {
2278 if (request.pendingRootTasks === 0) {
2279 if (enableFloat) {
2288 - const preamble = request.preamble;
2289 - for (i = 0; i < preamble.length; i++) {
2290 - // we expect the preamble to be tiny and will ignore backpressure
2291 - writeChunk(destination, preamble[i]);
2292 - }
2293 -
2294 - flushInitialResources(
2280 + writePreamble(
2281 destination,
2282 request.resources,
2283 request.responseState,
@@ -2307,7 +2293,7 @@ function flushCompletedQueues(
2293 return;
2294 }
2295 } else if (enableFloat) {
2310 - flushImmediateResources(destination, request);
2296 + writeHoistables(destination, request.resources, request.responseState);
2297 }
2298
2299 // We emit client rendering instructions for already emitted boundaries first.
@@ -2385,10 +2371,7 @@ function flushCompletedQueues(
2371 // either they have pending task or they're complete.
2372 ) {
2373 if (enableFloat) {
2388 - const postamble = request.postamble;
2389 - for (let i = 0; i < postamble.length; i++) {
2390 - writeChunk(destination, postamble[i]);
2391 - }
2374 + writePostamble(destination, request.responseState);
2375 }
2376 completeWriting(destination);
2377 flushBuffered(destination);
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+5 -2
@@ -74,11 +74,14 @@ export const cleanupAfterRender = $$$hostConfig.cleanupAfterRender;
74 // -------------------------
75 // Resources
76 // -------------------------
77 -export const writeInitialResources = $$$hostConfig.writeInitialResources;
78 -export const writeImmediateResources = $$$hostConfig.writeImmediateResources;
77 +export const writePreamble = $$$hostConfig.writePreamble;
78 +export const writeHoistables = $$$hostConfig.writeHoistables;
79 +export const writePostamble = $$$hostConfig.writePostamble;
80 export const hoistResources = $$$hostConfig.hoistResources;
81 export const hoistResourcesToRoot = $$$hostConfig.hoistResourcesToRoot;
82 export const createResources = $$$hostConfig.createResources;
83 export const createBoundaryResources = $$$hostConfig.createBoundaryResources;
84 export const setCurrentlyRenderingBoundaryResourcesTarget =
85 $$$hostConfig.setCurrentlyRenderingBoundaryResourcesTarget;
86 +export const writeResourcesForBoundary =
87 + $$$hostConfig.writeResourcesForBoundary;
packages/react-test-renderer/src/ReactTestRenderer.js
+3 -3
@@ -30,7 +30,7 @@ import {
30 FunctionComponent,
31 ClassComponent,
32 HostComponent,
33 - HostResource,
33 + HostHoistable,
34 HostSingleton,
35 HostPortal,
36 HostText,
@@ -203,7 +203,7 @@ function toTree(node: null | Fiber): $FlowFixMe {
203 instance: null,
204 rendered: childrenToTree(node.child),
205 };
206 - case HostResource:
206 + case HostHoistable:
207 case HostSingleton:
208 case HostComponent: {
209 return {
@@ -314,7 +314,7 @@ class ReactTestInstance {
314 const tag = this._fiber.tag;
315 if (
316 tag === HostComponent ||
317 - tag === HostResource ||
317 + tag === HostHoistable ||
318 tag === HostSingleton
319 ) {
320 return getPublicInstance(this._fiber.stateNode);
scripts/error-codes/codes.json
+1 -1
@@ -429,7 +429,7 @@
429 "441": "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.",
430 "442": "The current renderer does not support Resources. This error is likely caused by a bug in React. Please file an issue.",
431 "443": "acquireResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
432 - "444": "getResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
432 + "444": "getResource encountered a type it did not expect: \"%s\". this is a bug in React.",
433 "445": "\"currentResources\" was expected to exist. This is a bug in React.",
434 "446": "\"resourceRoot\" was expected to exist. This is a bug in React.",
435 "447": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.",
scripts/rollup/generate-inline-fizz-runtime.js
+1 -1
@@ -77,7 +77,7 @@ async function main() {
77 // Fizz runtime, and should break immediately if there were a mistake, so I'm
78 // not too worried about it.
79 outputCode = outputCode.replace(
80 - /window\.(\$[A-z0-9_]*)/g,
80 + /window\.(\$[A-z0-9_]*|matchMedia)/g,
81 (_, variableName) => variableName
82 );
83