Files
react/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
T
Josh Story 4494f2a86f [Float] add support for scripts and other enhancements (#25480)
* float enhance!!!

Support preinit as script
Support resources from async scripts
Support saving the precedence place when rendering the shell

There was a significant change to the flushing order of resources which follows the general principal of...
1. stuff that blocks display
2. stuff that we know will be used
3. stuff that was explicitly preloaded

As a consequence if you preinit a style now it won't automatically flush in the shell unless you actually depend on it in your tree. To avoid races with precedence order we now emit a tag that saves the place amongst the precedence hierarchy so late insertions still end up where they were intended

There is also a novel hydration pathway for certain tags. If you render an async script with an onLoad or onError it will always treat it like an insertion rather than a hydration.

* restore preinit style flushing behavior and nits
2022-10-17 14:00:20 -07:00

768 lines
21 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {
validatePreloadResourceDifference,
validateStyleResourceDifference,
validateStyleAndHintProps,
validateScriptResourceDifference,
validateScriptAndHintProps,
validateLinkPropsForStyleResource,
validateLinkPropsForPreloadResource,
validatePreloadArguments,
validatePreinitArguments,
} from '../shared/ReactDOMResourceValidation';
type Props = {[string]: mixed};
type ResourceType = 'style' | 'font' | 'script';
type PreloadProps = {
rel: 'preload',
as: ResourceType,
href: string,
[string]: mixed,
};
type PreloadResource = {
type: 'preload',
as: ResourceType,
href: string,
props: PreloadProps,
flushed: boolean,
};
type StyleProps = {
rel: 'stylesheet',
href: string,
'data-precedence': string,
[string]: mixed,
};
type StyleResource = {
type: 'style',
href: string,
precedence: string,
props: StyleProps,
flushed: boolean,
inShell: boolean, // flushedInShell
hint: PreloadResource,
set: Set<StyleResource>, // the precedence set this resource should be flushed in
};
type ScriptProps = {
src: string,
[string]: mixed,
};
type ScriptResource = {
type: 'script',
src: string,
props: ScriptProps,
flushed: boolean,
hint: PreloadResource,
};
export type Resource = PreloadResource | StyleResource | ScriptResource;
export type Resources = {
// Request local cache
preloadsMap: Map<string, PreloadResource>,
stylesMap: Map<string, StyleResource>,
scriptsMap: Map<string, ScriptResource>,
// Flushing queues for Resource dependencies
fontPreloads: Set<PreloadResource>,
// usedImagePreloads: Set<PreloadResource>,
precedences: Map<string, Set<StyleResource>>,
usedStylePreloads: Set<PreloadResource>,
scripts: Set<ScriptResource>,
usedScriptPreloads: Set<PreloadResource>,
explicitStylePreloads: Set<PreloadResource>,
// explicitImagePreloads: Set<PreloadResource>,
explicitScriptPreloads: Set<PreloadResource>,
// Module-global-like reference for current boundary resources
boundaryResources: ?BoundaryResources,
...
};
// @TODO add bootstrap script to implicit preloads
export function createResources(): Resources {
return {
// persistent
preloadsMap: new Map(),
stylesMap: new Map(),
scriptsMap: new Map(),
// cleared on flush
fontPreloads: new Set(),
// usedImagePreloads: new Set(),
precedences: new Map(),
usedStylePreloads: new Set(),
scripts: new Set(),
usedScriptPreloads: new Set(),
explicitStylePreloads: new Set(),
// explicitImagePreloads: new Set(),
explicitScriptPreloads: new Set(),
// like a module global for currently rendering boundary
boundaryResources: null,
};
}
export type BoundaryResources = Set<StyleResource>;
export function createBoundaryResources(): BoundaryResources {
return new Set();
}
let currentResources: null | Resources = null;
const currentResourcesStack = [];
export function prepareToRenderResources(resources: Resources) {
currentResourcesStack.push(currentResources);
currentResources = resources;
}
export function finishRenderingResources() {
currentResources = currentResourcesStack.pop();
}
export function setCurrentlyRenderingBoundaryResourcesTarget(
resources: Resources,
boundaryResources: null | BoundaryResources,
) {
resources.boundaryResources = boundaryResources;
}
export const ReactDOMServerDispatcher = {
preload,
preinit,
};
type PreloadAs = ResourceType;
type PreloadOptions = {as: PreloadAs, crossOrigin?: string, integrity?: string};
function preload(href: string, options: PreloadOptions) {
if (!currentResources) {
// While we expect that preload calls are primarily going to be observed
// during render because effects and events don't run on the server it is
// still possible that these get called in module scope. This is valid on
// the client since there is still a document to interact with but on the
// server we need a request to associate the call to. Because of this we
// simply return and do not warn.
return;
}
const resources = currentResources;
if (__DEV__) {
validatePreloadArguments(href, options);
}
if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null
) {
const as = options.as;
let resource = resources.preloadsMap.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
(resource: any)._dev_implicit_construction === true;
const latestProps = preloadPropsFromPreloadOptions(href, as, options);
validatePreloadResourceDifference(
resource.props,
originallyImplicit,
latestProps,
false,
);
}
} else {
resource = createPreloadResource(
resources,
href,
as,
preloadPropsFromPreloadOptions(href, as, options),
);
}
switch (as) {
case 'font': {
resources.fontPreloads.add(resource);
break;
}
case 'style': {
resources.explicitStylePreloads.add(resource);
break;
}
case 'script': {
resources.explicitScriptPreloads.add(resource);
break;
}
}
}
}
type PreinitAs = 'style' | 'script';
type PreinitOptions = {
as: PreinitAs,
precedence?: string,
crossOrigin?: string,
integrity?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (!currentResources) {
// While we expect that preinit calls are primarily going to be observed
// during render because effects and events don't run on the server it is
// still possible that these get called in module scope. This is valid on
// the client since there is still a document to interact with but on the
// server we need a request to associate the call to. Because of this we
// simply return and do not warn.
return;
}
const resources = currentResources;
if (__DEV__) {
validatePreinitArguments(href, options);
}
if (
typeof href === 'string' &&
href &&
typeof options === 'object' &&
options !== null
) {
const as = options.as;
switch (as) {
case 'style': {
let resource = resources.stylesMap.get(href);
if (resource) {
if (__DEV__) {
const latestProps = stylePropsFromPreinitOptions(
href,
resource.precedence,
options,
);
validateStyleResourceDifference(resource.props, latestProps);
}
} else {
const precedence = options.precedence || 'default';
const resourceProps = stylePropsFromPreinitOptions(
href,
precedence,
options,
);
resource = createStyleResource(
resources,
href,
precedence,
resourceProps,
);
}
resource.set.add(resource);
resources.explicitStylePreloads.add(resource.hint);
return;
}
case 'script': {
const src = href;
let resource = resources.scriptsMap.get(src);
if (resource) {
if (__DEV__) {
const latestProps = scriptPropsFromPreinitOptions(src, options);
validateScriptResourceDifference(resource.props, latestProps);
}
} else {
const scriptProps = scriptPropsFromPreinitOptions(src, options);
resource = createScriptResource(resources, src, scriptProps);
resources.scripts.add(resource);
}
return;
}
}
}
}
function preloadPropsFromPreloadOptions(
href: string,
as: ResourceType,
options: PreloadOptions,
): PreloadProps {
return {
href,
rel: 'preload',
as,
crossOrigin: as === 'font' ? '' : options.crossOrigin,
integrity: options.integrity,
};
}
function preloadPropsFromRawProps(
href: string,
as: ResourceType,
rawProps: Props,
): PreloadProps {
const props: PreloadProps = Object.assign({}, rawProps);
props.href = href;
props.rel = 'preload';
props.as = as;
if (as === 'font') {
// Font preloads always need CORS anonymous mode so we set it here
// regardless of the props provided. This should warn elsewhere in
// dev
props.crossOrigin = '';
}
return props;
}
function preloadAsStylePropsFromProps(
href: string,
props: Props | StyleProps,
): PreloadProps {
return {
rel: 'preload',
as: 'style',
href: href,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
media: props.media,
hrefLang: props.hrefLang,
referrerPolicy: props.referrerPolicy,
};
}
function preloadAsScriptPropsFromProps(
href: string,
props: Props | ScriptProps,
): PreloadProps {
return {
rel: 'preload',
as: 'script',
href,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
referrerPolicy: props.referrerPolicy,
};
}
function createPreloadResource(
resources: Resources,
href: string,
as: ResourceType,
props: PreloadProps,
): PreloadResource {
const {preloadsMap} = resources;
if (__DEV__) {
if (preloadsMap.has(href)) {
console.error(
'createPreloadResource was called when a preload Resource matching the same href already exists. This is a bug in React.',
);
}
}
const resource = {
type: 'preload',
as,
href,
flushed: false,
props,
};
preloadsMap.set(href, resource);
return resource;
}
function stylePropsFromRawProps(
href: string,
precedence: string,
rawProps: Props,
): StyleProps {
const props: StyleProps = Object.assign({}, rawProps);
props.href = href;
props.rel = 'stylesheet';
props['data-precedence'] = precedence;
delete props.precedence;
return props;
}
function stylePropsFromPreinitOptions(
href: string,
precedence: string,
options: PreinitOptions,
): StyleProps {
return {
rel: 'stylesheet',
href,
'data-precedence': precedence,
crossOrigin: options.crossOrigin,
};
}
function createStyleResource(
resources: Resources,
href: string,
precedence: string,
props: StyleProps,
): StyleResource {
if (__DEV__) {
if (resources.stylesMap.has(href)) {
console.error(
'createStyleResource was called when a style Resource matching the same href already exists. This is a bug in React.',
);
}
}
const {stylesMap, preloadsMap, precedences} = resources;
// If this is the first time we've seen this precedence we encode it's position in our set even though
// we don't add the resource to this set yet
let precedenceSet = precedences.get(precedence);
if (!precedenceSet) {
precedenceSet = new Set();
precedences.set(precedence, precedenceSet);
}
let hint = preloadsMap.get(href);
if (hint) {
// If a preload for this style Resource already exists there are certain props we want to adopt
// on the style Resource, primarily focussed on making sure the style network pathways utilize
// the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
// and a stylesheet the stylesheet will make a new request even if the preload had already loaded
adoptPreloadPropsForStyleProps(props, hint.props);
if (__DEV__) {
validateStyleAndHintProps(
hint.props,
props,
(hint: any)._dev_implicit_construction,
);
}
} else {
const preloadResourceProps = preloadAsStylePropsFromProps(href, props);
hint = createPreloadResource(
resources,
href,
'style',
preloadResourceProps,
);
if (__DEV__) {
(hint: any)._dev_implicit_construction = true;
}
resources.explicitStylePreloads.add(hint);
}
const resource = {
type: 'style',
href,
precedence,
flushed: false,
inShell: false,
props,
hint,
set: precedenceSet,
};
stylesMap.set(href, resource);
return resource;
}
function adoptPreloadPropsForStyleProps(
resourceProps: StyleProps,
preloadProps: PreloadProps,
): void {
if (resourceProps.crossOrigin == null)
resourceProps.crossOrigin = preloadProps.crossOrigin;
if (resourceProps.referrerPolicy == null)
resourceProps.referrerPolicy = preloadProps.referrerPolicy;
if (resourceProps.title == null) resourceProps.title = preloadProps.title;
}
function scriptPropsFromPreinitOptions(
src: string,
options: PreinitOptions,
): ScriptProps {
return {
src,
async: true,
crossOrigin: options.crossOrigin,
integrity: options.integrity,
};
}
function scriptPropsFromRawProps(src: string, rawProps: Props): ScriptProps {
const props = Object.assign({}, rawProps);
props.src = src;
return props;
}
function createScriptResource(
resources: Resources,
src: string,
props: ScriptProps,
): ScriptResource {
if (__DEV__) {
if (resources.scriptsMap.has(src)) {
console.error(
'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
);
}
}
const {scriptsMap, preloadsMap} = resources;
let hint = preloadsMap.get(src);
if (hint) {
// If a preload for this style Resource already exists there are certain props we want to adopt
// on the style Resource, primarily focussed on making sure the style network pathways utilize
// the preload pathways. For instance if you have diffreent crossOrigin attributes for a preload
// and a stylesheet the stylesheet will make a new request even if the preload had already loaded
adoptPreloadPropsForScriptProps(props, hint.props);
if (__DEV__) {
validateScriptAndHintProps(
hint.props,
props,
(hint: any)._dev_implicit_construction,
);
}
} else {
const preloadResourceProps = preloadAsScriptPropsFromProps(src, props);
hint = createPreloadResource(
resources,
src,
'script',
preloadResourceProps,
);
if (__DEV__) {
(hint: any)._dev_implicit_construction = true;
}
resources.explicitScriptPreloads.add(hint);
}
const resource = {
type: 'script',
src,
flushed: false,
props,
hint,
};
scriptsMap.set(src, resource);
return resource;
}
function adoptPreloadPropsForScriptProps(
resourceProps: ScriptProps,
preloadProps: PreloadProps,
): void {
if (resourceProps.crossOrigin == null)
resourceProps.crossOrigin = preloadProps.crossOrigin;
if (resourceProps.referrerPolicy == null)
resourceProps.referrerPolicy = preloadProps.referrerPolicy;
if (resourceProps.integrity == null)
resourceProps.integrity = preloadProps.integrity;
}
// Construct a resource from link props.
export function resourcesFromLink(props: Props): boolean {
if (!currentResources) {
throw new Error(
'"currentResources" was expected to exist. This is a bug in React.',
);
}
const resources = currentResources;
const {rel, href} = props;
if (!href || typeof href !== 'string') {
return false;
}
switch (rel) {
case 'stylesheet': {
const {onLoad, onError, precedence, disabled} = props;
if (
typeof precedence !== 'string' ||
onLoad ||
onError ||
disabled != null
) {
// This stylesheet is either not opted into Resource semantics or has conflicting properties which
// disqualify it for such. We can still create a preload resource to help it load faster on the
// client
if (__DEV__) {
validateLinkPropsForStyleResource(props);
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
let preloadResource = resources.preloadsMap.get(href);
if (!preloadResource) {
preloadResource = createPreloadResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
resources,
href,
'style',
preloadAsStylePropsFromProps(href, props),
);
if (__DEV__) {
(preloadResource: any)._dev_implicit_construction = true;
}
resources.usedStylePreloads.add(preloadResource);
}
return false;
} else {
// We are able to convert this link element to a resource exclusively. We construct the relevant Resource
// and return true indicating that this link was fully consumed.
let resource = resources.stylesMap.get(href);
if (resource) {
if (__DEV__) {
const resourceProps = stylePropsFromRawProps(
href,
precedence,
props,
);
adoptPreloadPropsForStyleProps(resourceProps, resource.hint.props);
validateStyleResourceDifference(resource.props, resourceProps);
}
} else {
const resourceProps = stylePropsFromRawProps(href, precedence, props);
resource = createStyleResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
currentResources,
href,
precedence,
resourceProps,
);
resources.usedStylePreloads.add(resource.hint);
}
if (resources.boundaryResources) {
resources.boundaryResources.add(resource);
} else {
resource.set.add(resource);
}
return true;
}
}
case 'preload': {
const {as} = props;
switch (as) {
case 'script':
case 'style':
case 'font': {
if (__DEV__) {
validateLinkPropsForPreloadResource(props);
}
let resource = resources.preloadsMap.get(href);
if (resource) {
if (__DEV__) {
const originallyImplicit =
(resource: any)._dev_implicit_construction === true;
const latestProps = preloadPropsFromRawProps(href, as, props);
validatePreloadResourceDifference(
resource.props,
originallyImplicit,
latestProps,
false,
);
}
} else {
resource = createPreloadResource(
resources,
href,
as,
preloadPropsFromRawProps(href, as, props),
);
switch (as) {
case 'script': {
resources.explicitScriptPreloads.add(resource);
break;
}
case 'style': {
resources.explicitStylePreloads.add(resource);
break;
}
case 'font': {
resources.fontPreloads.add(resource);
break;
}
}
}
return true;
}
}
return false;
}
}
return false;
}
// Construct a resource from link props.
export function resourcesFromScript(props: Props): boolean {
if (!currentResources) {
throw new Error(
'"currentResources" was expected to exist. This is a bug in React.',
);
}
const resources = currentResources;
const {src, async, onLoad, onError} = props;
if (!src || typeof src !== 'string') {
return false;
}
if (async) {
if (onLoad || onError) {
if (__DEV__) {
// validate
}
let preloadResource = resources.preloadsMap.get(src);
if (!preloadResource) {
preloadResource = createPreloadResource(
// $FlowFixMe[incompatible-call] found when upgrading Flow
resources,
src,
'script',
preloadAsScriptPropsFromProps(src, props),
);
if (__DEV__) {
(preloadResource: any)._dev_implicit_construction = true;
}
resources.usedScriptPreloads.add(preloadResource);
}
} else {
let resource = resources.scriptsMap.get(src);
if (resource) {
if (__DEV__) {
const latestProps = scriptPropsFromRawProps(src, props);
adoptPreloadPropsForScriptProps(latestProps, resource.hint.props);
validateScriptResourceDifference(resource.props, latestProps);
}
} else {
const resourceProps = scriptPropsFromRawProps(src, props);
resource = createScriptResource(resources, src, resourceProps);
resources.scripts.add(resource);
}
}
return true;
}
return false;
}
export function hoistResources(
resources: Resources,
source: BoundaryResources,
): void {
const currentBoundaryResources = resources.boundaryResources;
if (currentBoundaryResources) {
source.forEach(resource => currentBoundaryResources.add(resource));
source.clear();
}
}
export function hoistResourcesToRoot(
resources: Resources,
boundaryResources: BoundaryResources,
): void {
boundaryResources.forEach(resource => resource.set.add(resource));
boundaryResources.clear();
}