22
checkHtmlStringCoercion,
23
checkCSSPropertyStringCoercion,
24
checkAttributeStringCoercion,
25
+ checkOptionStringCoercion,
26
} from 'shared/CheckStringCoercion';
27
28
import {Children} from 'react';
66
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
67
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
68
import warnValidStyle from '../shared/warnValidStyle';
69
+import {getCrossOriginString} from '../shared/crossOriginStrings';
70
71
import escapeTextForBrowser from './escapeTextForBrowser';
72
import hyphenateStyleName from '../shared/hyphenateStyleName';
103
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
104
}
105
106
+// We make every property of the descriptor optional because it is not a contract that
107
+// the headers provided by onHeaders has any particular header types.
108
+export type HeadersDescriptor = {
109
+ Link?: string,
110
+};
111
+
112
// Used to distinguish these contexts from ones used in other renderers.
113
// E.g. this can be used to distinguish legacy renderers from this modern one.
114
export const isPrimaryRenderer = true;
155
preloadChunks: Array<Chunk | PrecomputedChunk>,
156
hoistableChunks: Array<Chunk | PrecomputedChunk>,
157
158
+ // Headers queues for Resources that can flush early
159
+ onHeaders: void | ((headers: HeadersDescriptor) => void),
160
+ headers: null | {
161
+ preconnects: string,
162
+ fontPreloads: string,
163
+ highImagePreloads: string,
164
+ remainingCapacity: number,
165
+ },
166
+ resets: {
167
+ // corresponds to ResumableState.unknownResources["font"]
168
+ font: {
169
+ [href: string]: Preloaded,
170
+ },
171
+ // the rest correspond to ResumableState[<...>Resources]
172
+ dns: {[key: string]: Exists},
173
+ connect: {
174
+ default: {[key: string]: Exists},
175
+ anonymous: {[key: string]: Exists},
176
+ credentials: {[key: string]: Exists},
177
+ },
178
+ image: {
179
+ [key: string]: Preloaded,
180
+ },
181
+ style: {
182
+ [key: string]: Exists | Preloaded | PreloadedWithCredentials,
183
+ },
184
+ },
185
+
186
// Flushing queues for Resource dependencies
187
preconnects: Set<Resource>,
188
fontPreloads: Set<Resource>,
224
// it seems that browsers do not treat this as part of the http cache key and does not affect
225
// which connection is used.
226
type PreloadedWithCredentials = [
191
- /* crossOrigin */ ?string,
227
+ /* crossOrigin */ ?CrossOriginEnum,
228
/* integrity */ ?string,
229
];
230
334
);
335
const importMapScriptEnd = stringToPrecomputedChunk('</script>');
336
337
+// Since we store headers as strings we deal with their length in utf16 code units
338
+// rather than visual characters or the utf8 encoding that is used for most binary
339
+// serialization. Some common HTTP servers only allow for headers to be 4kB in length.
340
+// We choose a default length that is likely to be well under this already limited length however
341
+// pathological cases may still cause the utf-8 encoding of the headers to approach this limit.
342
+// It should also be noted that this maximum is a soft maximum. we have not reached the limit we will
343
+// allow one more header to be captured which means in practice if the limit is approached it will be exceeded
344
+const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000;
345
+
346
// Allows us to keep track of what we've already written so we can refer back to it.
347
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
348
// is set, the server will send instructions via data attributes (instead of inline scripts)
354
bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
355
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
356
importMap: ImportMap | void,
357
+ onHeaders: void | ((headers: HeadersDescriptor) => void),
358
+ maxHeadersLength: void | number,
359
): RenderState {
360
const inlineScriptWithNonce =
361
nonce === undefined
420
);
421
importMapChunks.push(importMapScriptEnd);
422
}
423
+ if (__DEV__) {
424
+ if (onHeaders && typeof maxHeadersLength === 'number') {
425
+ if (maxHeadersLength <= 0) {
426
+ console.error(
427
+ 'React expected a positive non-zero `maxHeadersLength` option but found %s instead. When using the `onHeaders` option you may supply an optional `maxHeadersLength` option as well however, when setting this value to zero or less no headers will be captured.',
428
+ maxHeadersLength === 0 ? 'zero' : maxHeadersLength,
429
+ );
430
+ }
431
+ }
432
+ }
433
+ const headers = onHeaders
434
+ ? {
435
+ preconnects: '',
436
+ fontPreloads: '',
437
+ highImagePreloads: '',
438
+ remainingCapacity:
439
+ typeof maxHeadersLength === 'number'
440
+ ? maxHeadersLength
441
+ : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS,
442
+ }
443
+ : null;
444
const renderState: RenderState = {
445
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
446
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
452
externalRuntimeScript: externalRuntimeScript,
453
bootstrapChunks: bootstrapChunks,
454
455
+ onHeaders,
456
+ headers,
457
+ resets: {
458
+ font: {},
459
+ dns: {},
460
+ connect: {
461
+ default: {},
462
+ anonymous: {},
463
+ credentials: {},
464
+ },
465
+ image: {},
466
+ style: {},
467
+ },
468
+
469
charsetChunks: [],
470
preconnectChunks: [],
471
importMapChunks,
472
preloadChunks: [],
473
hoistableChunks: [],
474
+
475
// cleared on flush
476
preconnects: new Set(),
477
fontPreloads: new Set(),
618
undefined,
619
undefined,
620
undefined,
621
+ undefined,
622
);
623
}
624
669
resumableState.nextFormID = 0;
670
resumableState.hasBody = false;
671
resumableState.hasHtml = false;
588
- resumableState.unknownResources = {};
589
- resumableState.dnsResources = {};
590
- resumableState.connectResources = {
591
- default: {},
592
- anonymous: {},
593
- credentials: {},
672
+ resumableState.unknownResources = {
673
+ font: renderState.resets.font,
674
};
595
- resumableState.imageResources = {};
596
- resumableState.styleResources = {};
675
+ resumableState.dnsResources = renderState.resets.dns;
676
+ resumableState.connectResources = renderState.resets.connect;
677
+ resumableState.imageResources = renderState.resets.image;
678
+ resumableState.styleResources = renderState.resets.style;
679
resumableState.scriptResources = {};
680
resumableState.moduleUnknownResources = {};
681
resumableState.moduleScriptResources = {};
2718
} else if (!resumableState.imageResources.hasOwnProperty(key)) {
2719
// We must construct a new preload resource
2720
resumableState.imageResources[key] = PRELOAD_NO_CREDS;
2639
- resource = [];
2640
- pushLinkImpl(
2641
- resource,
2642
- ({
2643
- rel: 'preload',
2644
- as: 'image',
2645
- // There is a bug in Safari where imageSrcSet is not respected on preload links
2646
- // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2647
- // This harms older browers that do not support imageSrcSet by making their preloads not work
2648
- // but this population is shrinking fast and is already small so we accept this tradeoff.
2649
- href: srcSet ? undefined : src,
2650
- imageSrcSet: srcSet,
2651
- imageSizes: sizes,
2652
- crossOrigin: props.crossOrigin,
2721
+ const crossOrigin = getCrossOriginString(props.crossOrigin);
2722
+
2723
+ const headers = renderState.headers;
2724
+ let header;
2725
+ if (
2726
+ headers &&
2727
+ headers.remainingCapacity > 0 &&
2728
+ // this is a hueristic similar to capping element preloads to 10 unless explicitly
2729
+ // fetchPriority="high". We use length here which means it will fit fewer images when
2730
+ // the urls are long and more when short. arguably byte size is a better hueristic because
2731
+ // it directly translates to how much we send down before content is actually seen.
2732
+ // We could unify the counts and also make it so the total is tracked regardless of
2733
+ // flushing output but since the headers are likely to be go earlier than content
2734
+ // they don't really conflict so for now I've kept them separate
2735
+ (props.fetchPriority === 'high' ||
2736
+ headers.highImagePreloads.length < 500) &&
2737
+ // We manually construct the options for the preload only from strings. We don't want to pollute
2738
+ // the params list with arbitrary props and if we copied everything over as it we might get
2739
+ // coercion errors. We have checks for this in Dev but it seems safer to just only accept values
2740
+ // that are strings
2741
+ ((header = getPreloadAsHeader(src, 'image', {
2742
+ imageSrcSet: props.srcSet,
2743
+ imageSizes: props.sizes,
2744
+ crossOrigin,
2745
integrity: props.integrity,
2746
+ nonce: props.nonce,
2747
type: props.type,
2748
fetchPriority: props.fetchPriority,
2656
- referrerPolicy: props.referrerPolicy,
2657
- }: PreloadProps),
2658
- );
2659
- if (
2660
- props.fetchPriority === 'high' ||
2661
- renderState.highImagePreloads.size < 10
2749
+ referrerPolicy: props.refererPolicy,
2750
+ })),
2751
+ // We always consume the header length since once we find one header that doesn't fit
2752
+ // we assume all the rest won't as well. This is to avoid getting into a situation
2753
+ // where we have a very small remaining capacity but no headers will ever fit and we end
2754
+ // up constantly trying to see if the next resource might make it. In the future we can
2755
+ // make this behavior different between render and prerender since in the latter case
2756
+ // we are less sensitive to the current requests runtime per and more sensitive to maximizing
2757
+ // headers.
2758
+ (headers.remainingCapacity -= header.length) >= 2)
2759
) {
2663
- renderState.highImagePreloads.add(resource);
2760
+ // If we postpone in the shell we will still emit this preload so we track
2761
+ // it to make sure we don't reset it.
2762
+ renderState.resets.image[key] = PRELOAD_NO_CREDS;
2763
+ if (headers.highImagePreloads) {
2764
+ headers.highImagePreloads += ', ';
2765
+ }
2766
+ // $FlowFixMe[unsafe-addition]: we assign header during the if condition
2767
+ headers.highImagePreloads += header;
2768
} else {
2665
- renderState.bulkPreloads.add(resource);
2666
- // We can bump the priority up if the same img is rendered later
2667
- // with fetchPriority="high"
2668
- promotablePreloads.set(key, resource);
2769
+ resource = [];
2770
+ pushLinkImpl(
2771
+ resource,
2772
+ ({
2773
+ rel: 'preload',
2774
+ as: 'image',
2775
+ // There is a bug in Safari where imageSrcSet is not respected on preload links
2776
+ // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2777
+ // This harms older browers that do not support imageSrcSet by making their preloads not work
2778
+ // but this population is shrinking fast and is already small so we accept this tradeoff.
2779
+ href: srcSet ? undefined : src,
2780
+ imageSrcSet: srcSet,
2781
+ imageSizes: sizes,
2782
+ crossOrigin: crossOrigin,
2783
+ integrity: props.integrity,
2784
+ type: props.type,
2785
+ fetchPriority: props.fetchPriority,
2786
+ referrerPolicy: props.referrerPolicy,
2787
+ }: PreloadProps),
2788
+ );
2789
+ if (
2790
+ props.fetchPriority === 'high' ||
2791
+ renderState.highImagePreloads.size < 10
2792
+ ) {
2793
+ renderState.highImagePreloads.add(resource);
2794
+ } else {
2795
+ renderState.bulkPreloads.add(resource);
2796
+ // We can bump the priority up if the same img is rendered later
2797
+ // with fetchPriority="high"
2798
+ promotablePreloads.set(key, resource);
2799
+ }
2800
}
2801
}
2802
}
5167
type ScriptProps = {
5168
async: true,
5169
src: string,
5170
+ crossOrigin?: ?CrossOriginEnum,
5171
[string]: mixed,
5172
};
5173
type ModuleScriptProps = {
5174
async: true,
5175
src: string,
5176
type: 'module',
5177
+ crossOrigin?: ?CrossOriginEnum,
5178
[string]: mixed,
5179
};
5180
5184
rel: 'stylesheet',
5185
href: string,
5186
'data-precedence': string,
5187
+ crossOrigin?: ?CrossOriginEnum,
5188
+ integrity?: ?string,
5189
+ nonce?: ?string,
5190
+ type?: ?string,
5191
+ fetchPriority?: ?string,
5192
+ referrerPolicy?: ?string,
5193
+ media?: ?string,
5194
[string]: mixed,
5195
};
5196
type StylesheetResource = {
5258
if (typeof href === 'string' && href) {
5259
const key = getResourceKey(href);
5260
if (!resumableState.dnsResources.hasOwnProperty(key)) {
5121
- const resource: Resource = [];
5261
resumableState.dnsResources[key] = EXISTS;
5123
- pushLinkImpl(resource, ({href, rel: 'dns-prefetch'}: PreconnectProps));
5124
- renderState.preconnects.add(resource);
5262
+
5263
+ const headers = renderState.headers;
5264
+ let header;
5265
+ if (
5266
+ headers &&
5267
+ headers.remainingCapacity > 0 &&
5268
+ // Compute the header since we might be able to fit it in the max length
5269
+ ((header = getPrefetchDNSAsHeader(href)),
5270
+ // We always consume the header length since once we find one header that doesn't fit
5271
+ // we assume all the rest won't as well. This is to avoid getting into a situation
5272
+ // where we have a very small remaining capacity but no headers will ever fit and we end
5273
+ // up constantly trying to see if the next resource might make it. In the future we can
5274
+ // make this behavior different between render and prerender since in the latter case
5275
+ // we are less sensitive to the current requests runtime per and more sensitive to maximizing
5276
+ // headers.
5277
+ (headers.remainingCapacity -= header.length) >= 2)
5278
+ ) {
5279
+ // Store this as resettable in case we are prerendering and postpone in the Shell
5280
+ renderState.resets.dns[key] = EXISTS;
5281
+ if (headers.preconnects) {
5282
+ headers.preconnects += ', ';
5283
+ }
5284
+ // $FlowFixMe[unsafe-addition]: we assign header during the if condition
5285
+ headers.preconnects += header;
5286
+ } else {
5287
+ // Encode as element
5288
+ const resource: Resource = [];
5289
+ pushLinkImpl(resource, ({href, rel: 'dns-prefetch'}: PreconnectProps));
5290
+ renderState.preconnects.add(resource);
5291
+ }
5292
}
5293
flushResources(request);
5294
}
5311
const renderState = getRenderState(request);
5312
5313
if (typeof href === 'string' && href) {
5147
- const resources =
5314
+ const bucket =
5315
crossOrigin === 'use-credentials'
5149
- ? resumableState.connectResources.credentials
5316
+ ? 'credentials'
5317
: typeof crossOrigin === 'string'
5151
- ? resumableState.connectResources.anonymous
5152
- : resumableState.connectResources.default;
5318
+ ? 'anonymous'
5319
+ : 'default';
5320
const key = getResourceKey(href);
5154
- if (!resources.hasOwnProperty(key)) {
5155
- const resource: Resource = [];
5156
- resources[key] = EXISTS;
5157
- pushLinkImpl(
5158
- resource,
5159
- ({rel: 'preconnect', href, crossOrigin}: PreconnectProps),
5160
- );
5161
- renderState.preconnects.add(resource);
5321
+ if (!resumableState.connectResources[bucket].hasOwnProperty(key)) {
5322
+ resumableState.connectResources[bucket][key] = EXISTS;
5323
+
5324
+ const headers = renderState.headers;
5325
+ let header;
5326
+ if (
5327
+ headers &&
5328
+ headers.remainingCapacity > 0 &&
5329
+ // Compute the header since we might be able to fit it in the max length
5330
+ ((header = getPreconnectAsHeader(href, crossOrigin)),
5331
+ // We always consume the header length since once we find one header that doesn't fit
5332
+ // we assume all the rest won't as well. This is to avoid getting into a situation
5333
+ // where we have a very small remaining capacity but no headers will ever fit and we end
5334
+ // up constantly trying to see if the next resource might make it. In the future we can
5335
+ // make this behavior different between render and prerender since in the latter case
5336
+ // we are less sensitive to the current requests runtime per and more sensitive to maximizing
5337
+ // headers.
5338
+ (headers.remainingCapacity -= header.length) >= 2)
5339
+ ) {
5340
+ // Store this in resettableState in case we are prerending and postpone in the Shell
5341
+ renderState.resets.connect[bucket][key] = EXISTS;
5342
+ if (headers.preconnects) {
5343
+ headers.preconnects += ', ';
5344
+ }
5345
+ // $FlowFixMe[unsafe-addition]: we assign header during the if condition
5346
+ headers.preconnects += header;
5347
+ } else {
5348
+ const resource: Resource = [];
5349
+ pushLinkImpl(
5350
+ resource,
5351
+ ({rel: 'preconnect', href, crossOrigin}: PreconnectProps),
5352
+ );
5353
+ renderState.preconnects.add(resource);
5354
+ }
5355
}
5356
flushResources(request);
5357
}
5387
return;
5388
}
5389
resumableState.imageResources[key] = PRELOAD_NO_CREDS;
5197
- const resource = ([]: Resource);
5198
- pushLinkImpl(
5199
- resource,
5200
- Object.assign(
5201
- ({
5202
- rel: 'preload',
5203
- // There is a bug in Safari where imageSrcSet is not respected on preload links
5204
- // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
5205
- // This harms older browers that do not support imageSrcSet by making their preloads not work
5206
- // but this population is shrinking fast and is already small so we accept this tradeoff.
5207
- href: imageSrcSet ? undefined : href,
5208
- as,
5209
- }: PreloadAsProps),
5210
- options,
5211
- ),
5212
- );
5213
- if (fetchPriority === 'high') {
5214
- renderState.highImagePreloads.add(resource);
5390
+
5391
+ const headers = renderState.headers;
5392
+ let header: string;
5393
+ if (
5394
+ headers &&
5395
+ headers.remainingCapacity > 0 &&
5396
+ fetchPriority === 'high' &&
5397
+ // Compute the header since we might be able to fit it in the max length
5398
+ ((header = getPreloadAsHeader(href, as, options)),
5399
+ // We always consume the header length since once we find one header that doesn't fit
5400
+ // we assume all the rest won't as well. This is to avoid getting into a situation
5401
+ // where we have a very small remaining capacity but no headers will ever fit and we end
5402
+ // up constantly trying to see if the next resource might make it. In the future we can
5403
+ // make this behavior different between render and prerender since in the latter case
5404
+ // we are less sensitive to the current requests runtime per and more sensitive to maximizing
5405
+ // headers.
5406
+ (headers.remainingCapacity -= header.length) >= 2)
5407
+ ) {
5408
+ // If we postpone in the shell we will still emit a preload as a header so we
5409
+ // track this to make sure we don't reset it.
5410
+ renderState.resets.image[key] = PRELOAD_NO_CREDS;
5411
+ if (headers.highImagePreloads) {
5412
+ headers.highImagePreloads += ', ';
5413
+ }
5414
+ // $FlowFixMe[unsafe-addition]: we assign header during the if condition
5415
+ headers.highImagePreloads += header;
5416
} else {
5216
- renderState.bulkPreloads.add(resource);
5217
- // Stash the resource in case we need to promote it to higher priority
5218
- // when an img tag is rendered
5219
- renderState.preloads.images.set(key, resource);
5417
+ // If we don't have headers to write to we have to encode as elements to flush in the head
5418
+ // When we have imageSrcSet the browser probably cannot load the right version from headers
5419
+ // (this should be verified by testing). For now we assume these need to go in the head
5420
+ // as elements even if headers are available.
5421
+ const resource = ([]: Resource);
5422
+ pushLinkImpl(
5423
+ resource,
5424
+ Object.assign(
5425
+ ({
5426
+ rel: 'preload',
5427
+ // There is a bug in Safari where imageSrcSet is not respected on preload links
5428
+ // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
5429
+ // This harms older browers that do not support imageSrcSet by making their preloads not work
5430
+ // but this population is shrinking fast and is already small so we accept this tradeoff.
5431
+ href: imageSrcSet ? undefined : href,
5432
+ as,
5433
+ }: PreloadAsProps),
5434
+ options,
5435
+ ),
5436
+ );
5437
+ if (fetchPriority === 'high') {
5438
+ renderState.highImagePreloads.add(resource);
5439
+ } else {
5440
+ renderState.bulkPreloads.add(resource);
5441
+ // Stash the resource in case we need to promote it to higher priority
5442
+ // when an img tag is rendered
5443
+ renderState.preloads.images.set(key, resource);
5444
+ }
5445
}
5446
break;
5447
}
5501
resources = ({}: ResumableState['unknownResources']['asType']);
5502
resumableState.unknownResources[as] = resources;
5503
}
5279
- const resource = ([]: Resource);
5280
- const props = Object.assign(
5281
- ({
5282
- rel: 'preload',
5283
- href,
5284
- as,
5285
- }: PreloadAsProps),
5286
- options,
5287
- );
5288
- switch (as) {
5289
- case 'font':
5290
- renderState.fontPreloads.add(resource);
5291
- break;
5292
- // intentional fall through
5293
- default:
5294
- renderState.bulkPreloads.add(resource);
5295
- }
5296
- pushLinkImpl(resource, props);
5504
resources[key] = PRELOAD_NO_CREDS;
5505
+
5506
+ const headers = renderState.headers;
5507
+ let header;
5508
+ if (
5509
+ headers &&
5510
+ headers.remainingCapacity > 0 &&
5511
+ as === 'font' &&
5512
+ // We compute the header here because we might be able to fit it in the max length
5513
+ ((header = getPreloadAsHeader(href, as, options)),
5514
+ // We always consume the header length since once we find one header that doesn't fit
5515
+ // we assume all the rest won't as well. This is to avoid getting into a situation
5516
+ // where we have a very small remaining capacity but no headers will ever fit and we end
5517
+ // up constantly trying to see if the next resource might make it. In the future we can
5518
+ // make this behavior different between render and prerender since in the latter case
5519
+ // we are less sensitive to the current requests runtime per and more sensitive to maximizing
5520
+ // headers.
5521
+ (headers.remainingCapacity -= header.length) >= 2)
5522
+ ) {
5523
+ // If we postpone in the shell we will still emit this preload so we
5524
+ // track it here to prevent it from being reset.
5525
+ renderState.resets.font[key] = PRELOAD_NO_CREDS;
5526
+ if (headers.fontPreloads) {
5527
+ headers.fontPreloads += ', ';
5528
+ }
5529
+ // $FlowFixMe[unsafe-addition]: we assign header during the if condition
5530
+ headers.fontPreloads += header;
5531
+ } else {
5532
+ // We either don't have headers or we are preloading something that does
5533
+ // not warrant elevated priority so we encode as an element.
5534
+ const resource = ([]: Resource);
5535
+ const props = Object.assign(
5536
+ ({
5537
+ rel: 'preload',
5538
+ href,
5539
+ as,
5540
+ }: PreloadAsProps),
5541
+ options,
5542
+ );
5543
+ pushLinkImpl(resource, props);
5544
+ switch (as) {
5545
+ case 'font':
5546
+ renderState.fontPreloads.add(resource);
5547
+ break;
5548
+ // intentional fall through
5549
+ default:
5550
+ renderState.bulkPreloads.add(resource);
5551
+ }
5552
+ }
5553
}
5554
}
5555
// If we got this far we created a new resource
5938
if (target.integrity == null) target.integrity = preloadState[1];
5939
}
5940
5941
+function getPrefetchDNSAsHeader(href: string): string {
5942
+ const escapedHref = escapeHrefForLinkHeaderURLContext(href);
5943
+ return `<${escapedHref}>; rel=dns-prefetch`;
5944
+}
5945
+
5946
+function getPreconnectAsHeader(
5947
+ href: string,
5948
+ crossOrigin?: ?CrossOriginEnum,
5949
+): string {
5950
+ const escapedHref = escapeHrefForLinkHeaderURLContext(href);
5951
+ let value = `<${escapedHref}>; rel=preconnect`;
5952
+ if (typeof crossOrigin === 'string') {
5953
+ const escapedCrossOrigin = escapeStringForLinkHeaderQuotedParamValueContext(
5954
+ crossOrigin,
5955
+ 'crossOrigin',
5956
+ );
5957
+ value += `; crossorigin="${escapedCrossOrigin}"`;
5958
+ }
5959
+ return value;
5960
+}
5961
+
5962
+function getPreloadAsHeader(
5963
+ href: string,
5964
+ as: string,
5965
+ params: ?PreloadImplOptions,
5966
+): string {
5967
+ const escapedHref = escapeHrefForLinkHeaderURLContext(href);
5968
+ const escapedAs = escapeStringForLinkHeaderQuotedParamValueContext(as, 'as');
5969
+ let value = `<${escapedHref}>; rel=preload; as="${escapedAs}"`;
5970
+ for (const paramName in params) {
5971
+ if (hasOwnProperty.call(params, paramName)) {
5972
+ const paramValue = params[paramName];
5973
+ if (typeof paramValue === 'string') {
5974
+ value += `; ${paramName.toLowerCase()}="${escapeStringForLinkHeaderQuotedParamValueContext(
5975
+ paramValue,
5976
+ paramName,
5977
+ )}"`;
5978
+ }
5979
+ }
5980
+ }
5981
+ return value;
5982
+}
5983
+
5984
+function getStylesheetPreloadAsHeader(stylesheet: StylesheetResource): string {
5985
+ const props = stylesheet.props;
5986
+ const preloadOptions: PreloadImplOptions = {
5987
+ crossOrigin: props.crossOrigin,
5988
+ integrity: props.integrity,
5989
+ nonce: props.nonce,
5990
+ type: props.type,
5991
+ fetchPriority: props.fetchPriority,
5992
+ referrerPolicy: props.referrerPolicy,
5993
+ media: props.media,
5994
+ };
5995
+ return getPreloadAsHeader(props.href, 'style', preloadOptions);
5996
+}
5997
+
5998
+// This escaping function is only safe to use for href values being written into
5999
+// a "Link" header in between `<` and `>` characters. The primary concern with the href is
6000
+// to escape the bounding characters as well as new lines. This is unsafe to use in any other
6001
+// context
6002
+const regexForHrefInLinkHeaderURLContext = /[<>\r\n]/g;
6003
+function escapeHrefForLinkHeaderURLContext(hrefInput: string): string {
6004
+ if (__DEV__) {
6005
+ checkAttributeStringCoercion(hrefInput, 'href');
6006
+ }
6007
+ const coercedHref = '' + hrefInput;
6008
+ return coercedHref.replace(
6009
+ regexForHrefInLinkHeaderURLContext,
6010
+ escapeHrefForLinkHeaderURLContextReplacer,
6011
+ );
6012
+}
6013
+function escapeHrefForLinkHeaderURLContextReplacer(match: string): string {
6014
+ switch (match) {
6015
+ case '<':
6016
+ return '%3C';
6017
+ case '>':
6018
+ return '%3E';
6019
+ case '\n':
6020
+ return '%0A';
6021
+ case '\r':
6022
+ return '%0D';
6023
+ default: {
6024
+ // eslint-disable-next-line react-internal/prod-error-codes
6025
+ throw new Error(
6026
+ 'escapeLinkHrefForHeaderContextReplacer encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
6027
+ );
6028
+ }
6029
+ }
6030
+}
6031
+
6032
+// This escaping function is only safe to use for quoted param values in an HTTP header.
6033
+// It is unsafe to use for any value not inside quote marks in parater value position.
6034
+const regexForLinkHeaderQuotedParamValueContext = /["';,\r\n]/g;
6035
+function escapeStringForLinkHeaderQuotedParamValueContext(
6036
+ value: string,
6037
+ name: string,
6038
+): string {
6039
+ if (__DEV__) {
6040
+ checkOptionStringCoercion(value, name);
6041
+ }
6042
+ const coerced = '' + value;
6043
+ return coerced.replace(
6044
+ regexForLinkHeaderQuotedParamValueContext,
6045
+ escapeStringForLinkHeaderQuotedParamValueContextReplacer,
6046
+ );
6047
+}
6048
+function escapeStringForLinkHeaderQuotedParamValueContextReplacer(
6049
+ match: string,
6050
+): string {
6051
+ switch (match) {
6052
+ case '"':
6053
+ return '%22';
6054
+ case "'":
6055
+ return '%27';
6056
+ case ';':
6057
+ return '%3B';
6058
+ case ',':
6059
+ return '%2C';
6060
+ case '\n':
6061
+ return '%0A';
6062
+ case '\r':
6063
+ return '%0D';
6064
+ default: {
6065
+ // eslint-disable-next-line react-internal/prod-error-codes
6066
+ throw new Error(
6067
+ 'escapeStringForLinkHeaderQuotedParamValueContextReplacer encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
6068
+ );
6069
+ }
6070
+ }
6071
+}
6072
+
6073
function hoistStyleQueueDependency(
6074
this: BoundaryResources,
6075
styleQueue: StyleQueue,
6098
}
6099
}
6100
6101
+// This function is called at various times depending on whether we are rendering
6102
+// or prerendering. In this implementation we only actually emit headers once and
6103
+// subsequent calls are ignored. We track whether the request has a completed shell
6104
+// to determine whether we will follow headers with a flush including stylesheets.
6105
+// In the context of prerrender we don't have a completed shell when the request finishes
6106
+// with a postpone in the shell. In the context of a render we don't have a completed shell
6107
+// if this is called before the shell finishes rendering which usually will happen anytime
6108
+// anything suspends in the shell.
6109
+export function emitEarlyPreloads(
6110
+ renderState: RenderState,
6111
+ resumableState: ResumableState,
6112
+ shellComplete: boolean,
6113
+): void {
6114
+ const onHeaders = renderState.onHeaders;
6115
+ if (onHeaders) {
6116
+ const headers = renderState.headers;
6117
+ if (headers) {
6118
+ let linkHeader = headers.preconnects;
6119
+ if (headers.fontPreloads) {
6120
+ if (linkHeader) {
6121
+ linkHeader += ', ';
6122
+ }
6123
+ linkHeader += headers.fontPreloads;
6124
+ }
6125
+ if (headers.highImagePreloads) {
6126
+ if (linkHeader) {
6127
+ linkHeader += ', ';
6128
+ }
6129
+ linkHeader += headers.highImagePreloads;
6130
+ }
6131
+
6132
+ if (!shellComplete) {
6133
+ // We use raw iterators because we want to be able to halt iteration
6134
+ // We could refactor renderState to store these dually in arrays to
6135
+ // make this more efficient at the cost of additional memory and
6136
+ // write overhead. However this code only runs once per request so
6137
+ // for now I consider this sufficient.
6138
+ const queueIter = renderState.styles.values();
6139
+ outer: for (
6140
+ let queueStep = queueIter.next();
6141
+ headers.remainingCapacity > 0 && !queueStep.done;
6142
+ queueStep = queueIter.next()
6143
+ ) {
6144
+ const sheets = queueStep.value.sheets;
6145
+ const sheetIter = sheets.values();
6146
+ for (
6147
+ let sheetStep = sheetIter.next();
6148
+ headers.remainingCapacity > 0 && !sheetStep.done;
6149
+ sheetStep = sheetIter.next()
6150
+ ) {
6151
+ const sheet = sheetStep.value;
6152
+ const props = sheet.props;
6153
+ const key = getResourceKey(props.href);
6154
+
6155
+ const header = getStylesheetPreloadAsHeader(sheet);
6156
+ // We mutate the capacity b/c we don't want to keep checking if later headers will fit.
6157
+ // This means that a particularly long header might close out the header queue where later
6158
+ // headers could still fit. We could in the future alter the behavior here based on prerender vs render
6159
+ // since during prerender we aren't as concerned with pure runtime performance.
6160
+ if ((headers.remainingCapacity -= header.length) >= 2) {
6161
+ renderState.resets.style[key] = PRELOAD_NO_CREDS;
6162
+ if (linkHeader) {
6163
+ linkHeader += ', ';
6164
+ }
6165
+ linkHeader += header;
6166
+
6167
+ // We already track that the resource exists in resumableState however
6168
+ // if the resumableState resets because we postponed in the shell
6169
+ // which is what is happening in this branch if we are prerendering
6170
+ // then we will end up resetting the resumableState. When it resets we
6171
+ // want to record the fact that this stylesheet was already preloaded
6172
+ renderState.resets.style[key] =
6173
+ typeof props.crossOrigin === 'string' ||
6174
+ typeof props.integrity === 'string'
6175
+ ? [props.crossOrigin, props.integrity]
6176
+ : PRELOAD_NO_CREDS;
6177
+ } else {
6178
+ break outer;
6179
+ }
6180
+ }
6181
+ }
6182
+ }
6183
+ if (linkHeader) {
6184
+ onHeaders({
6185
+ Link: linkHeader,
6186
+ });
6187
+ } else {
6188
+ // We still call this with no headers because a user may be using it as a signal that
6189
+ // it React will not provide any headers
6190
+ onHeaders({});
6191
+ }
6192
+ renderState.headers = null;
6193
+ return;
6194
+ }
6195
+ }
6196
+}
6197
+
6198
export type TransitionStatus = FormStatus;
6199
export const NotPendingTransition: TransitionStatus = NotPending;