148
hoistableChunks: Array<Chunk | PrecomputedChunk>,
149
150
// Flushing queues for Resource dependencies
151
- preconnects: Set<PreconnectResource>,
152
- fontPreloads: Set<PreloadResource>,
153
- highImagePreloads: Set<PreloadResource>,
151
+ preconnects: Set<Resource>,
152
+ fontPreloads: Set<Resource>,
153
+ highImagePreloads: Set<Resource>,
154
// usedImagePreloads: Set<PreloadResource>,
155
- precedences: Map<string, Map<string, StyleResource>>,
156
- stylePrecedences: Map<string, StyleTagResource>,
157
- bootstrapScripts: Set<PreloadResource>,
158
- scripts: Set<ScriptResource>,
159
- bulkPreloads: Set<PreloadResource>,
155
+ styles: Map<string, StyleQueue>,
156
+ bootstrapScripts: Set<Resource>,
157
+ scripts: Set<Resource>,
158
+ bulkPreloads: Set<Resource>,
159
160
// Temporarily keeps track of key to preload resources before shell flushes.
162
- preloadsMap: Map<string, PreloadResource>,
161
+ preloads: {
162
+ images: Map<string, Resource>,
163
+ stylesheets: Map<string, Resource>,
164
+ scripts: Map<string, Resource>,
165
+ moduleScripts: Map<string, Resource>,
166
+ },
167
168
// Module-global-like reference for current boundary resources
169
boundaryResources: ?BoundaryResources,
178
...
179
};
180
181
+type Exists = null;
182
+type Preloaded = [];
183
+// Credentials here are things that affect whether a browser will make a request
184
+// as well as things that affect which connection the browser will use for that request.
185
+// We want these to be aligned across preloads and resources because otherwise the preload
186
+// will be wasted.
187
+// We investigated whether referrerPolicy should be included here but from experimentation
188
+// it seems that browsers do not treat this as part of the http cache key and does not affect
189
+// which connection is used.
190
+type PreloadedWithCredentials = [
191
+ /* crossOrigin */ ?string,
192
+ /* integrity */ ?string,
193
+];
194
+
195
+const EXISTS: Exists = null;
196
+// This constant is to mark preloads that have no unique credentials
197
+// to convey. It should never be checked by identity and we should not
198
+// assume Preload values in ResumableState equal this value because they
199
+// will have come from some parsed input.
200
+const PRELOAD_NO_CREDS: Preloaded = [];
201
+if (__DEV__) {
202
+ Object.freeze(PRELOAD_NO_CREDS);
203
+}
204
+
205
// Per response, global state that is not contextual to the rendering subtree.
206
// This is resumable and therefore should be serializable.
207
export type ResumableState = {
217
hasHtml: boolean,
218
219
// Resources - Request local cache
192
- preloadsMap: {[key: string]: PreloadProps},
193
- preconnectsMap: {[key: string]: null},
194
- stylesMap: {[key: string]: null},
195
- scriptsMap: {[key: string]: null},
220
+ unknownResources: {
221
+ [asType: string]: {
222
+ [href: string]: Preloaded,
223
+ },
224
+ },
225
+ dnsResources: {[key: string]: Exists},
226
+ connectResources: {
227
+ default: {[key: string]: Exists},
228
+ anonymous: {[key: string]: Exists},
229
+ credentials: {[key: string]: Exists},
230
+ },
231
+ imageResources: {
232
+ [key: string]: Preloaded,
233
+ },
234
+ styleResources: {
235
+ [key: string]: Exists | Preloaded | PreloadedWithCredentials,
236
+ },
237
+ scriptResources: {
238
+ [key: string]: Exists | Preloaded | PreloadedWithCredentials,
239
+ },
240
+ moduleUnknownResources: {
241
+ [asType: string]: {
242
+ [href: string]: Preloaded,
243
+ },
244
+ },
245
+ moduleScriptResources: {
246
+ [key: string]: Exists | Preloaded | PreloadedWithCredentials,
247
+ },
248
};
249
250
const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');
394
fontPreloads: new Set(),
395
highImagePreloads: new Set(),
396
// usedImagePreloads: new Set(),
345
- precedences: new Map(),
346
- stylePrecedences: new Map(),
397
+ styles: new Map(),
398
bootstrapScripts: new Set(),
399
scripts: new Set(),
400
bulkPreloads: new Set(),
401
351
- preloadsMap: new Map(),
402
+ preloads: {
403
+ images: new Map(),
404
+ stylesheets: new Map(),
405
+ scripts: new Map(),
406
+ moduleScripts: new Map(),
407
+ },
408
409
nonce,
410
// like a module global for currently rendering boundary
415
if (bootstrapScripts !== undefined) {
416
for (let i = 0; i < bootstrapScripts.length; i++) {
417
const scriptConfig = bootstrapScripts[i];
362
- const src =
363
- typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
364
- const integrity =
365
- typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
366
- const crossOrigin =
367
- typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
368
- ? undefined
369
- : scriptConfig.crossOrigin === 'use-credentials'
370
- ? 'use-credentials'
371
- : '';
372
-
373
- preloadBootstrapScript(
374
- resumableState,
375
- renderState,
376
- src,
418
+ let src, crossOrigin, integrity;
419
+ const props: PreloadAsProps = ({
420
+ rel: 'preload',
421
+ as: 'script',
422
+ fetchPriority: 'low',
423
nonce,
378
- integrity,
379
- crossOrigin,
380
- );
424
+ }: any);
425
+ if (typeof scriptConfig === 'string') {
426
+ props.href = src = scriptConfig;
427
+ } else {
428
+ props.href = src = scriptConfig.src;
429
+ props.integrity = integrity =
430
+ typeof scriptConfig.integrity === 'string'
431
+ ? scriptConfig.integrity
432
+ : undefined;
433
+ props.crossOrigin = crossOrigin =
434
+ typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
435
+ ? undefined
436
+ : scriptConfig.crossOrigin === 'use-credentials'
437
+ ? 'use-credentials'
438
+ : '';
439
+ }
440
+
441
+ preloadBootstrapScriptOrModule(resumableState, renderState, src, props);
442
443
bootstrapChunks.push(
444
startScriptSrc,
450
stringToChunk(escapeTextForBrowser(nonce)),
451
);
452
}
392
- if (integrity) {
453
+ if (typeof integrity === 'string') {
454
bootstrapChunks.push(
455
scriptIntegirty,
456
stringToChunk(escapeTextForBrowser(integrity)),
468
if (bootstrapModules !== undefined) {
469
for (let i = 0; i < bootstrapModules.length; i++) {
470
const scriptConfig = bootstrapModules[i];
410
- const src =
411
- typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
412
- const integrity =
413
- typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
414
- const crossOrigin =
415
- typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
416
- ? undefined
417
- : scriptConfig.crossOrigin === 'use-credentials'
418
- ? 'use-credentials'
419
- : '';
420
-
421
- preloadBootstrapModule(
422
- resumableState,
423
- renderState,
424
- src,
471
+ let src, crossOrigin, integrity;
472
+ const props: PreloadModuleProps = ({
473
+ rel: 'modulepreload',
474
+ fetchPriority: 'low',
475
nonce,
426
- integrity,
427
- crossOrigin,
428
- );
476
+ }: any);
477
+ if (typeof scriptConfig === 'string') {
478
+ props.href = src = scriptConfig;
479
+ } else {
480
+ props.href = src = scriptConfig.src;
481
+ props.integrity = integrity =
482
+ typeof scriptConfig.integrity === 'string'
483
+ ? scriptConfig.integrity
484
+ : undefined;
485
+ props.crossOrigin = crossOrigin =
486
+ typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
487
+ ? undefined
488
+ : scriptConfig.crossOrigin === 'use-credentials'
489
+ ? 'use-credentials'
490
+ : '';
491
+ }
492
+
493
+ preloadBootstrapScriptOrModule(resumableState, renderState, src, props);
494
495
bootstrapChunks.push(
496
startModuleSrc,
503
stringToChunk(escapeTextForBrowser(nonce)),
504
);
505
}
441
- if (integrity) {
506
+ if (typeof integrity === 'string') {
507
bootstrapChunks.push(
508
scriptIntegirty,
509
stringToChunk(escapeTextForBrowser(integrity)),
561
// @TODO add bootstrap script to implicit preloads
562
563
// persistent
499
- preloadsMap: {},
500
- preconnectsMap: {},
501
- stylesMap: {},
502
- scriptsMap: {},
564
+ unknownResources: {},
565
+ dnsResources: {},
566
+ connectResources: {
567
+ default: {},
568
+ anonymous: {},
569
+ credentials: {},
570
+ },
571
+ imageResources: {},
572
+ styleResources: {},
573
+ scriptResources: {},
574
+ moduleUnknownResources: {},
575
+ moduleScriptResources: {},
576
};
577
}
578
2175
2176
if (props.rel === 'stylesheet') {
2177
// This <link> may hoistable as a Stylesheet Resource, otherwise it will emit in place
2105
- const key = getResourceKey('style', href);
2178
+ const key = getResourceKey(href);
2179
if (
2180
typeof precedence !== 'string' ||
2181
props.disabled != null ||
2209
return pushLinkImpl(target, props);
2210
} else {
2211
// This stylesheet refers to a Resource and we create a new one if necessary
2139
- let stylesInPrecedence = renderState.precedences.get(precedence);
2140
- if (!resumableState.stylesMap.hasOwnProperty(key)) {
2141
- const resourceProps = stylesheetPropsFromRawProps(props);
2142
- let state = NoState;
2143
- if (resumableState.preloadsMap.hasOwnProperty(key)) {
2144
- const preloadProps: PreloadProps = resumableState.preloadsMap[key];
2145
- adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps);
2146
- const preloadResource = renderState.preloadsMap.get(key);
2147
- if (preloadResource) {
2148
- // If we already had a preload we don't want that resource to flush directly.
2149
- // We let the newly created resource govern flushing.
2150
- preloadResource.state |= Blocked;
2151
- if (preloadResource.state & Flushed) {
2152
- state = PreloadFlushed;
2153
- }
2154
- } else {
2155
- // If we resumed then we assume that this was already flushed
2156
- // by the shell.
2157
- state = PreloadFlushed;
2158
- }
2212
+ let styleQueue = renderState.styles.get(precedence);
2213
+ const hasKey = resumableState.styleResources.hasOwnProperty(key);
2214
+ const resourceState = hasKey
2215
+ ? resumableState.styleResources[key]
2216
+ : undefined;
2217
+ if (resourceState !== EXISTS) {
2218
+ // We are going to create this resource now so it is marked as Exists
2219
+ resumableState.styleResources[key] = EXISTS;
2220
+
2221
+ // If this is the first time we've encountered this precedence we need
2222
+ // to create a StyleQueue
2223
+ if (!styleQueue) {
2224
+ styleQueue = {
2225
+ precedence: stringToChunk(escapeTextForBrowser(precedence)),
2226
+ rules: ([]: Array<Chunk | PrecomputedChunk>),
2227
+ hrefs: ([]: Array<Chunk | PrecomputedChunk>),
2228
+ sheets: (new Map(): Map<string, StylesheetResource>),
2229
+ };
2230
+ renderState.styles.set(precedence, styleQueue);
2231
}
2160
- const resource = {
2161
- type: 'stylesheet',
2162
- chunks: ([]: Array<Chunk | PrecomputedChunk>),
2163
- state,
2164
- props: resourceProps,
2232
+
2233
+ const resource: StylesheetResource = {
2234
+ state: PENDING,
2235
+ props: stylesheetPropsFromRawProps(props),
2236
};
2166
- resumableState.stylesMap[key] = null;
2167
- if (!stylesInPrecedence) {
2168
- stylesInPrecedence = new Map();
2169
- renderState.precedences.set(precedence, stylesInPrecedence);
2170
- const emptyStyleResource = {
2171
- type: 'style',
2172
- chunks: ([]: Array<Chunk | PrecomputedChunk>),
2173
- state: NoState,
2174
- props: {
2175
- precedence,
2176
- hrefs: ([]: Array<string>),
2177
- },
2178
- };
2179
- stylesInPrecedence.set('', emptyStyleResource);
2180
- if (__DEV__) {
2181
- if (renderState.stylePrecedences.has(precedence)) {
2182
- console.error(
2183
- 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
2184
- precedence,
2185
- );
2186
- }
2237
+
2238
+ if (resourceState) {
2239
+ // When resourceState is truty it is a Preload state. We cast it for clarity
2240
+ const preloadState: Preloaded | PreloadedWithCredentials =
2241
+ resourceState;
2242
+ if (preloadState.length === 2) {
2243
+ adoptPreloadCredentials(resource.props, preloadState);
2244
+ }
2245
+
2246
+ const preloadResource = renderState.preloads.stylesheets.get(key);
2247
+ if (preloadResource && preloadResource.length > 0) {
2248
+ // The Preload for this resource was created in this render pass and has not flushed yet so
2249
+ // we need to clear it to avoid it flushing.
2250
+ preloadResource.length = 0;
2251
+ } else {
2252
+ // Either the preload resource from this render already flushed in this render pass
2253
+ // or the preload flushed in a prior pass (prerender). In either case we need to mark
2254
+ // this resource as already having been preloaded.
2255
+ resource.state = PRELOADED;
2256
}
2188
- renderState.stylePrecedences.set(precedence, emptyStyleResource);
2257
+ } else {
2258
+ // We don't need to check whether a preloadResource exists in the renderState
2259
+ // because if it did exist then the resourceState would also exist and we would
2260
+ // have hit the primary if condition above.
2261
}
2190
- stylesInPrecedence.set(key, resource);
2262
+
2263
+ // We add the newly created resource to our StyleQueue and if necessary
2264
+ // track the resource with the currently rendering boundary
2265
+ styleQueue.sheets.set(key, resource);
2266
if (renderState.boundaryResources) {
2192
- renderState.boundaryResources.add(resource);
2267
+ renderState.boundaryResources.stylesheets.add(resource);
2268
}
2269
} else {
2270
// We need to track whether this boundary should wait on this resource or not.
2272
// it. However, it's possible when you resume that the style has already been emitted
2273
// and then it wouldn't be recreated in the RenderState and there's no need to track
2274
// it again since we should've hoisted it to the shell already.
2200
- if (stylesInPrecedence) {
2201
- const resource = stylesInPrecedence.get(key);
2275
+ if (styleQueue) {
2276
+ const resource = styleQueue.sheets.get(key);
2277
if (resource) {
2278
if (renderState.boundaryResources) {
2204
- renderState.boundaryResources.add(resource);
2279
+ renderState.boundaryResources.stylesheets.add(resource);
2280
}
2281
}
2282
}
2409
}
2410
}
2411
2337
- const key = getResourceKey('style', href);
2338
- let resource = renderState.stylePrecedences.get(precedence);
2339
- if (!resumableState.stylesMap.hasOwnProperty(key)) {
2340
- if (!resource) {
2341
- resource = {
2342
- type: 'style',
2343
- chunks: [],
2344
- state: NoState,
2345
- props: {
2346
- precedence,
2347
- hrefs: [href],
2348
- },
2349
- };
2350
- renderState.stylePrecedences.set(precedence, resource);
2351
- const stylesInPrecedence: Map<string, StyleResource> = new Map();
2352
- stylesInPrecedence.set('', resource);
2353
- if (__DEV__) {
2354
- if (renderState.precedences.has(precedence)) {
2355
- console.error(
2356
- 'React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.',
2357
- precedence,
2358
- );
2359
- }
2412
+ const key = getResourceKey(href);
2413
+ let styleQueue = renderState.styles.get(precedence);
2414
+ const hasKey = resumableState.styleResources.hasOwnProperty(key);
2415
+ const resourceState = hasKey
2416
+ ? resumableState.styleResources[key]
2417
+ : undefined;
2418
+ if (resourceState !== EXISTS) {
2419
+ // We are going to create this resource now so it is marked as Exists
2420
+ resumableState.styleResources[key] = EXISTS;
2421
+
2422
+ if (__DEV__) {
2423
+ if (resourceState) {
2424
+ console.error(
2425
+ '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.',
2426
+ href,
2427
+ );
2428
}
2361
- renderState.precedences.set(precedence, stylesInPrecedence);
2429
+ }
2430
+
2431
+ if (!styleQueue) {
2432
+ // This is the first time we've encountered this precedence we need
2433
+ // to create a StyleQueue.
2434
+ styleQueue = {
2435
+ precedence: stringToChunk(escapeTextForBrowser(precedence)),
2436
+ rules: ([]: Array<Chunk | PrecomputedChunk>),
2437
+ hrefs: [stringToChunk(escapeTextForBrowser(href))],
2438
+ sheets: (new Map(): Map<string, StylesheetResource>),
2439
+ };
2440
+ renderState.styles.set(precedence, styleQueue);
2441
} else {
2363
- resource.props.hrefs.push(href);
2442
+ // We have seen this precedence before and need to track this href
2443
+ styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href)));
2444
}
2365
- resumableState.stylesMap[key] = null;
2366
- pushStyleContents(resource.chunks, props);
2445
+ pushStyleContents(styleQueue.rules, props);
2446
}
2368
- if (resource) {
2447
+ if (styleQueue) {
2448
// We need to track whether this boundary should wait on this resource or not.
2449
// Typically this resource should always exist since we either had it or just created
2450
// it. However, it's possible when you resume that the style has already been emitted
2451
// and then it wouldn't be recreated in the RenderState and there's no need to track
2452
// it again since we should've hoisted it to the shell already.
2453
if (renderState.boundaryResources) {
2375
- renderState.boundaryResources.add(resource);
2454
+ renderState.boundaryResources.styles.add(styleQueue);
2455
}
2456
}
2457
2554
return;
2555
}
2556
2478
-function getImagePreloadKey(
2479
- href: string,
2480
- imageSrcSet: ?string,
2481
- imageSizes: ?string,
2482
-) {
2483
- let uniquePart = '';
2484
- if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
2485
- uniquePart += '[' + imageSrcSet + ']';
2486
- if (typeof imageSizes === 'string') {
2487
- uniquePart += '[' + imageSizes + ']';
2488
- }
2489
- } else {
2490
- uniquePart += '[][]' + href;
2491
- }
2492
- return getResourceKey('image', uniquePart);
2493
-}
2494
-
2557
function pushImg(
2558
target: Array<Chunk | PrecomputedChunk>,
2559
props: Object,
2564
const {src, srcSet} = props;
2565
if (
2566
props.loading !== 'lazy' &&
2505
- (typeof src === 'string' || typeof srcSet === 'string') &&
2567
+ (src || srcSet) &&
2568
+ (typeof src === 'string' || src == null) &&
2569
+ (typeof srcSet === 'string' || srcSet == null) &&
2570
props.fetchPriority !== 'low' &&
2571
pictureTagInScope === false &&
2572
// We exclude data URIs in src and srcSet since these should not be preloaded
2589
) {
2590
// We have a suspensey image and ought to preload it to optimize the loading of display blocking
2591
// resumableState.
2528
- const {sizes} = props;
2529
- const key = getImagePreloadKey(src, srcSet, sizes);
2530
- let resource: void | PreloadResource;
2531
- if (!resumableState.preloadsMap.hasOwnProperty(key)) {
2532
- const preloadProps: PreloadProps = {
2533
- rel: 'preload',
2534
- as: 'image',
2535
- // There is a bug in Safari where imageSrcSet is not respected on preload links
2536
- // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2537
- // This harms older browers that do not support imageSrcSet by making their preloads not work
2538
- // but this population is shrinking fast and is already small so we accept this tradeoff.
2539
- href: srcSet ? undefined : src,
2540
- imageSrcSet: srcSet,
2541
- imageSizes: sizes,
2542
- crossOrigin: props.crossOrigin,
2543
- integrity: props.integrity,
2544
- type: props.type,
2545
- fetchPriority: props.fetchPriority,
2546
- referrerPolicy: props.referrerPolicy,
2547
- };
2548
- resource = {
2549
- type: 'preload',
2550
- chunks: [],
2551
- state: NoState,
2552
- props: preloadProps,
2553
- };
2554
- resumableState.preloadsMap[key] = preloadProps;
2555
- renderState.preloadsMap.set(key, resource);
2556
- pushLinkImpl(resource.chunks, preloadProps);
2557
- } else {
2558
- resource = renderState.preloadsMap.get(key);
2559
- }
2592
+ const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
2593
+ const key = getImageResourceKey(src, srcSet, sizes);
2594
+
2595
+ const promotablePreloads = renderState.preloads.images;
2596
+
2597
+ let resource = promotablePreloads.get(key);
2598
if (resource) {
2599
+ // We consider whether this preload can be promoted to higher priority flushing queue.
2600
+ // The only time a resource will exist here is if it was created during this render
2601
+ // and was not already in the high priority queue.
2602
+ if (
2603
+ props.fetchPriority === 'high' ||
2604
+ renderState.highImagePreloads.size < 10
2605
+ ) {
2606
+ // Delete the resource from the map since we are promoting it and don't want to
2607
+ // reenter this branch in a second pass for duplicate img hrefs.
2608
+ promotablePreloads.delete(key);
2609
+
2610
+ // $FlowFixMe - Flow should understand that this is a Resource if the condition was true
2611
+ renderState.highImagePreloads.add(resource);
2612
+ }
2613
+ } else if (!resumableState.imageResources.hasOwnProperty(key)) {
2614
+ // We must construct a new preload resource
2615
+ resumableState.imageResources[key] = PRELOAD_NO_CREDS;
2616
+ resource = [];
2617
+ pushLinkImpl(
2618
+ resource,
2619
+ ({
2620
+ rel: 'preload',
2621
+ as: 'image',
2622
+ // There is a bug in Safari where imageSrcSet is not respected on preload links
2623
+ // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2624
+ // This harms older browers that do not support imageSrcSet by making their preloads not work
2625
+ // but this population is shrinking fast and is already small so we accept this tradeoff.
2626
+ href: srcSet ? undefined : src,
2627
+ imageSrcSet: srcSet,
2628
+ imageSizes: sizes,
2629
+ crossOrigin: props.crossOrigin,
2630
+ integrity: props.integrity,
2631
+ type: props.type,
2632
+ fetchPriority: props.fetchPriority,
2633
+ referrerPolicy: props.referrerPolicy,
2634
+ }: PreloadProps),
2635
+ );
2636
if (
2637
props.fetchPriority === 'high' ||
2638
renderState.highImagePreloads.size < 10
2640
renderState.highImagePreloads.add(resource);
2641
} else {
2642
renderState.bulkPreloads.add(resource);
2643
+ // We can bump the priority up if the same img is rendered later
2644
+ // with fetchPriority="high"
2645
+ promotablePreloads.set(key, resource);
2646
}
2647
}
2648
}
2977
}
2978
2979
const src = props.src;
2902
- const key = getResourceKey('script', src);
2980
+ const key = getResourceKey(src);
2981
// We can make this <script> into a ScriptResource
2904
- if (!resumableState.scriptsMap.hasOwnProperty(key)) {
2905
- const resource: ScriptResource = {
2906
- type: 'script',
2907
- chunks: [],
2908
- state: NoState,
2909
- props: null,
2910
- };
2911
- resumableState.scriptsMap[key] = null;
2912
- // Add to the script flushing queue
2913
- renderState.scripts.add(resource);
2982
+
2983
+ let resources, preloads;
2984
+ if (props.type === 'module') {
2985
+ resources = resumableState.moduleScriptResources;
2986
+ preloads = renderState.preloads.moduleScripts;
2987
+ } else {
2988
+ resources = resumableState.scriptResources;
2989
+ preloads = renderState.preloads.scripts;
2990
+ }
2991
+
2992
+ const hasKey = resources.hasOwnProperty(key);
2993
+ const resourceState = hasKey ? resources[key] : undefined;
2994
+ if (resourceState !== EXISTS) {
2995
+ // We are going to create this resource now so it is marked as Exists
2996
+ resources[key] = EXISTS;
2997
2998
let scriptProps = props;
2916
- if (resumableState.preloadsMap.hasOwnProperty(key)) {
2917
- const preloadProps: PreloadProps = resumableState.preloadsMap[key];
2918
- scriptProps = {...props};
2919
- adoptPreloadPropsForScriptProps(scriptProps, preloadProps);
2920
- const preloadResource = renderState.preloadsMap.get(key);
2999
+ if (resourceState) {
3000
+ // When resourceState is truty it is a Preload state. We cast it for clarity
3001
+ const preloadState: Preloaded | PreloadedWithCredentials =
3002
+ resourceState;
3003
+ if (preloadState.length === 2) {
3004
+ scriptProps = {...props};
3005
+ adoptPreloadCredentials(scriptProps, preloadState);
3006
+ }
3007
+
3008
+ const preloadResource = preloads.get(key);
3009
if (preloadResource) {
2922
- // If we already had a preload we don't want that resource to flush directly.
2923
- // We let the newly created resource govern flushing.
2924
- preloadResource.state |= Blocked;
3010
+ // the preload resource exists was created in this render. Now that we have
3011
+ // a script resource which will emit earlier than a preload would if it
3012
+ // hasn't already flushed we prevent it from flushing by zeroing the length
3013
+ preloadResource.length = 0;
3014
}
3015
}
3016
+
3017
+ const resource: Resource = [];
3018
+ // Add to the script flushing queue
3019
+ renderState.scripts.add(resource);
3020
// encode the tag as Chunks
2928
- pushScriptImpl(resource.chunks, scriptProps);
3021
+ pushScriptImpl(resource, scriptProps);
3022
}
3023
3024
if (textEmbedded) {
4223
4224
function flushStyleTagsLateForBoundary(
4225
this: Destination,
4133
- resource: StyleResource,
4226
+ styleQueue: StyleQueue,
4227
) {
4135
- if (
4136
- resource.type === 'stylesheet' &&
4137
- (resource.state & FlushedInPreamble) === NoState
4138
- ) {
4139
- currentlyRenderingBoundaryHasStylesToHoist = true;
4140
- } else if (resource.type === 'style') {
4141
- const chunks = resource.chunks;
4142
- const hrefs = resource.props.hrefs;
4143
- let i = 0;
4144
- if (chunks.length) {
4145
- writeChunk(this, lateStyleTagResourceOpen1);
4146
- writeChunk(
4147
- this,
4148
- stringToChunk(escapeTextForBrowser(resource.props.precedence)),
4149
- );
4150
- if (hrefs.length) {
4151
- writeChunk(this, lateStyleTagResourceOpen2);
4152
- for (; i < hrefs.length - 1; i++) {
4153
- writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4154
- writeChunk(this, spaceSeparator);
4155
- }
4156
- writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4157
- }
4158
- writeChunk(this, lateStyleTagResourceOpen3);
4159
- for (i = 0; i < chunks.length; i++) {
4160
- writeChunk(this, chunks[i]);
4161
- }
4162
- destinationHasCapacity = writeChunkAndReturn(
4163
- this,
4164
- lateStyleTagTemplateClose,
4228
+ const rules = styleQueue.rules;
4229
+ const hrefs = styleQueue.hrefs;
4230
+ if (__DEV__) {
4231
+ if (rules.length > 0 && hrefs.length === 0) {
4232
+ console.error(
4233
+ 'React expected to have at least one href for an a hoistable style but found none. This is a bug in React.',
4234
);
4235
+ }
4236
+ }
4237
+ let i = 0;
4238
+ if (hrefs.length) {
4239
+ writeChunk(this, lateStyleTagResourceOpen1);
4240
+ writeChunk(this, styleQueue.precedence);
4241
+ writeChunk(this, lateStyleTagResourceOpen2);
4242
+ for (; i < hrefs.length - 1; i++) {
4243
+ writeChunk(this, hrefs[i]);
4244
+ writeChunk(this, spaceSeparator);
4245
+ }
4246
+ writeChunk(this, hrefs[i]);
4247
+ writeChunk(this, lateStyleTagResourceOpen3);
4248
+ for (i = 0; i < rules.length; i++) {
4249
+ writeChunk(this, rules[i]);
4250
+ }
4251
+ destinationHasCapacity = writeChunkAndReturn(
4252
+ this,
4253
+ lateStyleTagTemplateClose,
4254
+ );
4255
4167
- // We wrote style tags for this boundary and we may need to emit a script
4168
- // to hoist them.
4169
- currentlyRenderingBoundaryHasStylesToHoist = true;
4256
+ // We wrote style tags for this boundary and we may need to emit a script
4257
+ // to hoist them.
4258
+ currentlyRenderingBoundaryHasStylesToHoist = true;
4259
4171
- // style resources can flush continuously since more rules may be written into
4172
- // them with new hrefs. Instead of marking it flushed, we simply reset the chunks
4173
- // and hrefs
4174
- chunks.length = 0;
4175
- hrefs.length = 0;
4176
- }
4260
+ // style resources can flush continuously since more rules may be written into
4261
+ // them with new hrefs. Instead of marking it flushed, we simply reset the chunks
4262
+ // and hrefs
4263
+ rules.length = 0;
4264
+ hrefs.length = 0;
4265
+ }
4266
+}
4267
+
4268
+function hasStylesToHoist(stylesheet: StylesheetResource): boolean {
4269
+ // We need to reveal boundaries with styles whenever a stylesheet it depends on is either
4270
+ // not flushed or flushed after the preamble (shell).
4271
+ if (stylesheet.state !== PREAMBLE) {
4272
+ currentlyRenderingBoundaryHasStylesToHoist = true;
4273
+ return true;
4274
}
4275
+ return false;
4276
}
4277
4278
export function writeResourcesForBoundary(
4284
currentlyRenderingBoundaryHasStylesToHoist = false;
4285
destinationHasCapacity = true;
4286
4189
- // Flush each Boundary resource
4190
- boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
4287
+ // Flush style tags for each precedence this boundary depends on
4288
+ boundaryResources.styles.forEach(flushStyleTagsLateForBoundary, destination);
4289
+
4290
+ // Determine if this boundary has stylesheets that need to be awaited upon completion
4291
+ boundaryResources.stylesheets.forEach(hasStylesToHoist);
4292
+
4293
if (currentlyRenderingBoundaryHasStylesToHoist) {
4294
renderState.stylesToHoist = true;
4295
}
4296
return destinationHasCapacity;
4297
}
4298
4197
-function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
4198
- if ((resource.state & (Flushed | Blocked)) === NoState) {
4199
- const chunks = resource.chunks;
4200
- for (let i = 0; i < chunks.length; i++) {
4201
- writeChunk(this, chunks[i]);
4202
- }
4203
- resource.state |= FlushedInPreamble;
4204
- }
4205
-}
4206
-
4207
-function flushResourceLate<T: Resource>(this: Destination, resource: T) {
4208
- if ((resource.state & (Flushed | Blocked)) === NoState) {
4209
- const chunks = resource.chunks;
4210
- for (let i = 0; i < chunks.length; i++) {
4211
- writeChunk(this, chunks[i]);
4212
- }
4213
- resource.state |= FlushedLate;
4299
+function flushResource(this: Destination, resource: Resource) {
4300
+ for (let i = 0; i < resource.length; i++) {
4301
+ writeChunk(this, resource[i]);
4302
}
4303
+ resource.length = 0;
4304
}
4305
4217
-// This must always be read after flushing stylesheet styles. we know we will encounter a style resource
4218
-// per precedence and it will be set before ready so we cast this to avoid an extra check at runtime
4219
-let precedenceStyleTagResource: StyleTagResource = (null: any);
4220
-
4221
-// This flags let's us opt out of flushing a placeholder style tag to emit the precedence in the right order.
4222
-// If a stylesheet was flushed then we have the precedence order preserved and only need to emit <style> tags
4223
-// if there are actual chunks to flush
4224
-let didFlushPrecedence = false;
4306
+const stylesheetFlushingQueue: Array<Chunk | PrecomputedChunk> = [];
4307
4308
function flushStyleInPreamble(
4309
this: Destination,
4228
- resource: StyleResource,
4310
+ stylesheet: StylesheetResource,
4311
key: string,
4230
- map: Map<string, StyleResource>,
4312
+ map: Map<string, StylesheetResource>,
4313
) {
4232
- const chunks = resource.chunks;
4233
- if (resource.state & Flushed) {
4234
- // In theory this should never happen because we clear from the
4235
- // Set on flush but to ensure correct semantics we don't emit
4236
- // anything if we are in this state.
4237
- map.delete(key);
4238
- } else {
4239
- // We can emit this style or stylesheet as is.
4240
- if (resource.type === 'style') {
4241
- precedenceStyleTagResource = resource;
4242
- return;
4243
- }
4244
-
4245
- // We still need to encode stylesheet chunks
4246
- // because unlike most Hoistables and Resources we do not eagerly encode
4247
- // them during render. This is because if we flush late we have to send a
4248
- // different encoding and we don't want to encode multiple times
4249
- pushLinkImpl(chunks, resource.props);
4250
- for (let i = 0; i < chunks.length; i++) {
4251
- writeChunk(this, chunks[i]);
4252
- }
4253
- resource.state |= FlushedInPreamble;
4254
- didFlushPrecedence = true;
4255
- }
4314
+ // We still need to encode stylesheet chunks
4315
+ // because unlike most Hoistables and Resources we do not eagerly encode
4316
+ // them during render. This is because if we flush late we have to send a
4317
+ // different encoding and we don't want to encode multiple times
4318
+ pushLinkImpl(stylesheetFlushingQueue, stylesheet.props);
4319
+ for (let i = 0; i < stylesheetFlushingQueue.length; i++) {
4320
+ writeChunk(this, stylesheetFlushingQueue[i]);
4321
+ }
4322
+ stylesheetFlushingQueue.length = 0;
4323
+ stylesheet.state = PREAMBLE;
4324
}
4325
4326
const styleTagResourceOpen1 = stringToPrecomputedChunk(
4332
4333
const styleTagResourceClose = stringToPrecomputedChunk('</style>');
4334
4267
-function flushAllStylesInPreamble(
4335
+function flushStylesInPreamble(
4336
this: Destination,
4269
- map: Map<string, StyleResource>,
4337
+ styleQueue: StyleQueue,
4338
precedence: string,
4339
) {
4272
- didFlushPrecedence = false;
4273
- map.forEach(flushStyleInPreamble, this);
4274
- map.clear();
4275
-
4276
- const chunks = precedenceStyleTagResource.chunks;
4277
- const hrefs = precedenceStyleTagResource.props.hrefs;
4278
- if (didFlushPrecedence === false || chunks.length) {
4340
+ const hasStylesheets = styleQueue.sheets.size > 0;
4341
+ styleQueue.sheets.forEach(flushStyleInPreamble, this);
4342
+ styleQueue.sheets.clear();
4343
+
4344
+ const rules = styleQueue.rules;
4345
+ const hrefs = styleQueue.hrefs;
4346
+ // If we don't emit any stylesheets at this precedence we still need to maintain the precedence
4347
+ // order so even if there are no rules for style tags at this precedence we emit an empty style
4348
+ // tag with the data-precedence attribute
4349
+ if (!hasStylesheets || hrefs.length) {
4350
writeChunk(this, styleTagResourceOpen1);
4280
- writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
4351
+ writeChunk(this, styleQueue.precedence);
4352
let i = 0;
4353
if (hrefs.length) {
4354
writeChunk(this, styleTagResourceOpen2);
4355
for (; i < hrefs.length - 1; i++) {
4285
- writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4356
+ writeChunk(this, hrefs[i]);
4357
writeChunk(this, spaceSeparator);
4358
}
4288
- writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4359
+ writeChunk(this, hrefs[i]);
4360
}
4361
writeChunk(this, styleTagResourceOpen3);
4291
- for (i = 0; i < chunks.length; i++) {
4292
- writeChunk(this, chunks[i]);
4362
+ for (i = 0; i < rules.length; i++) {
4363
+ writeChunk(this, rules[i]);
4364
}
4365
writeChunk(this, styleTagResourceClose);
4366
4367
// style resources can flush continuously since more rules may be written into
4368
// them with new hrefs. Instead of marking it flushed, we simply reset the chunks
4369
// and hrefs
4299
- chunks.length = 0;
4370
+ rules.length = 0;
4371
hrefs.length = 0;
4372
}
4373
}
4374
4304
-function preloadLateStyle(this: Destination, resource: StyleResource) {
4305
- if (resource.state & PreloadFlushed) {
4306
- // This resource has already had a preload flushed
4307
- return;
4308
- }
4309
-
4310
- if (resource.type === 'style') {
4311
- // <style> tags do not need to be preloaded
4312
- return;
4313
- }
4314
-
4315
- const chunks = resource.chunks;
4316
- const preloadProps = preloadAsStylePropsFromProps(
4317
- resource.props.href,
4318
- resource.props,
4319
- );
4320
- pushLinkImpl(chunks, preloadProps);
4321
- for (let i = 0; i < chunks.length; i++) {
4322
- writeChunk(this, chunks[i]);
4375
+function preloadLateStyle(this: Destination, stylesheet: StylesheetResource) {
4376
+ if (stylesheet.state === PENDING) {
4377
+ stylesheet.state = PRELOADED;
4378
+ const preloadProps = preloadAsStylePropsFromProps(
4379
+ stylesheet.props.href,
4380
+ stylesheet.props,
4381
+ );
4382
+ pushLinkImpl(stylesheetFlushingQueue, preloadProps);
4383
+ for (let i = 0; i < stylesheetFlushingQueue.length; i++) {
4384
+ writeChunk(this, stylesheetFlushingQueue[i]);
4385
+ }
4386
+ stylesheetFlushingQueue.length = 0;
4387
}
4324
- resource.state |= PreloadFlushed;
4325
- chunks.length = 0;
4388
}
4389
4328
-function preloadLateStyles(
4329
- this: Destination,
4330
- map: Map<string, StyleResource>,
4331
- precedence: string,
4332
-) {
4333
- map.forEach(preloadLateStyle, this);
4334
- map.clear();
4390
+function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
4391
+ styleQueue.sheets.forEach(preloadLateStyle, this);
4392
+ styleQueue.sheets.clear();
4393
}
4394
4395
// We don't bother reporting backpressure at the moment because we expect to
4452
charsetChunks.length = 0;
4453
4454
// emit preconnect resources
4397
- renderState.preconnects.forEach(flushResourceInPreamble, destination);
4455
+ renderState.preconnects.forEach(flushResource, destination);
4456
renderState.preconnects.clear();
4457
4458
const preconnectChunks = renderState.preconnectChunks;
4461
}
4462
preconnectChunks.length = 0;
4463
4406
- renderState.fontPreloads.forEach(flushResourceInPreamble, destination);
4464
+ renderState.fontPreloads.forEach(flushResource, destination);
4465
renderState.fontPreloads.clear();
4466
4409
- renderState.highImagePreloads.forEach(flushResourceInPreamble, destination);
4467
+ renderState.highImagePreloads.forEach(flushResource, destination);
4468
renderState.highImagePreloads.clear();
4469
4470
// Flush unblocked stylesheets by precedence
4413
- renderState.precedences.forEach(flushAllStylesInPreamble, destination);
4471
+ renderState.styles.forEach(flushStylesInPreamble, destination);
4472
4473
const importMapChunks = renderState.importMapChunks;
4474
for (i = 0; i < importMapChunks.length; i++) {
4476
}
4477
importMapChunks.length = 0;
4478
4421
- renderState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
4479
+ renderState.bootstrapScripts.forEach(flushResource, destination);
4480
4423
- renderState.scripts.forEach(flushResourceInPreamble, destination);
4481
+ renderState.scripts.forEach(flushResource, destination);
4482
renderState.scripts.clear();
4483
4426
- renderState.bulkPreloads.forEach(flushResourceInPreamble, destination);
4484
+ renderState.bulkPreloads.forEach(flushResource, destination);
4485
renderState.bulkPreloads.clear();
4486
4487
// Write embedding preloadChunks
4527
// We omit charsetChunks because we have already sent the shell and if it wasn't
4528
// already sent it is too late now.
4529
4472
- renderState.preconnects.forEach(flushResourceLate, destination);
4530
+ renderState.preconnects.forEach(flushResource, destination);
4531
renderState.preconnects.clear();
4532
4533
const preconnectChunks = renderState.preconnectChunks;
4536
}
4537
preconnectChunks.length = 0;
4538
4481
- renderState.fontPreloads.forEach(flushResourceLate, destination);
4539
+ renderState.fontPreloads.forEach(flushResource, destination);
4540
renderState.fontPreloads.clear();
4541
4484
- renderState.highImagePreloads.forEach(flushResourceInPreamble, destination);
4542
+ renderState.highImagePreloads.forEach(flushResource, destination);
4543
renderState.highImagePreloads.clear();
4544
4545
// Preload any stylesheets. these will emit in a render instruction that follows this
4546
// but we want to kick off preloading as soon as possible
4489
- renderState.precedences.forEach(preloadLateStyles, destination);
4547
+ renderState.styles.forEach(preloadLateStyles, destination);
4548
4549
// We only hoist importmaps that are configured through createResponse and that will
4550
// always flush in the preamble. Generally we don't expect people to render them as
4554
// bootstrap scripts should flush above script priority but these can only flush in the preamble
4555
// so we elide the code here for performance
4556
4499
- renderState.scripts.forEach(flushResourceLate, destination);
4557
+ renderState.scripts.forEach(flushResource, destination);
4558
renderState.scripts.clear();
4559
4502
- renderState.bulkPreloads.forEach(flushResourceLate, destination);
4560
+ renderState.bulkPreloads.forEach(flushResource, destination);
4561
renderState.bulkPreloads.clear();
4562
4563
// Write embedding preloadChunks
4606
writeChunk(destination, arrayFirstOpenBracket);
4607
4608
let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
4551
- boundaryResources.forEach(resource => {
4552
- if (resource.type === 'style') {
4553
- // Style dependencies don't require coordinated reveal and can be omitted
4554
- } else if (resource.state & FlushedInPreamble) {
4609
+ boundaryResources.stylesheets.forEach(resource => {
4610
+ if (resource.state === PREAMBLE) {
4611
// We can elide this dependency because it was flushed in the shell and
4612
// should be ready before content is shown on the client
4557
- } else if (resource.state & Flushed) {
4613
+ } else if (resource.state === LATE) {
4614
// We only need to emit the href because this resource flushed in an earlier
4615
// boundary already which encoded the attributes necessary to construct
4616
// the resource instance on the client.
4621
);
4622
writeChunk(destination, arrayCloseBracket);
4623
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4568
- } else if (resource.type === 'stylesheet') {
4624
+ } else {
4625
// We need to emit the whole resource for insertion on the client
4626
writeChunk(destination, nextArrayOpenBrackChunk);
4627
writeStyleResourceDependencyInJS(
4633
writeChunk(destination, arrayCloseBracket);
4634
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4635
4580
- resource.state |= FlushedLate;
4636
+ resource.state = LATE;
4637
}
4638
});
4639
writeChunk(destination, arrayCloseBracket);
4799
writeChunk(destination, arrayFirstOpenBracket);
4800
4801
let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
4746
- boundaryResources.forEach(resource => {
4747
- if (resource.type === 'style') {
4748
- // Style dependencies don't require coordinated reveal and can be omitted
4749
- } else if (resource.state & FlushedInPreamble) {
4802
+ boundaryResources.stylesheets.forEach(resource => {
4803
+ if (resource.state === PREAMBLE) {
4804
// We can elide this dependency because it was flushed in the shell and
4805
// should be ready before content is shown on the client
4752
- } else if (resource.state & Flushed) {
4806
+ } else if (resource.state === LATE) {
4807
// We only need to emit the href because this resource flushed in an earlier
4808
// boundary already which encoded the attributes necessary to construct
4809
// the resource instance on the client.
4814
);
4815
writeChunk(destination, arrayCloseBracket);
4816
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4763
- } else if (resource.type === 'stylesheet') {
4817
+ } else {
4818
// We need to emit the whole resource for insertion on the client
4819
writeChunk(destination, nextArrayOpenBrackChunk);
4820
writeStyleResourceDependencyInAttr(
4826
writeChunk(destination, arrayCloseBracket);
4827
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4828
4775
- resource.state |= FlushedLate;
4829
+ resource.state = LATE;
4830
}
4831
});
4832
writeChunk(destination, arrayCloseBracket);
4987
* Resources
4988
*/
4989
4936
-type ResourceStateTag = number;
4937
-const NoState /* */ = 0b0000;
4938
-// These tags indicate whether the Resource was flushed and in which phase
4939
-const FlushedInPreamble /* */ = 0b0001;
4940
-const FlushedLate /* */ = 0b0010;
4941
-const Flushed /* */ = 0b0011;
4942
-// This tag indicates whether this Resource is blocked from flushing.
4943
-// This currently is only used with stylesheets that are blocked by a Boundary
4944
-const Blocked /* */ = 0b0100;
4945
-// This tag indicates whether this Resource has been preloaded.
4946
-// This generally only makes sense for Resources other than PreloadResource
4947
-const PreloadFlushed /* */ = 0b1000;
4948
-
4949
-type TResource<
4950
- T: 'stylesheet' | 'style' | 'script' | 'preload' | 'preconnect',
4951
- P,
4952
-> = {
4953
- type: T,
4954
- chunks: Array<Chunk | PrecomputedChunk>,
4955
- state: ResourceStateTag,
4956
- props: P,
4957
-};
4990
+type StylesheetState = 0 | 1 | 2 | 3;
4991
+const PENDING: StylesheetState = 0;
4992
+const PRELOADED: StylesheetState = 1;
4993
+const PREAMBLE: StylesheetState = 2;
4994
+const LATE: StylesheetState = 3;
4995
4996
type PreconnectProps = {
4997
rel: 'preconnect' | 'dns-prefetch',
4998
href: string,
4999
[string]: mixed,
5000
};
4964
-type PreconnectResource = TResource<'preconnect', null>;
5001
5002
type PreloadAsProps = {
5003
rel: 'preload',
5011
[string]: ?string,
5012
};
5013
type PreloadProps = PreloadAsProps | PreloadModuleProps;
4978
-type PreloadResource = TResource<'preload', PreloadProps>;
4979
-
4980
-type StylesheetProps = {
4981
- rel: 'stylesheet',
4982
- href: string,
4983
- 'data-precedence': string,
4984
- [string]: mixed,
4985
-};
4986
-type StylesheetResource = TResource<'stylesheet', StylesheetProps>;
4987
-
4988
-type StyleTagProps = {
4989
- hrefs: Array<string>,
4990
- precedence: string,
4991
-};
4992
-type StyleTagResource = TResource<'style', StyleTagProps>;
4993
-
4994
-type StyleResource = StylesheetResource | StyleTagResource;
5014
5015
type ScriptProps = {
5016
async: true,
5017
src: string,
5018
[string]: mixed,
5019
};
5001
-type ModuleProps = {
5020
+type ModuleScriptProps = {
5021
async: true,
5022
src: string,
5023
type: 'module',
5024
[string]: mixed,
5025
};
5007
-type ScriptResource = TResource<'script', null>;
5026
5009
-type Resource =
5010
- | StyleResource
5011
- | ScriptResource
5012
- | PreloadResource
5013
- | PreconnectResource;
5027
+export type Resource = Array<Chunk | PrecomputedChunk>;
5028
+
5029
+type StylesheetProps = {
5030
+ rel: 'stylesheet',
5031
+ href: string,
5032
+ 'data-precedence': string,
5033
+ [string]: mixed,
5034
+};
5035
+type StylesheetResource = {
5036
+ props: StylesheetProps,
5037
+ state: StylesheetState,
5038
+};
5039
+
5040
+export type BoundaryResources = {
5041
+ styles: Set<StyleQueue>,
5042
+ stylesheets: Set<StylesheetResource>,
5043
+};
5044
5015
-export type BoundaryResources = Set<StyleResource>;
5045
+export type StyleQueue = {
5046
+ precedence: Chunk | PrecomputedChunk,
5047
+ rules: Array<Chunk | PrecomputedChunk>,
5048
+ hrefs: Array<Chunk | PrecomputedChunk>,
5049
+ sheets: Map<string, StylesheetResource>,
5050
+};
5051
5052
export function createBoundaryResources(): BoundaryResources {
5018
- return new Set();
5053
+ return {
5054
+ styles: new Set(),
5055
+ stylesheets: new Set(),
5056
+ };
5057
}
5058
5059
export function setCurrentlyRenderingBoundaryResourcesTarget(
5063
renderState.boundaryResources = boundaryResources;
5064
}
5065
5028
-function getResourceKey(as: string, href: string): string {
5029
- return `[${as}]${href}`;
5066
+function getResourceKey(href: string): string {
5067
+ return href;
5068
+}
5069
+
5070
+function getImageResourceKey(
5071
+ href: string,
5072
+ imageSrcSet?: ?string,
5073
+ imageSizes?: ?string,
5074
+): string {
5075
+ if (imageSrcSet) {
5076
+ return imageSrcSet + '\n' + (imageSizes || '');
5077
+ }
5078
+ return href;
5079
}
5080
5081
function prefetchDNS(href: string) {
5095
const renderState = getRenderState(request);
5096
5097
if (typeof href === 'string' && href) {
5049
- const key = getResourceKey('prefetchDNS', href);
5050
- if (!resumableState.preconnectsMap.hasOwnProperty(key)) {
5051
- const resource: PreconnectResource = {
5052
- type: 'preconnect',
5053
- chunks: [],
5054
- state: NoState,
5055
- props: null,
5056
- };
5057
- resumableState.preconnectsMap[key] = null;
5058
- pushLinkImpl(
5059
- resource.chunks,
5060
- ({href, rel: 'dns-prefetch'}: PreconnectProps),
5061
- );
5098
+ const key = getResourceKey(href);
5099
+ if (!resumableState.dnsResources.hasOwnProperty(key)) {
5100
+ const resource: Resource = [];
5101
+ resumableState.dnsResources[key] = EXISTS;
5102
+ pushLinkImpl(resource, ({href, rel: 'dns-prefetch'}: PreconnectProps));
5103
renderState.preconnects.add(resource);
5104
}
5105
flushResources(request);
5123
const renderState = getRenderState(request);
5124
5125
if (typeof href === 'string' && href) {
5085
- const key = `[preconnect][${
5086
- typeof crossOrigin === 'string' ? crossOrigin : 'null'
5087
- }]${href}`;
5088
- if (!resumableState.preconnectsMap.hasOwnProperty(key)) {
5089
- const resource: PreconnectResource = {
5090
- type: 'preconnect',
5091
- chunks: [],
5092
- state: NoState,
5093
- props: null,
5094
- };
5095
- resumableState.preconnectsMap[key] = null;
5126
+ const resources =
5127
+ crossOrigin === 'use-credentials'
5128
+ ? resumableState.connectResources.credentials
5129
+ : typeof crossOrigin === 'string'
5130
+ ? resumableState.connectResources.anonymous
5131
+ : resumableState.connectResources.default;
5132
+ const key = getResourceKey(href);
5133
+ if (!resources.hasOwnProperty(key)) {
5134
+ const resource: Resource = [];
5135
+ resources[key] = EXISTS;
5136
pushLinkImpl(
5097
- resource.chunks,
5137
+ resource,
5138
({rel: 'preconnect', href, crossOrigin}: PreconnectProps),
5139
);
5140
renderState.preconnects.add(resource);
5159
const resumableState = getResumableState(request);
5160
const renderState = getRenderState(request);
5161
if (as && href) {
5122
- options = options || {};
5123
- let key: string;
5124
- if (as === 'image') {
5125
- // For image preloads the key contains either the imageSrcSet + imageSizes or the href but not
5126
- // both. This is to prevent identical calls with the same srcSet and sizes to be duplicated
5127
- // by varying the href. this is an edge case but it is the most correct behavior.
5128
- key = getImagePreloadKey(href, options.imageSrcSet, options.imageSizes);
5129
- } else {
5130
- key = getResourceKey(as, href);
5131
- }
5132
- if (!resumableState.preloadsMap.hasOwnProperty(key)) {
5133
- const props = Object.assign(
5134
- ({
5135
- rel: 'preload',
5136
- href: as === 'image' && options.imageSrcSet ? undefined : href,
5137
- as,
5138
- }: PreloadProps),
5139
- options,
5140
- );
5141
- const resource: PreloadResource = {
5142
- type: 'preload',
5143
- chunks: [],
5144
- state: NoState,
5145
- props,
5146
- };
5147
- resumableState.preloadsMap[key] = props;
5148
- renderState.preloadsMap.set(key, resource);
5149
- pushLinkImpl(resource.chunks, props);
5150
- if (as === 'font') {
5151
- renderState.fontPreloads.add(resource);
5152
- } else if (as === 'image' && resource.props.fetchPriority === 'high') {
5153
- renderState.highImagePreloads.add(resource);
5154
- } else {
5162
+ switch (as) {
5163
+ case 'image': {
5164
+ let imageSrcSet, imageSizes, fetchPriority;
5165
+ if (options) {
5166
+ imageSrcSet = options.imageSrcSet;
5167
+ imageSizes = options.imageSizes;
5168
+ fetchPriority = options.fetchPriority;
5169
+ }
5170
+ const key = getImageResourceKey(href, imageSrcSet, imageSizes);
5171
+ if (resumableState.imageResources.hasOwnProperty(key)) {
5172
+ // we can return if we already have this resource
5173
+ return;
5174
+ }
5175
+ resumableState.imageResources[key] = PRELOAD_NO_CREDS;
5176
+ const resource = ([]: Resource);
5177
+ pushLinkImpl(
5178
+ resource,
5179
+ Object.assign(
5180
+ ({
5181
+ rel: 'preload',
5182
+ // There is a bug in Safari where imageSrcSet is not respected on preload links
5183
+ // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
5184
+ // This harms older browers that do not support imageSrcSet by making their preloads not work
5185
+ // but this population is shrinking fast and is already small so we accept this tradeoff.
5186
+ href: imageSrcSet ? undefined : href,
5187
+ as,
5188
+ }: PreloadAsProps),
5189
+ options,
5190
+ ),
5191
+ );
5192
+ if (fetchPriority === 'high') {
5193
+ renderState.highImagePreloads.add(resource);
5194
+ } else {
5195
+ renderState.bulkPreloads.add(resource);
5196
+ // Stash the resource in case we need to promote it to higher priority
5197
+ // when an img tag is rendered
5198
+ renderState.preloads.images.set(key, resource);
5199
+ }
5200
+ break;
5201
+ }
5202
+ case 'style': {
5203
+ const key = getResourceKey(href);
5204
+ if (resumableState.styleResources.hasOwnProperty(key)) {
5205
+ // we can return if we already have this resource
5206
+ return;
5207
+ }
5208
+ const resource = ([]: Resource);
5209
+ pushLinkImpl(
5210
+ resource,
5211
+ Object.assign(({rel: 'preload', href, as}: PreloadAsProps), options),
5212
+ );
5213
+ resumableState.styleResources[key] =
5214
+ options &&
5215
+ (typeof options.crossOrigin === 'string' ||
5216
+ typeof options.integrity === 'string')
5217
+ ? [options.crossOrigin, options.integrity]
5218
+ : PRELOAD_NO_CREDS;
5219
+ renderState.preloads.stylesheets.set(key, resource);
5220
renderState.bulkPreloads.add(resource);
5221
+ break;
5222
+ }
5223
+ case 'script': {
5224
+ const key = getResourceKey(href);
5225
+ if (resumableState.scriptResources.hasOwnProperty(key)) {
5226
+ // we can return if we already have this resource
5227
+ return;
5228
+ }
5229
+ const resource = ([]: Resource);
5230
+ renderState.preloads.scripts.set(key, resource);
5231
+ renderState.bulkPreloads.add(resource);
5232
+ pushLinkImpl(
5233
+ resource,
5234
+ Object.assign(({rel: 'preload', href, as}: PreloadAsProps), options),
5235
+ );
5236
+ resumableState.scriptResources[key] =
5237
+ options &&
5238
+ (typeof options.crossOrigin === 'string' ||
5239
+ typeof options.integrity === 'string')
5240
+ ? [options.crossOrigin, options.integrity]
5241
+ : PRELOAD_NO_CREDS;
5242
+ break;
5243
+ }
5244
+ default: {
5245
+ const key = getResourceKey(href);
5246
+ const hasAsType = resumableState.unknownResources.hasOwnProperty(as);
5247
+ let resources;
5248
+ if (hasAsType) {
5249
+ resources = resumableState.unknownResources[as];
5250
+ if (resources.hasOwnProperty(key)) {
5251
+ // we can return if we already have this resource
5252
+ return;
5253
+ }
5254
+ } else {
5255
+ resources = ({}: ResumableState['unknownResources']['asType']);
5256
+ resumableState.unknownResources[as] = resources;
5257
+ }
5258
+ const resource = ([]: Resource);
5259
+ const props = Object.assign(
5260
+ ({
5261
+ rel: 'preload',
5262
+ href,
5263
+ as,
5264
+ }: PreloadAsProps),
5265
+ options,
5266
+ );
5267
+ switch (as) {
5268
+ case 'font':
5269
+ renderState.fontPreloads.add(resource);
5270
+ break;
5271
+ // intentional fall through
5272
+ default:
5273
+ renderState.bulkPreloads.add(resource);
5274
+ }
5275
+ pushLinkImpl(resource, props);
5276
+ resources[key] = PRELOAD_NO_CREDS;
5277
}
5278
}
5279
+ // If we got this far we created a new resource
5280
flushResources(request);
5281
}
5282
}
5300
const resumableState = getResumableState(request);
5301
const renderState = getRenderState(request);
5302
if (href) {
5303
+ const key = getResourceKey(href);
5304
const as =
5305
options && typeof options.as === 'string' ? options.as : 'script';
5183
- const key = getResourceKey(as, href);
5184
- if (!resumableState.preloadsMap.hasOwnProperty(key)) {
5185
- const props: PreloadModuleProps = Object.assign(
5306
+
5307
+ let resource;
5308
+ switch (as) {
5309
+ case 'script': {
5310
+ if (resumableState.moduleScriptResources.hasOwnProperty(key)) {
5311
+ // we can return if we already have this resource
5312
+ return;
5313
+ }
5314
+ resource = ([]: Resource);
5315
+ resumableState.moduleScriptResources[key] =
5316
+ options &&
5317
+ (typeof options.crossOrigin === 'string' ||
5318
+ typeof options.integrity === 'string')
5319
+ ? [options.crossOrigin, options.integrity]
5320
+ : PRELOAD_NO_CREDS;
5321
+ renderState.preloads.moduleScripts.set(key, resource);
5322
+ break;
5323
+ }
5324
+ default: {
5325
+ const hasAsType =
5326
+ resumableState.moduleUnknownResources.hasOwnProperty(as);
5327
+ let resources;
5328
+ if (hasAsType) {
5329
+ resources = resumableState.unknownResources[as];
5330
+ if (resources.hasOwnProperty(key)) {
5331
+ // we can return if we already have this resource
5332
+ return;
5333
+ }
5334
+ } else {
5335
+ resources = ({}: ResumableState['moduleUnknownResources']['asType']);
5336
+ resumableState.moduleUnknownResources[as] = resources;
5337
+ }
5338
+ resource = ([]: Resource);
5339
+ resources[key] = PRELOAD_NO_CREDS;
5340
+ }
5341
+ }
5342
+
5343
+ pushLinkImpl(
5344
+ resource,
5345
+ Object.assign(
5346
({
5347
rel: 'modulepreload',
5348
href,
5349
}: PreloadModuleProps),
5350
options,
5191
- );
5192
- const resource: PreloadResource = {
5193
- type: 'preload',
5194
- chunks: [],
5195
- state: NoState,
5196
- props,
5197
- };
5198
- resumableState.preloadsMap[key] = props;
5199
- renderState.preloadsMap.set(key, resource);
5200
- pushLinkImpl(resource.chunks, resource.props);
5201
- renderState.bulkPreloads.add(resource);
5202
- }
5351
+ ),
5352
+ );
5353
+ renderState.bulkPreloads.add(resource);
5354
+ // If we got this far we created a new resource
5355
flushResources(request);
5356
}
5357
}
5376
const resumableState = getResumableState(request);
5377
const renderState = getRenderState(request);
5378
if (href) {
5227
- const as = 'style';
5228
- const key = getResourceKey(as, href);
5229
- if (!resumableState.stylesMap.hasOwnProperty(key)) {
5230
- precedence = precedence || 'default';
5231
- let state = NoState;
5232
- const preloadResource = renderState.preloadsMap.get(key);
5233
- if (preloadResource && preloadResource.state & Flushed) {
5234
- state = PreloadFlushed;
5235
- } else if (resumableState.preloadsMap.hasOwnProperty(key)) {
5236
- // If we resumed then we assume that this was already flushed
5237
- // by the shell.
5238
- state = PreloadFlushed;
5239
- }
5240
- const props: StylesheetProps = Object.assign(
5241
- ({
5242
- rel: 'stylesheet',
5243
- href,
5244
- 'data-precedence': precedence,
5245
- }: StylesheetProps),
5246
- options,
5247
- );
5379
+ precedence = precedence || 'default';
5380
+ const key = getResourceKey(href);
5381
+
5382
+ let styleQueue = renderState.styles.get(precedence);
5383
+ const hasKey = resumableState.styleResources.hasOwnProperty(key);
5384
+ const resourceState = hasKey
5385
+ ? resumableState.styleResources[key]
5386
+ : undefined;
5387
+ if (resourceState !== EXISTS) {
5388
+ // We are going to create this resource now so it is marked as Exists
5389
+ resumableState.styleResources[key] = EXISTS;
5390
+
5391
+ // If this is the first time we've encountered this precedence we need
5392
+ // to create a StyleQueue
5393
+ if (!styleQueue) {
5394
+ styleQueue = {
5395
+ precedence: stringToChunk(escapeTextForBrowser(precedence)),
5396
+ rules: ([]: Array<Chunk | PrecomputedChunk>),
5397
+ hrefs: ([]: Array<Chunk | PrecomputedChunk>),
5398
+ sheets: (new Map(): Map<string, StylesheetResource>),
5399
+ };
5400
+ renderState.styles.set(precedence, styleQueue);
5401
+ }
5402
+
5403
const resource = {
5249
- type: 'stylesheet',
5250
- chunks: ([]: Array<Chunk | PrecomputedChunk>),
5251
- state,
5252
- props,
5404
+ state: PENDING,
5405
+ props: Object.assign(
5406
+ ({
5407
+ rel: 'stylesheet',
5408
+ href,
5409
+ 'data-precedence': precedence,
5410
+ }: StylesheetProps),
5411
+ options,
5412
+ ),
5413
};
5254
- resumableState.stylesMap[key] = null;
5255
- let stylesInPrecedence = renderState.precedences.get(precedence);
5256
- if (!stylesInPrecedence) {
5257
- stylesInPrecedence = new Map();
5258
- renderState.precedences.set(precedence, stylesInPrecedence);
5259
- const emptyStyleResource = {
5260
- type: 'style',
5261
- chunks: ([]: Array<Chunk | PrecomputedChunk>),
5262
- state: NoState,
5263
- props: {
5264
- precedence,
5265
- hrefs: ([]: Array<string>),
5266
- },
5267
- };
5268
- stylesInPrecedence.set('', emptyStyleResource);
5269
- if (__DEV__) {
5270
- if (renderState.stylePrecedences.has(precedence)) {
5271
- console.error(
5272
- 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
5273
- precedence,
5274
- );
5275
- }
5414
+
5415
+ if (resourceState) {
5416
+ // When resourceState is truty it is a Preload state. We cast it for clarity
5417
+ const preloadState: Preloaded | PreloadedWithCredentials =
5418
+ resourceState;
5419
+ if (preloadState.length === 2) {
5420
+ adoptPreloadCredentials(resource.props, preloadState);
5421
}
5277
- renderState.stylePrecedences.set(precedence, emptyStyleResource);
5422
+
5423
+ const preloadResource = renderState.preloads.stylesheets.get(key);
5424
+ if (preloadResource && preloadResource.length > 0) {
5425
+ // The Preload for this resource was created in this render pass and has not flushed yet so
5426
+ // we need to clear it to avoid it flushing.
5427
+ preloadResource.length = 0;
5428
+ } else {
5429
+ // Either the preload resource from this render already flushed in this render pass
5430
+ // or the preload flushed in a prior pass (prerender). In either case we need to mark
5431
+ // this resource as already having been preloaded.
5432
+ resource.state = PRELOADED;
5433
+ }
5434
+ } else {
5435
+ // We don't need to check whether a preloadResource exists in the renderState
5436
+ // because if it did exist then the resourceState would also exist and we would
5437
+ // have hit the primary if condition above.
5438
}
5279
- stylesInPrecedence.set(key, resource);
5439
+
5440
+ // We add the newly created resource to our StyleQueue and if necessary
5441
+ // track the resource with the currently rendering boundary
5442
+ styleQueue.sheets.set(key, resource);
5443
+
5444
+ // Notify the request that there are resources to flush even if no work is currently happening
5445
flushResources(request);
5446
}
5282
- return;
5447
}
5448
}
5449
5463
const resumableState = getResumableState(request);
5464
const renderState = getRenderState(request);
5465
if (src) {
5302
- const key = getResourceKey('script', src);
5303
- if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5304
- const resource: ScriptResource = {
5305
- type: 'script',
5306
- chunks: [],
5307
- state: NoState,
5308
- props: null,
5309
- };
5310
- resumableState.scriptsMap[key] = null;
5466
+ const key = getResourceKey(src);
5467
+
5468
+ const hasKey = resumableState.scriptResources.hasOwnProperty(key);
5469
+ const resourceState = hasKey
5470
+ ? resumableState.scriptResources[key]
5471
+ : undefined;
5472
+ if (resourceState !== EXISTS) {
5473
+ // We are going to create this resource now so it is marked as Exists
5474
+ resumableState.scriptResources[key] = EXISTS;
5475
+
5476
const props: ScriptProps = Object.assign(
5477
({
5478
src,
5480
}: ScriptProps),
5481
options,
5482
);
5483
+ if (resourceState) {
5484
+ // When resourceState is truty it is a Preload state. We cast it for clarity
5485
+ const preloadState: Preloaded | PreloadedWithCredentials =
5486
+ resourceState;
5487
+ if (preloadState.length === 2) {
5488
+ adoptPreloadCredentials(props, preloadState);
5489
+ }
5490
+
5491
+ const preloadResource = renderState.preloads.scripts.get(key);
5492
+ if (preloadResource) {
5493
+ // the preload resource exists was created in this render. Now that we have
5494
+ // a script resource which will emit earlier than a preload would if it
5495
+ // hasn't already flushed we prevent it from flushing by zeroing the length
5496
+ preloadResource.length = 0;
5497
+ }
5498
+ }
5499
+
5500
+ const resource: Resource = [];
5501
+ // Add to the script flushing queue
5502
renderState.scripts.add(resource);
5319
- pushScriptImpl(resource.chunks, props);
5503
+ // encode the tag as Chunks
5504
+ pushScriptImpl(resource, props);
5505
+ // Notify the request that there are resources to flush even if no work is currently happening
5506
flushResources(request);
5507
}
5508
return;
5528
const resumableState = getResumableState(request);
5529
const renderState = getRenderState(request);
5530
if (src) {
5345
- const key = getResourceKey('script', src);
5346
- if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5347
- const resource: ScriptResource = {
5348
- type: 'script',
5349
- chunks: [],
5350
- state: NoState,
5351
- props: null,
5352
- };
5353
- resumableState.scriptsMap[key] = null;
5531
+ const key = getResourceKey(src);
5532
+ const hasKey = resumableState.moduleScriptResources.hasOwnProperty(key);
5533
+ const resourceState = hasKey
5534
+ ? resumableState.moduleScriptResources[key]
5535
+ : undefined;
5536
+ if (resourceState !== EXISTS) {
5537
+ // We are going to create this resource now so it is marked as Exists
5538
+ resumableState.moduleScriptResources[key] = EXISTS;
5539
+
5540
const props = Object.assign(
5541
({
5542
src,
5543
type: 'module',
5544
async: true,
5359
- }: ModuleProps),
5545
+ }: ModuleScriptProps),
5546
options,
5547
);
5548
+ if (resourceState) {
5549
+ // When resourceState is truty it is a Preload state. We cast it for clarity
5550
+ const preloadState: Preloaded | PreloadedWithCredentials =
5551
+ resourceState;
5552
+ if (preloadState.length === 2) {
5553
+ adoptPreloadCredentials(props, preloadState);
5554
+ }
5555
+
5556
+ const preloadResource = renderState.preloads.moduleScripts.get(key);
5557
+ if (preloadResource) {
5558
+ // the preload resource exists was created in this render. Now that we have
5559
+ // a script resource which will emit earlier than a preload would if it
5560
+ // hasn't already flushed we prevent it from flushing by zeroing the length
5561
+ preloadResource.length = 0;
5562
+ }
5563
+ }
5564
+
5565
+ const resource: Resource = [];
5566
+ // Add to the script flushing queue
5567
renderState.scripts.add(resource);
5363
- pushScriptImpl(resource.chunks, props);
5568
+ // encode the tag as Chunks
5569
+ pushScriptImpl(resource, props);
5570
+ // Notify the request that there are resources to flush even if no work is currently happening
5571
flushResources(request);
5572
}
5573
return;
5575
}
5576
5577
// This function is only safe to call at Request start time since it assumes
5371
-// that each script has not already been preloaded. If we find a need to preload
5578
+// that each module has not already been preloaded. If we find a need to preload
5579
// scripts at any other point in time we will need to check whether the preload
5580
// already exists and not assume it
5374
-function preloadBootstrapScript(
5581
+function preloadBootstrapScriptOrModule(
5582
resumableState: ResumableState,
5583
renderState: RenderState,
5377
- src: string,
5378
- nonce: ?string,
5379
- integrity: ?string,
5380
- crossOrigin: ?string,
5584
+ href: string,
5585
+ props: PreloadProps,
5586
): void {
5382
- const key = getResourceKey('script', src);
5383
- if (__DEV__) {
5384
- if (resumableState.preloadsMap.hasOwnProperty(key)) {
5385
- // This is coded as a React error because it should be impossible for a userspace preload to preempt this call
5386
- // If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
5387
- // rather than instruct the user to not preload their bootstrap scripts themselves
5388
- console.error(
5389
- 'Internal React Error: React expected bootstrap script with src "%s" to not have been preloaded already. please file an issue',
5390
- src,
5391
- );
5392
- }
5587
+ if (!enableFloat) {
5588
+ return;
5589
}
5394
- const props: PreloadProps = {
5395
- rel: 'preload',
5396
- href: src,
5397
- as: 'script',
5398
- fetchPriority: 'low',
5399
- nonce,
5400
- integrity,
5401
- crossOrigin,
5402
- };
5403
- const resource: PreloadResource = {
5404
- type: 'preload',
5405
- chunks: [],
5406
- state: NoState,
5407
- props,
5408
- };
5409
- resumableState.preloadsMap[key] = props;
5410
- renderState.preloadsMap.set(key, resource);
5411
- renderState.bootstrapScripts.add(resource);
5412
- pushLinkImpl(resource.chunks, props);
5413
-}
5590
+ const key = getResourceKey(href);
5591
5415
-// This function is only safe to call at Request start time since it assumes
5416
-// that each module has not already been preloaded. If we find a need to preload
5417
-// scripts at any other point in time we will need to check whether the preload
5418
-// already exists and not assume it
5419
-function preloadBootstrapModule(
5420
- resumableState: ResumableState,
5421
- renderState: RenderState,
5422
- src: string,
5423
- nonce: ?string,
5424
- integrity: ?string,
5425
- crossOrigin: ?string,
5426
-): void {
5427
- const key = getResourceKey('script', src);
5592
if (__DEV__) {
5429
- if (resumableState.preloadsMap.hasOwnProperty(key)) {
5593
+ if (
5594
+ resumableState.scriptResources.hasOwnProperty(key) ||
5595
+ resumableState.moduleScriptResources.hasOwnProperty(key)
5596
+ ) {
5597
// This is coded as a React error because it should be impossible for a userspace preload to preempt this call
5598
// If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
5599
// rather than instruct the user to not preload their bootstrap scripts themselves
5600
console.error(
5434
- 'Internal React Error: React expected bootstrap module with src "%s" to not have been preloaded already. please file an issue',
5435
- src,
5601
+ 'Internal React Error: React expected bootstrap script or module with src "%s" to not have been preloaded already. please file an issue',
5602
+ href,
5603
);
5604
}
5605
}
5439
- const props: PreloadModuleProps = {
5440
- rel: 'modulepreload',
5441
- href: src,
5442
- fetchPriority: 'low',
5443
- nonce,
5444
- integrity,
5445
- crossOrigin,
5446
- };
5447
- const resource: PreloadResource = {
5448
- type: 'preload',
5449
- chunks: [],
5450
- state: NoState,
5451
- props,
5452
- };
5453
- resumableState.preloadsMap[key] = props;
5454
- renderState.preloadsMap.set(key, resource);
5606
+
5607
+ // The href used for bootstrap scripts and bootstrap modules should never be
5608
+ // used to preinit the resource. If a script can be preinited then it shouldn't
5609
+ // be a bootstrap script/module and if it is a bootstrap script/module then it
5610
+ // must not be safe to emit early. To avoid possibly allowing for preinits of
5611
+ // bootstrap scripts/modules we occlude these keys.
5612
+ resumableState.scriptResources[key] = EXISTS;
5613
+ resumableState.moduleScriptResources[key] = EXISTS;
5614
+
5615
+ const resource: Resource = [];
5616
+ pushLinkImpl(resource, props);
5617
renderState.bootstrapScripts.add(resource);
5456
- pushLinkImpl(resource.chunks, props);
5457
- return;
5618
}
5619
5620
function internalPreinitScript(
5623
src: string,
5624
chunks: Array<Chunk | PrecomputedChunk>,
5625
): void {
5466
- const key = getResourceKey('script', src);
5467
- if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5468
- const resource: ScriptResource = {
5469
- type: 'script',
5470
- chunks,
5471
- state: NoState,
5472
- props: null,
5473
- };
5474
- resumableState.scriptsMap[key] = null;
5626
+ const key = getResourceKey(src);
5627
+ if (!resumableState.scriptResources.hasOwnProperty(key)) {
5628
+ const resource: Resource = chunks;
5629
+ resumableState.scriptResources[key] = EXISTS;
5630
renderState.scripts.add(resource);
5631
}
5632
return;
5654
};
5655
}
5656
5502
-function adoptPreloadPropsForStylesheetProps(
5503
- resourceProps: StylesheetProps,
5504
- preloadProps: PreloadProps,
5657
+function adoptPreloadCredentials(
5658
+ target: StylesheetProps | ScriptProps | ModuleScriptProps,
5659
+ preloadState: PreloadedWithCredentials,
5660
): void {
5506
- if (resourceProps.crossOrigin == null)
5507
- resourceProps.crossOrigin = preloadProps.crossOrigin;
5508
- if (resourceProps.integrity == null)
5509
- resourceProps.integrity = preloadProps.integrity;
5661
+ if (target.crossOrigin == null) target.crossOrigin = preloadState[0];
5662
+ if (target.integrity == null) target.integrity = preloadState[1];
5663
}
5664
5512
-function adoptPreloadPropsForScriptProps(
5513
- resourceProps: ScriptProps,
5514
- preloadProps: PreloadProps,
5515
-): void {
5516
- if (resourceProps.crossOrigin == null)
5517
- resourceProps.crossOrigin = preloadProps.crossOrigin;
5518
- if (resourceProps.integrity == null)
5519
- resourceProps.integrity = preloadProps.integrity;
5665
+function hoistStyleQueueDependency(
5666
+ this: BoundaryResources,
5667
+ styleQueue: StyleQueue,
5668
+) {
5669
+ this.styles.add(styleQueue);
5670
}
5671
5522
-function hoistStyleResource(this: BoundaryResources, resource: StyleResource) {
5523
- this.add(resource);
5672
+function hoistStylesheetDependency(
5673
+ this: BoundaryResources,
5674
+ stylesheet: StylesheetResource,
5675
+) {
5676
+ this.stylesheets.add(stylesheet);
5677
}
5678
5679
export function hoistResources(
5682
): void {
5683
const currentBoundaryResources = renderState.boundaryResources;
5684
if (currentBoundaryResources) {
5532
- source.forEach(hoistStyleResource, currentBoundaryResources);
5685
+ source.styles.forEach(hoistStyleQueueDependency, currentBoundaryResources);
5686
+ source.stylesheets.forEach(
5687
+ hoistStylesheetDependency,
5688
+ currentBoundaryResources,
5689
+ );
5690
}
5691
}
5692