From a1c26b10eb5a6643cce532ba672818a61f153054 Mon Sep 17 00:00:00 2001 From: gnoff Date: Fri, 10 Feb 2023 07:07:19 +0000 Subject: [PATCH] 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 . 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 tag because the queues for hoistable elements won't flush until after we have flushed the preamble (``). 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 ## `` as a Resource analagous to `` 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 `'); + +function flushResourceInPreamble(resource) { + if ((resource.state & (Flushed | Blocked)) === NoState) { + var chunks = resource.chunks; + + for (var i = 0; i < chunks.length; i++) { + writeChunk(this, chunks[i]); + } + + resource.state |= FlushedInPreamble; + } +} + +function flushResourceLate(resource) { + if ((resource.state & Flushed) === NoState) { + var chunks = resource.chunks; + + for (var i = 0; i < chunks.length; i++) { + writeChunk(this, chunks[i]); + } + + resource.state |= FlushedLate; + } +} + +var didFlush = false; + +function flushUnblockedStyle(resource, key, set) { + var chunks = resource.chunks; + + if (resource.state & Flushed) { + // In theory this should never happen because we clear from the + // Set on flush but to ensure correct semantics we don't emit + // anything if we are in this state. + set.delete(resource); + } else if (resource.state & Blocked); + else { + didFlush = true; // We can emit this style or stylesheet as is. + + if (resource.type === "stylesheet") { + // We still need to encode stylesheet chunks + // because unlike most Hoistables and Resources we do not eagerly encode + // them during render. This is because if we flush late we have to send a + // different encoding and we don't want to encode multiple times + pushLinkImpl(chunks, resource.props); + } + + for (var i = 0; i < chunks.length; i++) { + writeChunk(this, chunks[i]); + } + + resource.state |= FlushedInPreamble; + set.delete(resource); + } +} + +function flushUnblockedStyles(set, precedence) { + didFlush = false; + set.forEach(flushUnblockedStyle, this); + + if (!didFlush) { + // if we did not flush anything for this precedence slot we emit + // an empty '); -function writeInitialResources( +function preloadLateStyles(set, precedence) { + set.forEach(preloadLateStyle, this); + set.clear(); +} // We don't bother reporting backpressure at the moment because we expect to +// flush the entire preamble in a single pass. This probably should be modified +// in the future to be backpressure sensitive but that requires a larger refactor +// of the flushing code in Fizz. + +function writePreamble( destination, resources, responseState, willFlushAllSegments ) { - // Write initially discovered resources after the shell completes + // This function must be called exactly once on every request if (!willFlushAllSegments && responseState.externalRuntimeConfig) { // If the root segment is incomplete due to suspended tasks // (e.g. willFlushAllSegments = false) and we are using data @@ -6143,206 +5878,184 @@ function writeInitialResources( as: "script", integrity: integrity }); - } // $FlowFixMe[missing-local-annot] + } + + var htmlChunks = responseState.htmlChunks; + var headChunks = responseState.headChunks; + var i = 0; // Emit open tags before Hoistables and Resources + + if (htmlChunks) { + // We have an to emit as part of the preamble + for (i = 0; i < htmlChunks.length; i++) { + writeChunk(destination, htmlChunks[i]); + } - function flushLinkResource(resource) { - if (!resource.flushed) { - pushLinkImpl(target, resource.props, responseState); - resource.flushed = true; + if (headChunks) { + for (i = 0; i < headChunks.length; i++) { + writeChunk(destination, headChunks[i]); + } + } else { + // We did not render a head but we emitted an so we emit one now + writeChunk(destination, startChunkForTag("head")); + writeChunk(destination, endOfStartTag); } + } else if (headChunks) { + // We do not have an but we do have a + for (i = 0; i < headChunks.length; i++) { + writeChunk(destination, headChunks[i]); + } + } // Emit high priority Hoistables + + var charsetChunks = responseState.charsetChunks; + + for (i = 0; i < charsetChunks.length; i++) { + writeChunk(destination, charsetChunks[i]); } - var target = []; - var charset = resources.charset, - bases = resources.bases, - preconnects = resources.preconnects, - fontPreloads = resources.fontPreloads, - precedences = resources.precedences, - usedStylePreloads = resources.usedStylePreloads, - scripts = resources.scripts, - usedScriptPreloads = resources.usedScriptPreloads, - explicitStylePreloads = resources.explicitStylePreloads, - explicitScriptPreloads = resources.explicitScriptPreloads, - headResources = resources.headResources; + charsetChunks.length = 0; + var preconnectChunks = responseState.preconnectChunks; - if (charset) { - pushSelfClosing(target, charset.props, "meta", responseState); - charset.flushed = true; - resources.charset = null; + for (i = 0; i < preconnectChunks.length; i++) { + writeChunk(destination, preconnectChunks[i]); } - bases.forEach(function (r) { - pushSelfClosing(target, r.props, "base", responseState); - r.flushed = true; - }); - bases.clear(); - preconnects.forEach(function (r) { - // font preload Resources should not already be flushed so we elide this check - pushLinkImpl(target, r.props, responseState); - r.flushed = true; - }); - preconnects.clear(); - fontPreloads.forEach(function (r) { - // font preload Resources should not already be flushed so we elide this check - pushLinkImpl(target, r.props, responseState); - r.flushed = true; - }); - fontPreloads.clear(); // Flush stylesheets first by earliest precedence - - precedences.forEach(function (p, precedence) { - if (p.size) { - p.forEach(function (r) { - // resources should not already be flushed so we elide this check - pushLinkImpl(target, r.props, responseState); - r.flushed = true; - r.inShell = true; - r.hint.flushed = true; - }); - p.clear(); - } else { - target.push( - precedencePlaceholderStart, - stringToChunk(escapeTextForBrowser(precedence)), - precedencePlaceholderEnd - ); - } - }); - usedStylePreloads.forEach(flushLinkResource); - usedStylePreloads.clear(); - scripts.forEach(function (r) { - // should never be flushed already - pushScriptImpl(target, r.props, responseState); - r.flushed = true; - r.hint.flushed = true; - }); - scripts.clear(); - usedScriptPreloads.forEach(flushLinkResource); - usedScriptPreloads.clear(); - explicitStylePreloads.forEach(flushLinkResource); - explicitStylePreloads.clear(); - explicitScriptPreloads.forEach(flushLinkResource); - explicitScriptPreloads.clear(); - headResources.forEach(function (r) { - switch (r.type) { - case "title": { - pushTitleImpl(target, r.props, responseState); - break; - } + preconnectChunks.length = 0; + resources.fontPreloads.forEach(flushResourceInPreamble, destination); + resources.fontPreloads.clear(); // Flush unblocked stylesheets by precedence - case "meta": { - pushSelfClosing(target, r.props, "meta", responseState); - break; - } + resources.precedences.forEach(flushUnblockedStyles, destination); // Flush preloads for Blocked stylesheets - case "link": { - pushLinkImpl(target, r.props, responseState); - break; + resources.precedences.forEach(preloadBlockedStyles, destination); + resources.usedStylesheets.forEach(function (resource) { + var key = getResourceKey(resource.props.as, resource.props.href); + + if (resources.stylesMap.has(key)); + else { + var chunks = resource.chunks; + + for (i = 0; i < chunks.length; i++) { + writeChunk(destination, chunks[i]); } } - - r.flushed = true; }); - headResources.clear(); - var i; - var r = true; + resources.usedStylesheets.clear(); + resources.scripts.forEach(flushResourceInPreamble, destination); + resources.scripts.clear(); + resources.usedScripts.forEach(flushResourceInPreamble, destination); + resources.usedScripts.clear(); + resources.explicitStylesheetPreloads.forEach( + flushResourceInPreamble, + destination + ); + resources.explicitStylesheetPreloads.clear(); + resources.explicitScriptPreloads.forEach( + flushResourceInPreamble, + destination + ); + resources.explicitScriptPreloads.clear(); + resources.explicitOtherPreloads.forEach(flushResourceInPreamble, destination); + resources.explicitOtherPreloads.clear(); // Write embedding preloadChunks - for (i = 0; i < target.length - 1; i++) { - writeChunk(destination, target[i]); + var preloadChunks = responseState.preloadChunks; + + for (i = 0; i < preloadChunks.length; i++) { + writeChunk(destination, preloadChunks[i]); } - if (i < target.length) { - r = writeChunkAndReturn(destination, target[i]); + preloadChunks.length = 0; // Write embedding hoistableChunks + + var hoistableChunks = responseState.hoistableChunks; + + for (i = 0; i < hoistableChunks.length; i++) { + writeChunk(destination, hoistableChunks[i]); } - return r; -} -function writeImmediateResources(destination, resources, responseState) { - // $FlowFixMe[missing-local-annot] - function flushLinkResource(resource) { - if (!resource.flushed) { - pushLinkImpl(target, resource.props, responseState); - resource.flushed = true; - } - } - - var target = []; - var charset = resources.charset, - preconnects = resources.preconnects, - fontPreloads = resources.fontPreloads, - usedStylePreloads = resources.usedStylePreloads, - scripts = resources.scripts, - usedScriptPreloads = resources.usedScriptPreloads, - explicitStylePreloads = resources.explicitStylePreloads, - explicitScriptPreloads = resources.explicitScriptPreloads, - headResources = resources.headResources; - - if (charset) { - pushSelfClosing(target, charset.props, "meta", responseState); - charset.flushed = true; - resources.charset = null; - } - - preconnects.forEach(function (r) { - // font preload Resources should not already be flushed so we elide this check - pushLinkImpl(target, r.props, responseState); - r.flushed = true; - }); - preconnects.clear(); - fontPreloads.forEach(function (r) { - // font preload Resources should not already be flushed so we elide this check - pushLinkImpl(target, r.props, responseState); - r.flushed = true; - }); - fontPreloads.clear(); - usedStylePreloads.forEach(flushLinkResource); - usedStylePreloads.clear(); - scripts.forEach(function (r) { - // should never be flushed already - pushStartGenericElement(target, r.props, "script", responseState); - pushEndInstance(target, target, "script", r.props); - r.flushed = true; - r.hint.flushed = true; - }); - scripts.clear(); - usedScriptPreloads.forEach(flushLinkResource); - usedScriptPreloads.clear(); - explicitStylePreloads.forEach(flushLinkResource); - explicitStylePreloads.clear(); - explicitScriptPreloads.forEach(flushLinkResource); - explicitScriptPreloads.clear(); - headResources.forEach(function (r) { - switch (r.type) { - case "title": { - pushTitleImpl(target, r.props, responseState); - break; - } + hoistableChunks.length = 0; // Flush closing head if necessary - case "meta": { - pushSelfClosing(target, r.props, "meta", responseState); - break; - } + if (htmlChunks && headChunks === null) { + // We have an rendered but no rendered. We however inserted + // a up above so we need to emit the now. This is safe because + // if the main content contained the it would also have provided a + // . This means that all the content inside is either or + // invalid HTML + writeChunk(destination, endTag1); + writeChunk(destination, stringToChunk("head")); + writeChunk(destination, endTag2); + } +} // We don't bother reporting backpressure at the moment because we expect to +// flush the entire preamble in a single pass. This probably should be modified +// in the future to be backpressure sensitive but that requires a larger refactor +// of the flushing code in Fizz. - case "link": { - pushLinkImpl(target, r.props, responseState); - break; +function writeHoistables(destination, resources, responseState) { + var i = 0; // Emit high priority Hoistables + // We omit charsetChunks because we have already sent the shell and if it wasn't + // already sent it is too late now. + + var preconnectChunks = responseState.preconnectChunks; + + for (i = 0; i < preconnectChunks.length; i++) { + writeChunk(destination, preconnectChunks[i]); + } + + preconnectChunks.length = 0; + resources.fontPreloads.forEach(flushResourceLate, destination); + resources.fontPreloads.clear(); // Preload any stylesheets. these will emit in a render instruction that follows this + // but we want to kick off preloading as soon as possible + + resources.precedences.forEach(preloadLateStyles, destination); + resources.usedStylesheets.forEach(function (resource) { + var key = getResourceKey(resource.props.as, resource.props.href); + + if (resources.stylesMap.has(key)); + else { + var chunks = resource.chunks; + + for (i = 0; i < chunks.length; i++) { + writeChunk(destination, chunks[i]); } } - - r.flushed = true; }); - headResources.clear(); - var i; - var r = true; + resources.usedStylesheets.clear(); + resources.scripts.forEach(flushResourceLate, destination); + resources.scripts.clear(); + resources.usedScripts.forEach(flushResourceLate, destination); + resources.usedScripts.clear(); + resources.explicitStylesheetPreloads.forEach(flushResourceLate, destination); + resources.explicitStylesheetPreloads.clear(); + resources.explicitScriptPreloads.forEach(flushResourceLate, destination); + resources.explicitScriptPreloads.clear(); + resources.explicitOtherPreloads.forEach(flushResourceLate, destination); + resources.explicitOtherPreloads.clear(); // Write embedding preloadChunks + + var preloadChunks = responseState.preloadChunks; + + for (i = 0; i < preloadChunks.length; i++) { + writeChunk(destination, preloadChunks[i]); + } + + preloadChunks.length = 0; // Write embedding hoistableChunks - for (i = 0; i < target.length - 1; i++) { - writeChunk(destination, target[i]); + var hoistableChunks = responseState.hoistableChunks; + + for (i = 0; i < hoistableChunks.length; i++) { + writeChunk(destination, hoistableChunks[i]); } - if (i < target.length) { - r = writeChunkAndReturn(destination, target[i]); + hoistableChunks.length = 0; +} +function writePostamble(destination, responseState) { + if (responseState.hasBody) { + writeChunk(destination, endTag1); + writeChunk(destination, stringToChunk("body")); + writeChunk(destination, endTag2); } - return r; + if (responseState.htmlChunks) { + writeChunk(destination, endTag1); + writeChunk(destination, stringToChunk("html")); + writeChunk(destination, endTag2); + } } function hasStyleResourceDependencies(boundaryResources) { @@ -6356,7 +6069,7 @@ function hasStyleResourceDependencies(boundaryResources) { if (!resource) break; // If every style Resource flushed in the shell we do not need to send // any dependencies - if (!resource.inShell) { + if ((resource.state & FlushedInPreamble) === NoState) { return true; } } @@ -6375,31 +6088,236 @@ function writeStyleResourceDependenciesInJS(destination, boundaryResources) { writeChunk(destination, arrayFirstOpenBracket); var nextArrayOpenBrackChunk = arrayFirstOpenBracket; boundaryResources.forEach(function (resource) { - if (resource.inShell); - else if (resource.flushed) { + if (resource.state & FlushedInPreamble); + else if (resource.state & Flushed) { + // We only need to emit the href because this resource flushed in an earlier + // boundary already which encoded the attributes necessary to construct + // the resource instance on the client. + writeChunk(destination, nextArrayOpenBrackChunk); + writeStyleResourceDependencyHrefOnlyInJS( + destination, + resource.type === "style" + ? resource.props["data-href"] + : resource.props.href + ); + writeChunk(destination, arrayCloseBracket); + nextArrayOpenBrackChunk = arraySubsequentOpenBracket; + } else if (resource.type === "stylesheet") { + // We need to emit the whole resource for insertion on the client + writeChunk(destination, nextArrayOpenBrackChunk); + writeStyleResourceDependencyInJS( + destination, + resource.props.href, + resource.props["data-precedence"], + resource.props + ); + writeChunk(destination, arrayCloseBracket); + nextArrayOpenBrackChunk = arraySubsequentOpenBracket; + resource.state |= FlushedLate; + } + }); + writeChunk(destination, arrayCloseBracket); +} +/* Helper functions */ + +function writeStyleResourceDependencyHrefOnlyInJS(destination, href) { + // We should actually enforce this earlier when the resource is created but for + // now we make sure we are actually dealing with a string here. + { + checkAttributeStringCoercion(href, "href"); + } + + var coercedHref = "" + href; + writeChunk( + destination, + stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)) + ); +} + +function writeStyleResourceDependencyInJS( + destination, + href, + precedence, + props +) { + { + checkAttributeStringCoercion(href, "href"); + } + + var coercedHref = "" + href; + sanitizeURL(coercedHref); + writeChunk( + destination, + stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)) + ); + + { + checkAttributeStringCoercion(precedence, "precedence"); + } + + var coercedPrecedence = "" + precedence; + writeChunk(destination, arrayInterstitial); + writeChunk( + destination, + stringToChunk(escapeJSObjectForInstructionScripts(coercedPrecedence)) + ); + + for (var propKey in props) { + if (hasOwnProperty.call(props, propKey)) { + var propValue = props[propKey]; + + if (propValue == null) { + continue; + } + + switch (propKey) { + case "href": + case "rel": + case "precedence": + case "data-precedence": { + break; + } + + case "children": + case "dangerouslySetInnerHTML": + throw new Error( + "link" + + " is a self-closing tag and must neither have `children` nor " + + "use `dangerouslySetInnerHTML`." + ); + // eslint-disable-next-line-no-fallthrough + + default: + writeStyleResourceAttributeInJS(destination, propKey, propValue); + break; + } + } + } + + return null; +} + +function writeStyleResourceAttributeInJS(destination, name, value) { + // not null or undefined + var attributeName = name.toLowerCase(); + var attributeValue; + + switch (typeof value) { + case "function": + case "symbol": + return; + } + + switch (name) { + // Reserved names + case "innerHTML": + case "dangerouslySetInnerHTML": + case "suppressContentEditableWarning": + case "suppressHydrationWarning": + case "style": + // Ignored + return; + // Attribute renames + + case "className": + attributeName = "class"; + break; + // Booleans + + case "hidden": + if (value === false) { + return; + } + + attributeValue = ""; + break; + // Santized URLs + + case "src": + case "href": { + { + checkAttributeStringCoercion(value, attributeName); + } + + attributeValue = "" + value; + sanitizeURL(attributeValue); + break; + } + + default: { + if (!isAttributeNameSafe(name)) { + return; + } + } + } + + if ( + // shouldIgnoreAttribute + // We have already filtered out null/undefined and reserved words. + name.length > 2 && + (name[0] === "o" || name[0] === "O") && + (name[1] === "n" || name[1] === "N") + ) { + return; + } + + { + checkAttributeStringCoercion(value, attributeName); + } + + attributeValue = "" + value; + writeChunk(destination, arrayInterstitial); + writeChunk( + destination, + stringToChunk(escapeJSObjectForInstructionScripts(attributeName)) + ); + writeChunk(destination, arrayInterstitial); + writeChunk( + destination, + stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)) + ); +} // This function writes a 2D array of strings to be embedded in an attribute +// value and read with JSON.parse in ReactDOMServerExternalRuntime.js +// E.g. +// [["JSON_escaped_string1", "JSON_escaped_string2"]] + +function writeStyleResourceDependenciesInAttr(destination, boundaryResources) { + writeChunk(destination, arrayFirstOpenBracket); + var nextArrayOpenBrackChunk = arrayFirstOpenBracket; + boundaryResources.forEach(function (resource) { + if (resource.state & FlushedInPreamble); + else if (resource.state & Flushed) { + // We only need to emit the href because this resource flushed in an earlier + // boundary already which encoded the attributes necessary to construct + // the resource instance on the client. writeChunk(destination, nextArrayOpenBrackChunk); - writeStyleResourceDependencyHrefOnlyInJS(destination, resource.href); + writeStyleResourceDependencyHrefOnlyInAttr( + destination, + resource.type === "style" + ? resource.props["data-href"] + : resource.props.href + ); writeChunk(destination, arrayCloseBracket); nextArrayOpenBrackChunk = arraySubsequentOpenBracket; - } else { + } else if (resource.type === "stylesheet") { + // We need to emit the whole resource for insertion on the client writeChunk(destination, nextArrayOpenBrackChunk); - writeStyleResourceDependencyInJS( + writeStyleResourceDependencyInAttr( destination, - resource.href, - resource.precedence, + resource.props.href, + resource.props["data-precedence"], resource.props ); writeChunk(destination, arrayCloseBracket); nextArrayOpenBrackChunk = arraySubsequentOpenBracket; - resource.flushed = true; - resource.hint.flushed = true; + resource.state |= FlushedLate; } }); writeChunk(destination, arrayCloseBracket); } /* Helper functions */ -function writeStyleResourceDependencyHrefOnlyInJS(destination, href) { +function writeStyleResourceDependencyHrefOnlyInAttr(destination, href) { // We should actually enforce this earlier when the resource is created but for // now we make sure we are actually dealing with a string here. { @@ -6409,11 +6327,11 @@ function writeStyleResourceDependencyHrefOnlyInJS(destination, href) { var coercedHref = "" + href; writeChunk( destination, - stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)) + stringToChunk(escapeTextForBrowser(JSON.stringify(coercedHref))) ); } -function writeStyleResourceDependencyInJS( +function writeStyleResourceDependencyInAttr( destination, href, precedence, @@ -6427,7 +6345,7 @@ function writeStyleResourceDependencyInJS( sanitizeURL(coercedHref); writeChunk( destination, - stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)) + stringToChunk(escapeTextForBrowser(JSON.stringify(coercedHref))) ); { @@ -6438,7 +6356,7 @@ function writeStyleResourceDependencyInJS( writeChunk(destination, arrayInterstitial); writeChunk( destination, - stringToChunk(escapeJSObjectForInstructionScripts(coercedPrecedence)) + stringToChunk(escapeTextForBrowser(JSON.stringify(coercedPrecedence))) ); for (var propKey in props) { @@ -6467,7 +6385,7 @@ function writeStyleResourceDependencyInJS( // eslint-disable-next-line-no-fallthrough default: - writeStyleResourceAttributeInJS(destination, propKey, propValue); + writeStyleResourceAttributeInAttr(destination, propKey, propValue); break; } } @@ -6476,7 +6394,7 @@ function writeStyleResourceDependencyInJS( return null; } -function writeStyleResourceAttributeInJS(destination, name, value) { +function writeStyleResourceAttributeInAttr(destination, name, value) { // not null or undefined var attributeName = name.toLowerCase(); var attributeValue; @@ -6548,202 +6466,655 @@ function writeStyleResourceAttributeInJS(destination, name, value) { writeChunk(destination, arrayInterstitial); writeChunk( destination, - stringToChunk(escapeJSObjectForInstructionScripts(attributeName)) + stringToChunk(escapeTextForBrowser(JSON.stringify(attributeName))) ); writeChunk(destination, arrayInterstitial); writeChunk( destination, - stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)) + stringToChunk(escapeTextForBrowser(JSON.stringify(attributeValue))) ); -} // This function writes a 2D array of strings to be embedded in an attribute -// value and read with JSON.parse in ReactDOMServerExternalRuntime.js -// E.g. -// [["JSON_escaped_string1", "JSON_escaped_string2"]] +} +/** + * Resources + */ -function writeStyleResourceDependenciesInAttr(destination, boundaryResources) { - writeChunk(destination, arrayFirstOpenBracket); - var nextArrayOpenBrackChunk = arrayFirstOpenBracket; - boundaryResources.forEach(function (resource) { - if (resource.inShell); - else if (resource.flushed) { - writeChunk(destination, nextArrayOpenBrackChunk); - writeStyleResourceDependencyHrefOnlyInAttr(destination, resource.href); - writeChunk(destination, arrayCloseBracket); - nextArrayOpenBrackChunk = arraySubsequentOpenBracket; - } else { - writeChunk(destination, nextArrayOpenBrackChunk); - writeStyleResourceDependencyInAttr( - destination, - resource.href, - resource.precedence, - resource.props +var NoState = + /* */ + 0; // These tags indicate whether the Resource was flushed and in which phase + +var FlushedInPreamble = + /* */ + 1; +var FlushedLate = + /* */ + 2; +var Flushed = + /* */ + 3; // This tag indicates whether this Resource is blocked from flushing. +// This currently is only used with stylesheets that are blocked by a Boundary + +var Blocked = + /* */ + 4; // This tag indicates whether this Resource has been preloaded. +// This generally only makes sense for Resources other than PreloadResource + +var PreloadFlushed = + /* */ + 8; // Dev extensions. +// Stylesheets and Scripts rendered with jsx +// Preloads, Stylesheets, and Scripts from ReactDOM.preload or ReactDOM.preinit +// Preloads created for normal components we rendered but know we can preload early such as +// sync Scripts and stylesheets without precedence or with onLoad/onError handlers +// @TODO add bootstrap script to implicit preloads + +function createResources() { + return { + // persistent + preloadsMap: new Map(), + stylesMap: new Map(), + scriptsMap: new Map(), + // cleared on flush + fontPreloads: new Set(), + // usedImagePreloads: new Set(), + precedences: new Map(), + usedStylesheets: new Set(), + scripts: new Set(), + usedScripts: new Set(), + explicitStylesheetPreloads: new Set(), + // explicitImagePreloads: new Set(), + explicitScriptPreloads: new Set(), + explicitOtherPreloads: new Set(), + // like a module global for currently rendering boundary + boundaryResources: null + }; +} +function createBoundaryResources() { + return new Set(); +} +function setCurrentlyRenderingBoundaryResourcesTarget( + resources, + boundaryResources +) { + resources.boundaryResources = boundaryResources; +} + +function getResourceKey(as, href) { + return "[" + as + "]" + href; +} + +function preload(href, options) { + if (!currentResources) { + // While we expect that preload calls are primarily going to be observed + // during render because effects and events don't run on the server it is + // still possible that these get called in module scope. This is valid on + // the client since there is still a document to interact with but on the + // server we need a request to associate the call to. Because of this we + // simply return and do not warn. + return; + } + + var resources = currentResources; + + { + if (typeof href !== "string" || !href) { + error( + "ReactDOM.preload(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.", + getValueDescriptorExpectingObjectForWarning(href) ); - writeChunk(destination, arrayCloseBracket); - nextArrayOpenBrackChunk = arraySubsequentOpenBracket; - resource.flushed = true; - resource.hint.flushed = true; + } else if (options == null || typeof options !== "object") { + error( + "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.", + getValueDescriptorExpectingEnumForWarning(options) + ); + } else if (typeof options.as !== "string") { + error( + '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 `` tag are valid here.', + getValueDescriptorExpectingEnumForWarning(options.as) + ); + } + } + + if ( + typeof href === "string" && + href && + typeof options === "object" && + options !== null && + typeof options.as === "string" + ) { + var as = options.as; + var key = getResourceKey(as, href); + var resource = resources.preloadsMap.get(key); + + { + var devResource = getAsResourceDEV(resource); + + if (devResource) { + switch (devResource.__provenance) { + case "preload": { + var differenceDescription = describeDifferencesForPreloads( + options, + devResource.__originalOptions + ); + + if (differenceDescription) { + error( + 'ReactDOM.preload(): The options provided conflict with another call to `ReactDOM.preload("%s", { as: "%s", ...})`.' + + " 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.%s", + href, + as, + differenceDescription + ); + } + + break; + } + + case "implicit": { + var _differenceDescription3 = + describeDifferencesForPreloadOverImplicitPreload( + options, + devResource.__impliedProps + ); + + if (_differenceDescription3) { + var elementDescription = + as === "style" + ? '' + : as === "script" + ? "