| 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 | } |