main
js 205 lines 5.92 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 CrossOriginEnum,
12 PreloadImplOptions,
13 PreloadModuleImplOptions,
14 PreinitStyleOptions,
15 PreinitScriptOptions,
16 PreinitModuleScriptOptions,
17 } from 'react-dom/src/shared/ReactDOMTypes';
18
19 // This module registers the host dispatcher so it needs to be imported
20 // even if no exports are used.
21 import {preload, preloadModule} from './ReactDOMFlightServerHostDispatcher';
22
23 import {getCrossOriginString} from '../shared/crossOriginStrings';
24
25 // We use zero to represent the absence of an explicit precedence because it is
26 // small, smaller than how we encode undefined, and is unambiguous. We could use
27 // a different tuple structure to encode this instead but this makes the runtime
28 // cost cheaper by eliminating a type checks in more positions.
29 type UnspecifiedPrecedence = 0;
30
31 // prettier-ignore
32 type TypeMap = {
33 // prefetchDNS(href)
34 'D': /* href */ string,
35 // preconnect(href, options)
36 'C':
37 | /* href */ string
38 | [/* href */ string, CrossOriginEnum],
39 // preconnect(href, options)
40 'L':
41 | [/* href */ string, /* as */ string]
42 | [/* href */ string, /* as */ string, PreloadImplOptions],
43 'm':
44 | /* href */ string
45 | [/* href */ string, PreloadModuleImplOptions],
46 'S':
47 | /* href */ string
48 | [/* href */ string, /* precedence */ string]
49 | [/* href */ string, /* precedence */ string | UnspecifiedPrecedence, PreinitStyleOptions],
50 'X':
51 | /* href */ string
52 | [/* href */ string, PreinitScriptOptions],
53 'M':
54 | /* href */ string
55 | [/* href */ string, PreinitModuleScriptOptions],
56 }
57
58 export type HintCode = $Keys<TypeMap>;
59 export type HintModel<T: HintCode> = TypeMap[T];
60
61 export type Hints = Set<string>;
62
63 export function createHints(): Hints {
64 return new Set();
65 }
66
67 const NO_SCOPE = /* */ 0b000000;
68 const NOSCRIPT_SCOPE = /* */ 0b000001;
69 const PICTURE_SCOPE = /* */ 0b000010;
70
71 export opaque type FormatContext = number;
72
73 export function createRootFormatContext(): FormatContext {
74 return NO_SCOPE;
75 }
76
77 function processImg(props: Object, formatContext: FormatContext): void {
78 // This should mirror the logic of pushImg in ReactFizzConfigDOM.
79 const pictureOrNoScriptTagInScope =
80 formatContext & (PICTURE_SCOPE | NOSCRIPT_SCOPE);
81 const {src, srcSet} = props;
82 if (
83 props.loading !== 'lazy' &&
84 (src || srcSet) &&
85 (typeof src === 'string' || src == null) &&
86 (typeof srcSet === 'string' || srcSet == null) &&
87 props.fetchPriority !== 'low' &&
88 !pictureOrNoScriptTagInScope &&
89 // We exclude data URIs in src and srcSet since these should not be preloaded
90 !(
91 typeof src === 'string' &&
92 src[4] === ':' &&
93 (src[0] === 'd' || src[0] === 'D') &&
94 (src[1] === 'a' || src[1] === 'A') &&
95 (src[2] === 't' || src[2] === 'T') &&
96 (src[3] === 'a' || src[3] === 'A')
97 ) &&
98 !(
99 typeof srcSet === 'string' &&
100 srcSet[4] === ':' &&
101 (srcSet[0] === 'd' || srcSet[0] === 'D') &&
102 (srcSet[1] === 'a' || srcSet[1] === 'A') &&
103 (srcSet[2] === 't' || srcSet[2] === 'T') &&
104 (srcSet[3] === 'a' || srcSet[3] === 'A')
105 )
106 ) {
107 // We have a suspensey image and ought to preload it to optimize the loading of display blocking
108 // resumableState.
109 const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
110
111 const crossOrigin = getCrossOriginString(props.crossOrigin);
112
113 preload(
114 // The preload() API requires a href but if we have an imageSrcSet then that will take precedence.
115 // We already remove the href anyway in both Fizz and Fiber due to a Safari bug so the empty string
116 // will never actually appear in the DOM.
117 src || '',
118 'image',
119 {
120 imageSrcSet: srcSet,
121 imageSizes: sizes,
122 crossOrigin: crossOrigin,
123 integrity: props.integrity,
124 type: props.type,
125 fetchPriority: props.fetchPriority,
126 referrerPolicy: props.referrerPolicy,
127 },
128 );
129 }
130 }
131
132 function processLink(props: Object, formatContext: FormatContext): void {
133 const noscriptTagInScope = formatContext & NOSCRIPT_SCOPE;
134 const rel = props.rel;
135 const href = props.href;
136 if (
137 noscriptTagInScope ||
138 props.itemProp != null ||
139 typeof rel !== 'string' ||
140 typeof href !== 'string' ||
141 href === ''
142 ) {
143 // We shouldn't preload resources that are in noscript or have no configuration.
144 return;
145 }
146
147 switch (rel) {
148 case 'preload': {
149 preload(href, props.as, {
150 crossOrigin: props.crossOrigin,
151 integrity: props.integrity,
152 nonce: props.nonce,
153 type: props.type,
154 fetchPriority: props.fetchPriority,
155 referrerPolicy: props.referrerPolicy,
156 imageSrcSet: props.imageSrcSet,
157 imageSizes: props.imageSizes,
158 media: props.media,
159 });
160 return;
161 }
162 case 'modulepreload': {
163 preloadModule(href, {
164 as: props.as,
165 crossOrigin: props.crossOrigin,
166 integrity: props.integrity,
167 nonce: props.nonce,
168 });
169 return;
170 }
171 case 'stylesheet': {
172 preload(href, 'style', {
173 crossOrigin: props.crossOrigin,
174 integrity: props.integrity,
175 nonce: props.nonce,
176 type: props.type,
177 fetchPriority: props.fetchPriority,
178 referrerPolicy: props.referrerPolicy,
179 media: props.media,
180 });
181 return;
182 }
183 }
184 }
185
186 export function getChildFormatContext(
187 parentContext: FormatContext,
188 type: string,
189 props: Object,
190 ): FormatContext {
191 switch (type) {
192 case 'img':
193 processImg(props, parentContext);
194 return parentContext;
195 case 'link':
196 processLink(props, parentContext);
197 return parentContext;
198 case 'picture':
199 return parentContext | PICTURE_SCOPE;
200 case 'noscript':
201 return parentContext | NOSCRIPT_SCOPE;
202 default:
203 return parentContext;
204 }
205 }