From 09b0ca42fb9d809e1417039c444b5957d57104c3 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 3 Oct 2023 14:40:25 -0700 Subject: [PATCH] App Router - preinitialize chunks during SSR (#54752) Today when we hydrate an SSR'd RSC response on the client we encounter import chunks which initiate code loading for client components. However we only start fetching these chunks after hydration has begun which is necessarily after the initial chunks for the entrypoint have loaded. React has upstream changes that need to land which will preinitialize the rendered chunks for all client components used during the SSR pass. This will cause a `'); @@ -1760,12 +1778,16 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1775,17 +1797,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1801,20 +1838,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1847,10 +1896,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3208,7 +3265,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3229,67 +3286,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3297,12 +3345,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3400,47 +3448,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3529,36 +3577,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); + + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3573,25 +3620,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3765,35 +3802,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1761,12 +1779,16 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1776,17 +1798,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1802,20 +1839,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1848,10 +1897,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3209,7 +3266,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3230,67 +3287,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3298,12 +3346,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3401,47 +3449,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3530,36 +3578,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); + + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3574,25 +3621,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3766,35 +3803,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1840,12 +1858,16 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1855,17 +1877,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1881,20 +1918,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1931,10 +1980,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3292,7 +3349,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3313,67 +3370,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3381,12 +3429,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3484,47 +3532,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3613,36 +3661,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); + + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3657,25 +3704,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3849,35 +3886,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1840,12 +1858,16 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1855,17 +1877,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1881,20 +1918,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1931,10 +1980,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3292,7 +3349,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3313,67 +3370,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3381,12 +3429,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3484,47 +3532,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3613,36 +3661,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); + + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3657,25 +3704,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3849,35 +3886,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1823,12 +1841,16 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1838,17 +1860,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1864,20 +1901,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1914,10 +1963,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3275,7 +3332,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3296,67 +3353,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3364,12 +3412,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3467,47 +3515,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3596,36 +3644,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); + + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3640,25 +3687,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3832,35 +3869,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1630,12 +1648,16 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1645,17 +1667,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1671,20 +1708,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1717,10 +1766,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -2938,7 +2995,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -2959,67 +3016,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3027,12 +3075,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3130,47 +3178,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3259,36 +3307,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true + + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3303,25 +3350,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3495,35 +3532,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1631,12 +1649,16 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1646,17 +1668,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1672,20 +1709,32 @@ function createRenderState$1(resumableState, nonce, bootstrapScriptContent, boot for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1718,10 +1767,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -2939,7 +2996,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -2960,67 +3017,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3028,12 +3076,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3131,47 +3179,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3260,36 +3308,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true + + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3304,25 +3351,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3496,35 +3533,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1711,12 +1729,16 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1726,17 +1748,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1752,20 +1789,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1798,10 +1847,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3021,7 +3078,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3042,67 +3099,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3110,12 +3158,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3213,47 +3261,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3342,36 +3390,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true + + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3386,25 +3433,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3578,35 +3615,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1711,12 +1729,16 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1726,17 +1748,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1752,20 +1789,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1798,10 +1847,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3021,7 +3078,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3042,67 +3099,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3110,12 +3158,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3213,47 +3261,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3342,36 +3390,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true + + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3386,25 +3433,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3578,35 +3615,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this '); @@ -1780,12 +1798,16 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map() + }, nonce: nonce, // like a module global for currently rendering boundary boundaryResources: null, @@ -1795,17 +1817,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst if (bootstrapScripts !== undefined) { for (var i = 0; i < bootstrapScripts.length; i++) { var scriptConfig = bootstrapScripts[i]; - var src = typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - var integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - var crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; - preloadBootstrapScript(resumableState, renderState, src, nonce, integrity, crossOrigin); + var src = void 0, + crossOrigin = void 0, + integrity = void 0; + var props = { + rel: 'preload', + as: 'script', + fetchPriority: 'low', + nonce: nonce + }; + + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = typeof scriptConfig.integrity === 'string' ? scriptConfig.integrity : undefined; + props.crossOrigin = crossOrigin = typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null ? undefined : scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push(startScriptSrc, stringToChunk(escapeTextForBrowser(src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity))); } @@ -1821,20 +1858,32 @@ function createRenderState(resumableState, nonce, bootstrapScriptContent, bootst for (var _i = 0; _i < bootstrapModules.length; _i++) { var _scriptConfig = bootstrapModules[_i]; - var _src = typeof _scriptConfig === 'string' ? _scriptConfig : _scriptConfig.src; + var _src = void 0, + _crossOrigin = void 0, + _integrity = void 0; - var _integrity = typeof _scriptConfig === 'string' ? undefined : _scriptConfig.integrity; + var _props = { + rel: 'modulepreload', + fetchPriority: 'low', + nonce: nonce + }; - var _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + if (typeof _scriptConfig === 'string') { + _props.href = _src = _scriptConfig; + } else { + _props.href = _src = _scriptConfig.src; + _props.integrity = _integrity = typeof _scriptConfig.integrity === 'string' ? _scriptConfig.integrity : undefined; + _props.crossOrigin = _crossOrigin = typeof _scriptConfig === 'string' || _scriptConfig.crossOrigin == null ? undefined : _scriptConfig.crossOrigin === 'use-credentials' ? 'use-credentials' : ''; + } - preloadBootstrapModule(resumableState, renderState, _src, nonce, _integrity, _crossOrigin); + preloadBootstrapScriptOrModule(resumableState, renderState, _src, _props); bootstrapChunks.push(startModuleSrc, stringToChunk(escapeTextForBrowser(_src))); if (nonce) { bootstrapChunks.push(scriptNonce, stringToChunk(escapeTextForBrowser(nonce))); } - if (_integrity) { + if (typeof _integrity === 'string') { bootstrapChunks.push(scriptIntegirty, stringToChunk(escapeTextForBrowser(_integrity))); } @@ -1867,10 +1916,18 @@ function createResumableState(identifierPrefix, externalRuntimeConfig) { hasHtml: false, // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {} + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {} + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {} }; } // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion // modes. We only include the variants as they matter for the sake of our purposes. @@ -3090,7 +3147,7 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - var key = getResourceKey('style', href); + var key = getResourceKey(href); if (typeof precedence !== 'string' || props.disabled != null || props.onLoad || props.onError) { // This stylesheet is either not opted into Resource semantics or has conflicting properties which @@ -3111,67 +3168,58 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - var stylesInPrecedence = renderState.precedences.get(precedence); - - if (!resumableState.stylesMap.hasOwnProperty(key)) { - var resourceProps = stylesheetPropsFromRawProps(props); - var state = NoState; + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; + + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); + } - if (resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - var preloadResource = renderState.preloadsMap.get(key); + var resource = { + state: PENDING$1, + props: stylesheetPropsFromRawProps(props) + }; - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + var preloadState = resourceState; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); } - } - var resource = { - type: 'stylesheet', - chunks: [], - state: state, - props: resourceProps - }; - resumableState.stylesMap[key] = null; - - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - var emptyStyleResource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [] - } - }; - stylesInPrecedence.set('', emptyStyleResource); + var preloadResource = renderState.preloads.stylesheets.get(key); - { - if (renderState.stylePrecedences.has(precedence)) { - error('React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', precedence); - } + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } + } // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary - renderState.stylePrecedences.set(precedence, emptyStyleResource); - } - stylesInPrecedence.set(key, resource); + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -3179,12 +3227,12 @@ function pushLink(target, props, resumableState, renderState, textEmbedded, inse // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - var _resource = stylesInPrecedence.get(key); + if (styleQueue) { + var _resource = styleQueue.sheets.get(key); if (_resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(_resource); + renderState.boundaryResources.stylesheets.add(_resource); } } } @@ -3282,47 +3330,47 @@ function pushStyle(target, props, resumableState, renderState, textEmbedded, ins } } - var key = getResourceKey('style', href); - var resource = renderState.stylePrecedences.get(precedence); + var key = getResourceKey(href); + var styleQueue = renderState.styles.get(precedence); + var hasKey = resumableState.styleResources.hasOwnProperty(key); + var resourceState = hasKey ? resumableState.styleResources[key] : undefined; - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence: precedence, - hrefs: [href] - } - }; - renderState.stylePrecedences.set(precedence, resource); - var stylesInPrecedence = new Map(); - stylesInPrecedence.set('', resource); + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; - { - if (renderState.precedences.has(precedence)) { - error('React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', precedence); - } + { + if (resourceState) { + error('React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.', href); } + } - renderState.precedences.set(precedence, stylesInPrecedence); + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: [], + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: new Map() + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -3411,36 +3459,35 @@ function pushStyleContents(target, props) { return; } -function getImagePreloadKey(href, imageSrcSet, imageSizes) { - var uniquePart = ''; - - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - - return getResourceKey('image', uniquePart); -} - function pushImg(target, props, resumableState, renderState, pictureTagInScope) { var src = props.src, srcSet = props.srcSet; - if (props.loading !== 'lazy' && (typeof src === 'string' || typeof srcSet === 'string') && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded + if (props.loading !== 'lazy' && (src || srcSet) && (typeof src === 'string' || src == null) && (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded !(typeof src === 'string' && src[4] === ':' && (src[0] === 'd' || src[0] === 'D') && (src[1] === 'a' || src[1] === 'A') && (src[2] === 't' || src[2] === 'T') && (src[3] === 'a' || src[3] === 'A')) && !(typeof srcSet === 'string' && srcSet[4] === ':' && (srcSet[0] === 'd' || srcSet[0] === 'D') && (srcSet[1] === 'a' || srcSet[1] === 'A') && (srcSet[2] === 't' || srcSet[2] === 'T') && (srcSet[3] === 'a' || srcSet[3] === 'A'))) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - var sizes = props.sizes; - var key = getImagePreloadKey(src, srcSet, sizes); - var resource; + var sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + var key = getImageResourceKey(src, srcSet, sizes); + var promotablePreloads = renderState.preloads.images; + var resource = promotablePreloads.get(key); - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - var preloadProps = { + if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); // $FlowFixMe - Flow should understand that this is a Resource if the condition was true + + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl(resource, { rel: 'preload', as: 'image', // There is a bug in Safari where imageSrcSet is not respected on preload links @@ -3455,25 +3502,15 @@ function pushImg(target, props, resumableState, renderState, pictureTagInScope) type: props.type, fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + }); - if (resource) { if (props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10) { renderState.highImagePreloads.add(resource); } else { - renderState.bulkPreloads.add(resource); + renderState.bulkPreloads.add(resource); // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + + promotablePreloads.set(key, resource); } } } @@ -3647,35 +3684,50 @@ function pushScript(target, props, resumableState, renderState, textEmbedded, in } var src = props.src; - var key = getResourceKey('script', src); // We can make this