18
import type {ReactScopeInstance} from 'shared/ReactTypes';
19
import type {AncestorInfoDev} from './validateDOMNesting';
20
21
+import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
22
+import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
23
+// TODO: Remove this deep import when we delete the legacy root API
24
+import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
25
+
26
+import hasOwnProperty from 'shared/hasOwnProperty';
27
+import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
28
+import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
29
+const {Dispatcher} = ReactDOMSharedInternals;
30
+
31
import {
32
precacheFiberNode,
33
updateFiberProps,
36
getInstanceFromNode as getInstanceFromNodeDOMTree,
37
isContainerMarkedAsRoot,
38
detachDeletedInstance,
39
+ getResourcesFromRoot,
40
isMarkedHoistable,
41
markNodeAsHoistable,
42
+ isOwnedInstance,
43
} from './ReactDOMComponentTree';
44
export {detachDeletedInstance};
45
import {hasRole} from './DOMAccessibilityRoles';
98
HostSingleton,
99
} from 'react-reconciler/src/ReactWorkTags';
100
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
89
-
90
-import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
91
-
92
-// TODO: Remove this deep import when we delete the legacy root API
93
-import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
94
-
101
import {
96
- prepareToRenderResources,
97
- cleanupAfterRenderResources,
98
-} from './ReactDOMFloatClient';
99
-import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
102
+ validatePreloadArguments,
103
+ validatePreinitArguments,
104
+ validateLinkPropsForStyleResource,
105
+ getValueDescriptorExpectingObjectForWarning,
106
+ getValueDescriptorExpectingEnumForWarning,
107
+} from '../shared/ReactDOMResourceValidation';
108
109
export type Type = string;
110
export type Props = {
146
| interface extends DocumentFragment {_reactRootContainer?: FiberRoot};
147
export type Instance = Element;
148
export type TextInstance = Text;
141
-export type {HoistableRoot} from './ReactDOMFloatClient';
149
export interface SuspenseInstance extends Comment {
150
_reactRetry?: () => void;
151
}
1620
});
1621
}
1622
1616
-export function maySuspendCommit(type: Type, props: Props): boolean {
1617
- return false;
1618
-}
1619
-
1620
-export function preloadInstance(type: Type, props: Props): boolean {
1621
- // Return true to indicate it's already loaded
1622
- return true;
1623
-}
1624
-
1625
-export function startSuspendingCommit(): void {}
1626
-
1627
-export function suspendInstance(type: Type, props: Props): void {}
1628
-
1629
-export function waitForCommitToBeReady(): null {
1630
- return null;
1631
-}
1632
-
1633
-// -------------------
1634
-// Resources
1635
-// -------------------
1636
-
1637
-export const supportsResources = true;
1638
-
1639
-export function isHostHoistableType(
1640
- type: string,
1641
- props: RawProps,
1642
- hostContext: HostContext,
1643
-): boolean {
1644
- let outsideHostContainerContext: boolean;
1645
- let namespace: HostContextProd;
1646
- if (__DEV__) {
1647
- const hostContextDev: HostContextDev = (hostContext: any);
1648
- // We can only render resources when we are not within the host container context
1649
- outsideHostContainerContext =
1650
- !hostContextDev.ancestorInfo.containerTagInScope;
1651
- namespace = hostContextDev.namespace;
1652
- } else {
1653
- const hostContextProd: HostContextProd = (hostContext: any);
1654
- namespace = hostContextProd;
1655
- }
1656
-
1657
- // Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
1658
- if (namespace === SVG_NAMESPACE || props.itemProp != null) {
1659
- if (__DEV__) {
1660
- if (
1661
- outsideHostContainerContext &&
1662
- props.itemProp != null &&
1663
- (type === 'meta' ||
1664
- type === 'title' ||
1665
- type === 'style' ||
1666
- type === 'link' ||
1667
- type === 'script')
1668
- ) {
1669
- console.error(
1670
- 'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
1671
- ' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
1672
- ' Otherwise, try moving this tag into the <head> or <body> of the Document.',
1673
- type,
1674
- type,
1675
- );
1676
- }
1677
- }
1678
- return false;
1679
- }
1680
-
1681
- switch (type) {
1682
- case 'meta':
1683
- case 'title': {
1684
- return true;
1685
- }
1686
- case 'style': {
1687
- if (
1688
- typeof props.precedence !== 'string' ||
1689
- typeof props.href !== 'string' ||
1690
- props.href === ''
1691
- ) {
1692
- if (__DEV__) {
1693
- if (outsideHostContainerContext) {
1694
- console.error(
1695
- 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key.' +
1696
- ' React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that' +
1697
- ' does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' +
1698
- ' Note that hoisting <style> tags is considered an advanced feature that most will not use directly.' +
1699
- ' Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style>' +
1700
- ' to the <style> tag.',
1701
- );
1702
- }
1703
- }
1704
- return false;
1705
- }
1706
- return true;
1707
- }
1708
- case 'link': {
1709
- if (
1710
- typeof props.rel !== 'string' ||
1711
- typeof props.href !== 'string' ||
1712
- props.href === '' ||
1713
- props.onLoad ||
1714
- props.onError
1715
- ) {
1716
- if (__DEV__) {
1717
- if (
1718
- props.rel === 'stylesheet' &&
1719
- typeof props.precedence === 'string'
1720
- ) {
1721
- validateLinkPropsForStyleResource(props);
1722
- }
1723
- if (outsideHostContainerContext) {
1724
- if (
1725
- typeof props.rel !== 'string' ||
1726
- typeof props.href !== 'string' ||
1727
- props.href === ''
1728
- ) {
1729
- console.error(
1730
- 'Cannot render a <link> outside the main document without a `rel` and `href` prop.' +
1731
- ' Try adding a `rel` and/or `href` prop to this <link> or moving the link into the <head> tag',
1732
- );
1733
- } else if (props.onError || props.onLoad) {
1734
- console.error(
1735
- 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
1736
- ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1737
- ' somewhere in the <body>.',
1738
- );
1739
- }
1740
- }
1741
- }
1742
- return false;
1743
- }
1744
- switch (props.rel) {
1745
- case 'stylesheet': {
1746
- const {precedence, disabled} = props;
1747
- if (__DEV__) {
1748
- if (typeof precedence !== 'string') {
1749
- if (outsideHostContainerContext) {
1750
- console.error(
1751
- 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
1752
- ' Consider adding precedence="default" or moving it into the root <head> tag.',
1753
- );
1754
- }
1755
- }
1756
- }
1757
- return typeof precedence === 'string' && disabled == null;
1758
- }
1759
- default: {
1760
- return true;
1761
- }
1762
- }
1763
- }
1764
- case 'script': {
1765
- if (
1766
- props.async !== true ||
1767
- props.onLoad ||
1768
- props.onError ||
1769
- typeof props.src !== 'string' ||
1770
- !props.src
1771
- ) {
1772
- if (__DEV__) {
1773
- if (outsideHostContainerContext) {
1774
- if (props.async !== true) {
1775
- console.error(
1776
- 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
1777
- ' Try adding async="" or moving it into the root <head> tag.',
1778
- );
1779
- } else if (props.onLoad || props.onError) {
1780
- console.error(
1781
- 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
1782
- ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
1783
- ' somewhere in the <body>.',
1784
- );
1785
- } else {
1786
- console.error(
1787
- 'Cannot render a <script> outside the main document without `async={true}` and a non-empty `src` prop.' +
1788
- ' Ensure there is a valid `src` and either make the script async or move it into the root <head> tag or' +
1789
- ' somewhere in the <body>.',
1790
- );
1791
- }
1792
- }
1793
- }
1794
- return false;
1795
- }
1796
- return true;
1797
- }
1798
- case 'noscript':
1799
- case 'template': {
1800
- if (__DEV__) {
1801
- if (outsideHostContainerContext) {
1802
- console.error(
1803
- 'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
1804
- type,
1805
- );
1806
- }
1807
- }
1808
- return false;
1809
- }
1810
- }
1811
- return false;
1812
-}
1813
-
1814
-export function prepareRendererToRender(rootContainer: Container) {
1815
- if (enableFloat) {
1816
- prepareToRenderResources(rootContainer);
1817
- }
1818
-}
1819
-
1820
-export function resetRendererAfterRender() {
1821
- if (enableFloat) {
1822
- cleanupAfterRenderResources();
1823
- }
1824
-}
1825
-
1826
-export {
1827
- getHoistableRoot,
1828
- getResource,
1829
- acquireResource,
1830
- releaseResource,
1831
- hydrateHoistable,
1832
- mountHoistable,
1833
- unmountHoistable,
1834
- prepareToCommitHoistables,
1835
-} from './ReactDOMFloatClient';
1836
-
1623
// -------------------
1624
// Singletons
1625
// -------------------
1762
}
1763
return;
1764
}
1765
+
1766
+// -------------------
1767
+// Resources
1768
+// -------------------
1769
+
1770
+export const supportsResources = true;
1771
+
1772
+// The resource types we support. currently they match the form for the as argument.
1773
+// In the future this may need to change, especially when modules / scripts are supported
1774
+type ResourceType = 'style' | 'font' | 'script';
1775
+
1776
+type HoistableTagType = 'link' | 'meta' | 'title';
1777
+type TResource<
1778
+ T: 'stylesheet' | 'style' | 'script' | 'void',
1779
+ S: null | {...},
1780
+> = {
1781
+ type: T,
1782
+ instance: null | Instance,
1783
+ count: number,
1784
+ state: S,
1785
+};
1786
+type StylesheetResource = TResource<'stylesheet', StylesheetState>;
1787
+type StyleTagResource = TResource<'style', null>;
1788
+type StyleResource = StyleTagResource | StylesheetResource;
1789
+type ScriptResource = TResource<'script', null>;
1790
+type VoidResource = TResource<'void', null>;
1791
+type Resource = StyleResource | ScriptResource | VoidResource;
1792
+
1793
+type LoadingState = number;
1794
+const NotLoaded = /* */ 0b000;
1795
+const Loaded = /* */ 0b001;
1796
+const Errored = /* */ 0b010;
1797
+const Settled = /* */ 0b011;
1798
+const Inserted = /* */ 0b100;
1799
+
1800
+type StylesheetState = {
1801
+ loading: LoadingState,
1802
+ preload: null | HTMLLinkElement,
1803
+};
1804
+
1805
+type StyleTagProps = {
1806
+ 'data-href': string,
1807
+ 'data-precedence': string,
1808
+ [string]: mixed,
1809
+};
1810
+type StylesheetProps = {
1811
+ rel: 'stylesheet',
1812
+ href: string,
1813
+ 'data-precedence': string,
1814
+ [string]: mixed,
1815
+};
1816
+
1817
+type ScriptProps = {
1818
+ src: string,
1819
+ async: true,
1820
+ [string]: mixed,
1821
+};
1822
+
1823
+type PreloadProps = {
1824
+ rel: 'preload',
1825
+ href: string,
1826
+ [string]: mixed,
1827
+};
1828
+
1829
+export type RootResources = {
1830
+ hoistableStyles: Map<string, StyleResource>,
1831
+ hoistableScripts: Map<string, ScriptResource>,
1832
+};
1833
+
1834
+export function prepareToCommitHoistables() {
1835
+ tagCaches = null;
1836
+}
1837
+
1838
+// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
1839
+// called when there is no rendering we track the last used document. It is not safe to insert
1840
+// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
1841
+// that the resource is meant to apply too (for example stylesheets or scripts). This is only
1842
+// appropriate for resources that don't really have a strict tie to the document itself for example
1843
+// preloads
1844
+let lastCurrentDocument: ?Document = null;
1845
+let previousDispatcher = null;
1846
+export function prepareRendererToRender(rootContainer: Container) {
1847
+ if (enableFloat) {
1848
+ const rootNode = getHoistableRoot(rootContainer);
1849
+ lastCurrentDocument = getDocumentFromRoot(rootNode);
1850
+
1851
+ previousDispatcher = Dispatcher.current;
1852
+ Dispatcher.current = ReactDOMClientDispatcher;
1853
+ }
1854
+}
1855
+
1856
+export function resetRendererAfterRender() {
1857
+ if (enableFloat) {
1858
+ Dispatcher.current = previousDispatcher;
1859
+ previousDispatcher = null;
1860
+ }
1861
+}
1862
+
1863
+// global collections of Resources
1864
+const preloadPropsMap: Map<string, PreloadProps> = new Map();
1865
+const preconnectsSet: Set<string> = new Set();
1866
+
1867
+export type HoistableRoot = Document | ShadowRoot;
1868
+
1869
+// getRootNode is missing from IE and old jsdom versions
1870
+export function getHoistableRoot(container: Container): HoistableRoot {
1871
+ // $FlowFixMe[method-unbinding]
1872
+ return typeof container.getRootNode === 'function'
1873
+ ? /* $FlowFixMe[incompatible-return] Flow types this as returning a `Node`,
1874
+ * but it's either a `Document` or `ShadowRoot`. */
1875
+ container.getRootNode()
1876
+ : container.ownerDocument;
1877
+}
1878
+
1879
+function getCurrentResourceRoot(): null | HoistableRoot {
1880
+ const currentContainer = getCurrentRootHostContainer();
1881
+ return currentContainer ? getHoistableRoot(currentContainer) : null;
1882
+}
1883
+
1884
+// Preloads are somewhat special. Even if we don't have the Document
1885
+// used by the root that is rendering a component trying to insert a preload
1886
+// we can still seed the file cache by doing the preload on any document we have
1887
+// access to. We prefer the currentDocument if it exists, we also prefer the
1888
+// lastCurrentDocument if that exists. As a fallback we will use the window.document
1889
+// if available.
1890
+function getDocumentForPreloads(): ?Document {
1891
+ const root = getCurrentResourceRoot();
1892
+ if (root) {
1893
+ return root.ownerDocument || root;
1894
+ } else {
1895
+ try {
1896
+ return lastCurrentDocument || window.document;
1897
+ } catch (error) {
1898
+ return null;
1899
+ }
1900
+ }
1901
+}
1902
+
1903
+function getDocumentFromRoot(root: HoistableRoot): Document {
1904
+ return root.ownerDocument || root;
1905
+}
1906
+
1907
+// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
1908
+// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
1909
+// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
1910
+export const ReactDOMClientDispatcher = {
1911
+ prefetchDNS,
1912
+ preconnect,
1913
+ preload,
1914
+ preinit,
1915
+};
1916
+
1917
+function preconnectAs(
1918
+ rel: 'preconnect' | 'dns-prefetch',
1919
+ crossOrigin: null | '' | 'use-credentials',
1920
+ href: string,
1921
+) {
1922
+ const ownerDocument = getDocumentForPreloads();
1923
+ if (typeof href === 'string' && href && ownerDocument) {
1924
+ const limitedEscapedHref =
1925
+ escapeSelectorAttributeValueInsideDoubleQuotes(href);
1926
+ let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
1927
+ if (typeof crossOrigin === 'string') {
1928
+ key += `[crossorigin="${crossOrigin}"]`;
1929
+ }
1930
+ if (!preconnectsSet.has(key)) {
1931
+ preconnectsSet.add(key);
1932
+
1933
+ const preconnectProps = {rel, crossOrigin, href};
1934
+ if (null === ownerDocument.querySelector(key)) {
1935
+ const instance = ownerDocument.createElement('link');
1936
+ setInitialProperties(instance, 'link', preconnectProps);
1937
+ markNodeAsHoistable(instance);
1938
+ (ownerDocument.head: any).appendChild(instance);
1939
+ }
1940
+ }
1941
+ }
1942
+}
1943
+
1944
+function prefetchDNS(href: string, options?: mixed) {
1945
+ if (__DEV__) {
1946
+ if (typeof href !== 'string' || !href) {
1947
+ console.error(
1948
+ 'ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
1949
+ getValueDescriptorExpectingObjectForWarning(href),
1950
+ );
1951
+ } else if (options != null) {
1952
+ if (
1953
+ typeof options === 'object' &&
1954
+ hasOwnProperty.call(options, 'crossOrigin')
1955
+ ) {
1956
+ console.error(
1957
+ 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.',
1958
+ getValueDescriptorExpectingEnumForWarning(options),
1959
+ );
1960
+ } else {
1961
+ console.error(
1962
+ 'ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.',
1963
+ getValueDescriptorExpectingEnumForWarning(options),
1964
+ );
1965
+ }
1966
+ }
1967
+ }
1968
+ preconnectAs('dns-prefetch', null, href);
1969
+}
1970
+
1971
+function preconnect(href: string, options?: {crossOrigin?: string}) {
1972
+ if (__DEV__) {
1973
+ if (typeof href !== 'string' || !href) {
1974
+ console.error(
1975
+ 'ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.',
1976
+ getValueDescriptorExpectingObjectForWarning(href),
1977
+ );
1978
+ } else if (options != null && typeof options !== 'object') {
1979
+ console.error(
1980
+ 'ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.',
1981
+ getValueDescriptorExpectingEnumForWarning(options),
1982
+ );
1983
+ } else if (options != null && typeof options.crossOrigin !== 'string') {
1984
+ console.error(
1985
+ 'ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.',
1986
+ getValueDescriptorExpectingObjectForWarning(options.crossOrigin),
1987
+ );
1988
+ }
1989
+ }
1990
+ const crossOrigin =
1991
+ options == null || typeof options.crossOrigin !== 'string'
1992
+ ? null
1993
+ : options.crossOrigin === 'use-credentials'
1994
+ ? 'use-credentials'
1995
+ : '';
1996
+ preconnectAs('preconnect', crossOrigin, href);
1997
+}
1998
+
1999
+type PreloadAs = ResourceType;
2000
+type PreloadOptions = {
2001
+ as: PreloadAs,
2002
+ crossOrigin?: string,
2003
+ integrity?: string,
2004
+ type?: string,
2005
+};
2006
+function preload(href: string, options: PreloadOptions) {
2007
+ if (__DEV__) {
2008
+ validatePreloadArguments(href, options);
2009
+ }
2010
+ const ownerDocument = getDocumentForPreloads();
2011
+ if (
2012
+ typeof href === 'string' &&
2013
+ href &&
2014
+ typeof options === 'object' &&
2015
+ options !== null &&
2016
+ ownerDocument
2017
+ ) {
2018
+ const as = options.as;
2019
+ const limitedEscapedHref =
2020
+ escapeSelectorAttributeValueInsideDoubleQuotes(href);
2021
+ const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2022
+ let key = preloadKey;
2023
+ switch (as) {
2024
+ case 'style':
2025
+ key = getStyleKey(href);
2026
+ break;
2027
+ case 'script':
2028
+ key = getScriptKey(href);
2029
+ break;
2030
+ }
2031
+ if (!preloadPropsMap.has(key)) {
2032
+ const preloadProps = preloadPropsFromPreloadOptions(href, as, options);
2033
+ preloadPropsMap.set(key, preloadProps);
2034
+
2035
+ if (null === ownerDocument.querySelector(preloadKey)) {
2036
+ const instance = ownerDocument.createElement('link');
2037
+ setInitialProperties(instance, 'link', preloadProps);
2038
+ markNodeAsHoistable(instance);
2039
+ (ownerDocument.head: any).appendChild(instance);
2040
+ }
2041
+ }
2042
+ }
2043
+}
2044
+
2045
+function preloadPropsFromPreloadOptions(
2046
+ href: string,
2047
+ as: ResourceType,
2048
+ options: PreloadOptions,
2049
+): PreloadProps {
2050
+ return {
2051
+ href,
2052
+ rel: 'preload',
2053
+ as,
2054
+ crossOrigin: as === 'font' ? '' : options.crossOrigin,
2055
+ integrity: options.integrity,
2056
+ type: options.type,
2057
+ };
2058
+}
2059
+
2060
+type PreinitAs = 'style' | 'script';
2061
+type PreinitOptions = {
2062
+ as: PreinitAs,
2063
+ precedence?: string,
2064
+ crossOrigin?: string,
2065
+ integrity?: string,
2066
+};
2067
+function preinit(href: string, options: PreinitOptions) {
2068
+ if (__DEV__) {
2069
+ validatePreinitArguments(href, options);
2070
+ }
2071
+
2072
+ if (
2073
+ typeof href === 'string' &&
2074
+ href &&
2075
+ typeof options === 'object' &&
2076
+ options !== null
2077
+ ) {
2078
+ const resourceRoot = getCurrentResourceRoot();
2079
+ const as = options.as;
2080
+ if (!resourceRoot) {
2081
+ if (as === 'style' || as === 'script') {
2082
+ // We are going to emit a preload as a best effort fallback since this preinit
2083
+ // was called outside of a render. Given the passive nature of this fallback
2084
+ // we do not warn in dev when props disagree if there happens to already be a
2085
+ // matching preload with this href
2086
+ const preloadDocument = getDocumentForPreloads();
2087
+ if (preloadDocument) {
2088
+ const limitedEscapedHref =
2089
+ escapeSelectorAttributeValueInsideDoubleQuotes(href);
2090
+ const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2091
+ let key = preloadKey;
2092
+ switch (as) {
2093
+ case 'style':
2094
+ key = getStyleKey(href);
2095
+ break;
2096
+ case 'script':
2097
+ key = getScriptKey(href);
2098
+ break;
2099
+ }
2100
+ if (!preloadPropsMap.has(key)) {
2101
+ const preloadProps = preloadPropsFromPreinitOptions(
2102
+ href,
2103
+ as,
2104
+ options,
2105
+ );
2106
+ preloadPropsMap.set(key, preloadProps);
2107
+
2108
+ if (null === preloadDocument.querySelector(preloadKey)) {
2109
+ const instance = preloadDocument.createElement('link');
2110
+ setInitialProperties(instance, 'link', preloadProps);
2111
+ markNodeAsHoistable(instance);
2112
+ (preloadDocument.head: any).appendChild(instance);
2113
+ }
2114
+ }
2115
+ }
2116
+ }
2117
+ return;
2118
+ }
2119
+
2120
+ switch (as) {
2121
+ case 'style': {
2122
+ const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2123
+
2124
+ const key = getStyleKey(href);
2125
+ const precedence = options.precedence || 'default';
2126
+
2127
+ // Check if this resource already exists
2128
+ let resource = styles.get(key);
2129
+ if (resource) {
2130
+ // We can early return. The resource exists and there is nothing
2131
+ // more to do
2132
+ return;
2133
+ }
2134
+
2135
+ const state = {
2136
+ loading: NotLoaded,
2137
+ preload: null,
2138
+ };
2139
+
2140
+ // Attempt to hydrate instance from DOM
2141
+ let instance: null | Instance = resourceRoot.querySelector(
2142
+ getStylesheetSelectorFromKey(key),
2143
+ );
2144
+ if (instance) {
2145
+ state.loading = Loaded;
2146
+ } else {
2147
+ // Construct a new instance and insert it
2148
+ const stylesheetProps = stylesheetPropsFromPreinitOptions(
2149
+ href,
2150
+ precedence,
2151
+ options,
2152
+ );
2153
+ const preloadProps = preloadPropsMap.get(key);
2154
+ if (preloadProps) {
2155
+ adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
2156
+ }
2157
+ const ownerDocument = getDocumentFromRoot(resourceRoot);
2158
+ const link = (instance = ownerDocument.createElement('link'));
2159
+ markNodeAsHoistable(link);
2160
+ setInitialProperties(link, 'link', stylesheetProps);
2161
+
2162
+ (link: any)._p = new Promise((resolve, reject) => {
2163
+ link.onload = resolve;
2164
+ link.onerror = reject;
2165
+ });
2166
+ link.addEventListener('load', () => {
2167
+ state.loading |= Loaded;
2168
+ });
2169
+ link.addEventListener('error', () => {
2170
+ state.loading |= Errored;
2171
+ });
2172
+
2173
+ insertStylesheet(instance, precedence, resourceRoot);
2174
+ }
2175
+
2176
+ // Construct a Resource and cache it
2177
+ resource = {
2178
+ type: 'stylesheet',
2179
+ instance,
2180
+ count: 1,
2181
+ state,
2182
+ };
2183
+ styles.set(key, resource);
2184
+ return;
2185
+ }
2186
+ case 'script': {
2187
+ const src = href;
2188
+ const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2189
+
2190
+ const key = getScriptKey(src);
2191
+
2192
+ // Check if this resource already exists
2193
+ let resource = scripts.get(key);
2194
+ if (resource) {
2195
+ // We can early return. The resource exists and there is nothing
2196
+ // more to do
2197
+ return;
2198
+ }
2199
+
2200
+ // Attempt to hydrate instance from DOM
2201
+ let instance: null | Instance = resourceRoot.querySelector(
2202
+ getScriptSelectorFromKey(key),
2203
+ );
2204
+ if (!instance) {
2205
+ // Construct a new instance and insert it
2206
+ const scriptProps = scriptPropsFromPreinitOptions(src, options);
2207
+ // Adopt certain preload props
2208
+ const preloadProps = preloadPropsMap.get(key);
2209
+ if (preloadProps) {
2210
+ adoptPreloadPropsForScript(scriptProps, preloadProps);
2211
+ }
2212
+ const ownerDocument = getDocumentFromRoot(resourceRoot);
2213
+ instance = ownerDocument.createElement('script');
2214
+ markNodeAsHoistable(instance);
2215
+ setInitialProperties(instance, 'link', scriptProps);
2216
+ (ownerDocument.head: any).appendChild(instance);
2217
+ }
2218
+
2219
+ // Construct a Resource and cache it
2220
+ resource = {
2221
+ type: 'script',
2222
+ instance,
2223
+ count: 1,
2224
+ state: null,
2225
+ };
2226
+ scripts.set(key, resource);
2227
+ return;
2228
+ }
2229
+ }
2230
+ }
2231
+}
2232
+
2233
+function preloadPropsFromPreinitOptions(
2234
+ href: string,
2235
+ as: ResourceType,
2236
+ options: PreinitOptions,
2237
+): PreloadProps {
2238
+ return {
2239
+ href,
2240
+ rel: 'preload',
2241
+ as,
2242
+ crossOrigin: as === 'font' ? '' : options.crossOrigin,
2243
+ integrity: options.integrity,
2244
+ };
2245
+}
2246
+
2247
+function stylesheetPropsFromPreinitOptions(
2248
+ href: string,
2249
+ precedence: string,
2250
+ options: PreinitOptions,
2251
+): StylesheetProps {
2252
+ return {
2253
+ rel: 'stylesheet',
2254
+ href,
2255
+ 'data-precedence': precedence,
2256
+ crossOrigin: options.crossOrigin,
2257
+ };
2258
+}
2259
+
2260
+function scriptPropsFromPreinitOptions(
2261
+ src: string,
2262
+ options: PreinitOptions,
2263
+): ScriptProps {
2264
+ return {
2265
+ src,
2266
+ async: true,
2267
+ crossOrigin: options.crossOrigin,
2268
+ integrity: options.integrity,
2269
+ };
2270
+}
2271
+
2272
+type StyleTagQualifyingProps = {
2273
+ href: string,
2274
+ precedence: string,
2275
+ [string]: mixed,
2276
+};
2277
+
2278
+type StylesheetQualifyingProps = {
2279
+ rel: 'stylesheet',
2280
+ href: string,
2281
+ precedence: string,
2282
+ [string]: mixed,
2283
+};
2284
+
2285
+// This function is called in begin work and we should always have a currentDocument set
2286
+export function getResource(
2287
+ type: string,
2288
+ currentProps: any,
2289
+ pendingProps: any,
2290
+): null | Resource {
2291
+ const resourceRoot = getCurrentResourceRoot();
2292
+ if (!resourceRoot) {
2293
+ throw new Error(
2294
+ '"resourceRoot" was expected to exist. This is a bug in React.',
2295
+ );
2296
+ }
2297
+ switch (type) {
2298
+ case 'meta':
2299
+ case 'title': {
2300
+ return null;
2301
+ }
2302
+ case 'style': {
2303
+ if (
2304
+ typeof pendingProps.precedence === 'string' &&
2305
+ typeof pendingProps.href === 'string'
2306
+ ) {
2307
+ const key = getStyleKey(pendingProps.href);
2308
+ const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2309
+ let resource = styles.get(key);
2310
+ if (!resource) {
2311
+ resource = {
2312
+ type: 'style',
2313
+ instance: null,
2314
+ count: 0,
2315
+ state: null,
2316
+ };
2317
+ styles.set(key, resource);
2318
+ }
2319
+ return resource;
2320
+ }
2321
+ return {
2322
+ type: 'void',
2323
+ instance: null,
2324
+ count: 0,
2325
+ state: null,
2326
+ };
2327
+ }
2328
+ case 'link': {
2329
+ if (
2330
+ pendingProps.rel === 'stylesheet' &&
2331
+ typeof pendingProps.href === 'string' &&
2332
+ typeof pendingProps.precedence === 'string'
2333
+ ) {
2334
+ const qualifiedProps: StylesheetQualifyingProps = pendingProps;
2335
+ const key = getStyleKey(qualifiedProps.href);
2336
+
2337
+ const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2338
+
2339
+ let resource = styles.get(key);
2340
+ if (!resource) {
2341
+ // We asserted this above but Flow can't figure out that the type satisfies
2342
+ const ownerDocument = getDocumentFromRoot(resourceRoot);
2343
+ resource = {
2344
+ type: 'stylesheet',
2345
+ instance: null,
2346
+ count: 0,
2347
+ state: {
2348
+ loading: NotLoaded,
2349
+ preload: null,
2350
+ },
2351
+ };
2352
+ styles.set(key, resource);
2353
+ if (!preloadPropsMap.has(key)) {
2354
+ preloadStylesheet(
2355
+ ownerDocument,
2356
+ key,
2357
+ preloadPropsFromStylesheet(qualifiedProps),
2358
+ resource.state,
2359
+ );
2360
+ }
2361
+ }
2362
+ return resource;
2363
+ }
2364
+ return null;
2365
+ }
2366
+ case 'script': {
2367
+ if (typeof pendingProps.src === 'string' && pendingProps.async === true) {
2368
+ const scriptProps: ScriptProps = pendingProps;
2369
+ const key = getScriptKey(scriptProps.src);
2370
+ const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2371
+
2372
+ let resource = scripts.get(key);
2373
+ if (!resource) {
2374
+ resource = {
2375
+ type: 'script',
2376
+ instance: null,
2377
+ count: 0,
2378
+ state: null,
2379
+ };
2380
+ scripts.set(key, resource);
2381
+ }
2382
+ return resource;
2383
+ }
2384
+ return {
2385
+ type: 'void',
2386
+ instance: null,
2387
+ count: 0,
2388
+ state: null,
2389
+ };
2390
+ }
2391
+ default: {
2392
+ throw new Error(
2393
+ `getResource encountered a type it did not expect: "${type}". this is a bug in React.`,
2394
+ );
2395
+ }
2396
+ }
2397
+}
2398
+
2399
+function styleTagPropsFromRawProps(
2400
+ rawProps: StyleTagQualifyingProps,
2401
+): StyleTagProps {
2402
+ return {
2403
+ ...rawProps,
2404
+ 'data-href': rawProps.href,
2405
+ 'data-precedence': rawProps.precedence,
2406
+ href: null,
2407
+ precedence: null,
2408
+ };
2409
+}
2410
+
2411
+function getStyleKey(href: string) {
2412
+ const limitedEscapedHref =
2413
+ escapeSelectorAttributeValueInsideDoubleQuotes(href);
2414
+ return `href~="${limitedEscapedHref}"`;
2415
+}
2416
+
2417
+function getStyleTagSelectorFromKey(key: string) {
2418
+ return `style[data-${key}]`;
2419
+}
2420
+
2421
+function getStylesheetSelectorFromKey(key: string) {
2422
+ return `link[rel="stylesheet"][${key}]`;
2423
+}
2424
+
2425
+function getPreloadStylesheetSelectorFromKey(key: string) {
2426
+ return `link[rel="preload"][as="style"][${key}]`;
2427
+}
2428
+
2429
+function stylesheetPropsFromRawProps(
2430
+ rawProps: StylesheetQualifyingProps,
2431
+): StylesheetProps {
2432
+ return {
2433
+ ...rawProps,
2434
+ 'data-precedence': rawProps.precedence,
2435
+ precedence: null,
2436
+ };
2437
+}
2438
+function preloadStylesheet(
2439
+ ownerDocument: Document,
2440
+ key: string,
2441
+ preloadProps: PreloadProps,
2442
+ state: StylesheetState,
2443
+) {
2444
+ preloadPropsMap.set(key, preloadProps);
2445
+
2446
+ if (!ownerDocument.querySelector(getStylesheetSelectorFromKey(key))) {
2447
+ // There is no matching stylesheet instance in the Document.
2448
+ // We will insert a preload now to kick off loading because
2449
+ // we expect this stylesheet to commit
2450
+ const preloadEl = ownerDocument.querySelector(
2451
+ getPreloadStylesheetSelectorFromKey(key),
2452
+ );
2453
+ if (preloadEl) {
2454
+ // If we find a preload already it was SSR'd and we won't have an actual
2455
+ // loading state to track. For now we will just assume it is loaded
2456
+ state.loading = Loaded;
2457
+ } else {
2458
+ const instance = ownerDocument.createElement('link');
2459
+ state.preload = instance;
2460
+ instance.addEventListener('load', () => (state.loading |= Loaded));
2461
+ instance.addEventListener('error', () => (state.loading |= Errored));
2462
+ setInitialProperties(instance, 'link', preloadProps);
2463
+ markNodeAsHoistable(instance);
2464
+ (ownerDocument.head: any).appendChild(instance);
2465
+ }
2466
+ }
2467
+}
2468
+
2469
+function preloadPropsFromStylesheet(
2470
+ props: StylesheetQualifyingProps,
2471
+): PreloadProps {
2472
+ return {
2473
+ rel: 'preload',
2474
+ as: 'style',
2475
+ href: props.href,
2476
+ crossOrigin: props.crossOrigin,
2477
+ integrity: props.integrity,
2478
+ media: props.media,
2479
+ hrefLang: props.hrefLang,
2480
+ referrerPolicy: props.referrerPolicy,
2481
+ };
2482
+}
2483
+
2484
+function getScriptKey(src: string): string {
2485
+ const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src);
2486
+ return `[src="${limitedEscapedSrc}"]`;
2487
+}
2488
+
2489
+function getScriptSelectorFromKey(key: string): string {
2490
+ return 'script[async]' + key;
2491
+}
2492
+
2493
+export function acquireResource(
2494
+ hoistableRoot: HoistableRoot,
2495
+ resource: Resource,
2496
+ props: any,
2497
+): null | Instance {
2498
+ resource.count++;
2499
+ if (resource.instance === null) {
2500
+ switch (resource.type) {
2501
+ case 'style': {
2502
+ const qualifiedProps: StyleTagQualifyingProps = props;
2503
+ const key = getStyleKey(qualifiedProps.href);
2504
+
2505
+ // Attempt to hydrate instance from DOM
2506
+ let instance: null | Instance = hoistableRoot.querySelector(
2507
+ getStyleTagSelectorFromKey(key),
2508
+ );
2509
+ if (instance) {
2510
+ resource.instance = instance;
2511
+ markNodeAsHoistable(instance);
2512
+ return instance;
2513
+ }
2514
+
2515
+ const styleProps = styleTagPropsFromRawProps(props);
2516
+ const ownerDocument = getDocumentFromRoot(hoistableRoot);
2517
+ instance = ownerDocument.createElement('style');
2518
+
2519
+ markNodeAsHoistable(instance);
2520
+ setInitialProperties(instance, 'style', styleProps);
2521
+ insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
2522
+ resource.instance = instance;
2523
+
2524
+ return instance;
2525
+ }
2526
+ case 'stylesheet': {
2527
+ // This typing is enforce by `getResource`. If we change the logic
2528
+ // there for what qualifies as a stylesheet resource we need to ensure
2529
+ // this cast still makes sense;
2530
+ const qualifiedProps: StylesheetQualifyingProps = props;
2531
+ const key = getStyleKey(qualifiedProps.href);
2532
+
2533
+ // Attempt to hydrate instance from DOM
2534
+ let instance: null | Instance = hoistableRoot.querySelector(
2535
+ getStylesheetSelectorFromKey(key),
2536
+ );
2537
+ if (instance) {
2538
+ resource.instance = instance;
2539
+ markNodeAsHoistable(instance);
2540
+ return instance;
2541
+ }
2542
+
2543
+ const stylesheetProps = stylesheetPropsFromRawProps(props);
2544
+ const preloadProps = preloadPropsMap.get(key);
2545
+ if (preloadProps) {
2546
+ adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
2547
+ }
2548
+
2549
+ // Construct and insert a new instance
2550
+ const ownerDocument = getDocumentFromRoot(hoistableRoot);
2551
+ instance = ownerDocument.createElement('link');
2552
+ markNodeAsHoistable(instance);
2553
+ const linkInstance: HTMLLinkElement = (instance: any);
2554
+ (linkInstance: any)._p = new Promise((resolve, reject) => {
2555
+ linkInstance.onload = resolve;
2556
+ linkInstance.onerror = reject;
2557
+ });
2558
+ setInitialProperties(instance, 'link', stylesheetProps);
2559
+ insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
2560
+ resource.instance = instance;
2561
+
2562
+ return instance;
2563
+ }
2564
+ case 'script': {
2565
+ // This typing is enforce by `getResource`. If we change the logic
2566
+ // there for what qualifies as a stylesheet resource we need to ensure
2567
+ // this cast still makes sense;
2568
+ const borrowedScriptProps: ScriptProps = props;
2569
+ const key = getScriptKey(borrowedScriptProps.src);
2570
+
2571
+ // Attempt to hydrate instance from DOM
2572
+ let instance: null | Instance = hoistableRoot.querySelector(
2573
+ getScriptSelectorFromKey(key),
2574
+ );
2575
+ if (instance) {
2576
+ resource.instance = instance;
2577
+ markNodeAsHoistable(instance);
2578
+ return instance;
2579
+ }
2580
+
2581
+ let scriptProps = borrowedScriptProps;
2582
+ const preloadProps = preloadPropsMap.get(key);
2583
+ if (preloadProps) {
2584
+ scriptProps = {...borrowedScriptProps};
2585
+ adoptPreloadPropsForScript(scriptProps, preloadProps);
2586
+ }
2587
+
2588
+ // Construct and insert a new instance
2589
+ const ownerDocument = getDocumentFromRoot(hoistableRoot);
2590
+ instance = ownerDocument.createElement('script');
2591
+ markNodeAsHoistable(instance);
2592
+ setInitialProperties(instance, 'link', scriptProps);
2593
+ (ownerDocument.head: any).appendChild(instance);
2594
+ resource.instance = instance;
2595
+
2596
+ return instance;
2597
+ }
2598
+ case 'void': {
2599
+ return null;
2600
+ }
2601
+ default: {
2602
+ throw new Error(
2603
+ `acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
2604
+ );
2605
+ }
2606
+ }
2607
+ }
2608
+ return resource.instance;
2609
+}
2610
+
2611
+export function releaseResource(resource: Resource): void {
2612
+ resource.count--;
2613
+}
2614
+
2615
+function insertStylesheet(
2616
+ instance: HTMLElement,
2617
+ precedence: string,
2618
+ root: HoistableRoot,
2619
+): void {
2620
+ const nodes = root.querySelectorAll(
2621
+ 'link[rel="stylesheet"][data-precedence],style[data-precedence]',
2622
+ );
2623
+ const last = nodes.length ? nodes[nodes.length - 1] : null;
2624
+ let prior = last;
2625
+ for (let i = 0; i < nodes.length; i++) {
2626
+ const node = nodes[i];
2627
+ const nodePrecedence = node.dataset.precedence;
2628
+ if (nodePrecedence === precedence) {
2629
+ prior = node;
2630
+ } else if (prior !== last) {
2631
+ break;
2632
+ }
2633
+ }
2634
+
2635
+ if (prior) {
2636
+ // We get the prior from the document so we know it is in the tree.
2637
+ // We also know that links can't be the topmost Node so the parentNode
2638
+ // must exist.
2639
+ ((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
2640
+ } else {
2641
+ const parent =
2642
+ root.nodeType === DOCUMENT_NODE
2643
+ ? ((((root: any): Document).head: any): Element)
2644
+ : ((root: any): ShadowRoot);
2645
+ parent.insertBefore(instance, parent.firstChild);
2646
+ }
2647
+}
2648
+
2649
+function adoptPreloadPropsForStylesheet(
2650
+ stylesheetProps: StylesheetProps,
2651
+ preloadProps: PreloadProps,
2652
+): void {
2653
+ if (stylesheetProps.crossOrigin == null)
2654
+ stylesheetProps.crossOrigin = preloadProps.crossOrigin;
2655
+ if (stylesheetProps.referrerPolicy == null)
2656
+ stylesheetProps.referrerPolicy = preloadProps.referrerPolicy;
2657
+ if (stylesheetProps.title == null) stylesheetProps.title = preloadProps.title;
2658
+}
2659
+
2660
+function adoptPreloadPropsForScript(
2661
+ scriptProps: ScriptProps,
2662
+ preloadProps: PreloadProps,
2663
+): void {
2664
+ if (scriptProps.crossOrigin == null)
2665
+ scriptProps.crossOrigin = preloadProps.crossOrigin;
2666
+ if (scriptProps.referrerPolicy == null)
2667
+ scriptProps.referrerPolicy = preloadProps.referrerPolicy;
2668
+ if (scriptProps.integrity == null)
2669
+ scriptProps.referrerPolicy = preloadProps.integrity;
2670
+}
2671
+
2672
+type KeyedTagCache = Map<string, Array<Element>>;
2673
+type DocumentTagCaches = Map<Document, KeyedTagCache>;
2674
+let tagCaches: null | DocumentTagCaches = null;
2675
+
2676
+export function hydrateHoistable(
2677
+ hoistableRoot: HoistableRoot,
2678
+ type: HoistableTagType,
2679
+ props: any,
2680
+ internalInstanceHandle: Object,
2681
+): Instance {
2682
+ const ownerDocument = getDocumentFromRoot(hoistableRoot);
2683
+
2684
+ let instance: ?Instance = null;
2685
+ getInstance: switch (type) {
2686
+ case 'title': {
2687
+ instance = ownerDocument.getElementsByTagName('title')[0];
2688
+ if (
2689
+ !instance ||
2690
+ isOwnedInstance(instance) ||
2691
+ instance.namespaceURI === SVG_NAMESPACE ||
2692
+ instance.hasAttribute('itemprop')
2693
+ ) {
2694
+ instance = ownerDocument.createElement(type);
2695
+ (ownerDocument.head: any).insertBefore(
2696
+ instance,
2697
+ ownerDocument.querySelector('head > title'),
2698
+ );
2699
+ }
2700
+ setInitialProperties(instance, type, props);
2701
+ precacheFiberNode(internalInstanceHandle, instance);
2702
+ markNodeAsHoistable(instance);
2703
+ return instance;
2704
+ }
2705
+ case 'link': {
2706
+ const cache = getHydratableHoistableCache('link', 'href', ownerDocument);
2707
+ const key = type + (props.href || '');
2708
+ const maybeNodes = cache.get(key);
2709
+ if (maybeNodes) {
2710
+ const nodes = maybeNodes;
2711
+ for (let i = 0; i < nodes.length; i++) {
2712
+ const node = nodes[i];
2713
+ if (
2714
+ node.getAttribute('href') !==
2715
+ (props.href == null ? null : props.href) ||
2716
+ node.getAttribute('rel') !==
2717
+ (props.rel == null ? null : props.rel) ||
2718
+ node.getAttribute('title') !==
2719
+ (props.title == null ? null : props.title) ||
2720
+ node.getAttribute('crossorigin') !==
2721
+ (props.crossOrigin == null ? null : props.crossOrigin)
2722
+ ) {
2723
+ // mismatch, try the next node;
2724
+ continue;
2725
+ }
2726
+ instance = node;
2727
+ nodes.splice(i, 1);
2728
+ break getInstance;
2729
+ }
2730
+ }
2731
+ instance = ownerDocument.createElement(type);
2732
+ setInitialProperties(instance, type, props);
2733
+ (ownerDocument.head: any).appendChild(instance);
2734
+ break;
2735
+ }
2736
+ case 'meta': {
2737
+ const cache = getHydratableHoistableCache(
2738
+ 'meta',
2739
+ 'content',
2740
+ ownerDocument,
2741
+ );
2742
+ const key = type + (props.content || '');
2743
+ const maybeNodes = cache.get(key);
2744
+ if (maybeNodes) {
2745
+ const nodes = maybeNodes;
2746
+ for (let i = 0; i < nodes.length; i++) {
2747
+ const node = nodes[i];
2748
+
2749
+ // We coerce content to string because it is the most likely one to
2750
+ // use a `toString` capable value. For the rest we just do identity match
2751
+ // passing non-strings here is not really valid anyway.
2752
+ if (__DEV__) {
2753
+ checkAttributeStringCoercion(props.content, 'content');
2754
+ }
2755
+ if (
2756
+ node.getAttribute('content') !==
2757
+ (props.content == null ? null : '' + props.content) ||
2758
+ node.getAttribute('name') !==
2759
+ (props.name == null ? null : props.name) ||
2760
+ node.getAttribute('property') !==
2761
+ (props.property == null ? null : props.property) ||
2762
+ node.getAttribute('http-equiv') !==
2763
+ (props.httpEquiv == null ? null : props.httpEquiv) ||
2764
+ node.getAttribute('charset') !==
2765
+ (props.charSet == null ? null : props.charSet)
2766
+ ) {
2767
+ // mismatch, try the next node;
2768
+ continue;
2769
+ }
2770
+ instance = node;
2771
+ nodes.splice(i, 1);
2772
+ break getInstance;
2773
+ }
2774
+ }
2775
+ instance = ownerDocument.createElement(type);
2776
+ setInitialProperties(instance, type, props);
2777
+ (ownerDocument.head: any).appendChild(instance);
2778
+ break;
2779
+ }
2780
+ default:
2781
+ throw new Error(
2782
+ `getNodesForType encountered a type it did not expect: "${type}". This is a bug in React.`,
2783
+ );
2784
+ }
2785
+
2786
+ // This node is a match
2787
+ precacheFiberNode(internalInstanceHandle, instance);
2788
+ markNodeAsHoistable(instance);
2789
+ return instance;
2790
+}
2791
+
2792
+function getHydratableHoistableCache(
2793
+ type: HoistableTagType,
2794
+ keyAttribute: string,
2795
+ ownerDocument: Document,
2796
+): KeyedTagCache {
2797
+ let cache: KeyedTagCache;
2798
+ let caches: DocumentTagCaches;
2799
+ if (tagCaches === null) {
2800
+ cache = new Map();
2801
+ caches = tagCaches = new Map();
2802
+ caches.set(ownerDocument, cache);
2803
+ } else {
2804
+ caches = tagCaches;
2805
+ const maybeCache = caches.get(ownerDocument);
2806
+ if (!maybeCache) {
2807
+ cache = new Map();
2808
+ caches.set(ownerDocument, cache);
2809
+ } else {
2810
+ cache = maybeCache;
2811
+ }
2812
+ }
2813
+
2814
+ if (cache.has(type)) {
2815
+ // We use type as a special key that signals that this cache has been seeded for this type
2816
+ return cache;
2817
+ }
2818
+
2819
+ // Mark this cache as seeded for this type
2820
+ cache.set(type, (null: any));
2821
+
2822
+ const nodes = ownerDocument.getElementsByTagName(type);
2823
+ for (let i = 0; i < nodes.length; i++) {
2824
+ const node = nodes[i];
2825
+ if (
2826
+ !isOwnedInstance(node) &&
2827
+ (type !== 'link' || node.getAttribute('rel') !== 'stylesheet') &&
2828
+ node.namespaceURI !== SVG_NAMESPACE
2829
+ ) {
2830
+ const nodeKey = node.getAttribute(keyAttribute) || '';
2831
+ const key = type + nodeKey;
2832
+ const existing = cache.get(key);
2833
+ if (existing) {
2834
+ existing.push(node);
2835
+ } else {
2836
+ cache.set(key, [node]);
2837
+ }
2838
+ }
2839
+ }
2840
+
2841
+ return cache;
2842
+}
2843
+
2844
+export function mountHoistable(
2845
+ hoistableRoot: HoistableRoot,
2846
+ type: HoistableTagType,
2847
+ instance: Instance,
2848
+): void {
2849
+ const ownerDocument = getDocumentFromRoot(hoistableRoot);
2850
+ (ownerDocument.head: any).insertBefore(
2851
+ instance,
2852
+ type === 'title' ? ownerDocument.querySelector('head > title') : null,
2853
+ );
2854
+}
2855
+
2856
+export function unmountHoistable(instance: Instance): void {
2857
+ (instance.parentNode: any).removeChild(instance);
2858
+}
2859
+
2860
+// When passing user input into querySelector(All) the embedded string must not alter
2861
+// the semantics of the query. This escape function is safe to use when we know the
2862
+// provided value is going to be wrapped in double quotes as part of an attribute selector
2863
+// Do not use it anywhere else
2864
+// we escape double quotes and backslashes
2865
+const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\n\"\\]/g;
2866
+function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string {
2867
+ return value.replace(
2868
+ escapeSelectorAttributeValueInsideDoubleQuotesRegex,
2869
+ ch => '\\' + ch.charCodeAt(0).toString(16),
2870
+ );
2871
+}
2872
+
2873
+export function isHostHoistableType(
2874
+ type: string,
2875
+ props: RawProps,
2876
+ hostContext: HostContext,
2877
+): boolean {
2878
+ let outsideHostContainerContext: boolean;
2879
+ let namespace: HostContextProd;
2880
+ if (__DEV__) {
2881
+ const hostContextDev: HostContextDev = (hostContext: any);
2882
+ // We can only render resources when we are not within the host container context
2883
+ outsideHostContainerContext =
2884
+ !hostContextDev.ancestorInfo.containerTagInScope;
2885
+ namespace = hostContextDev.namespace;
2886
+ } else {
2887
+ const hostContextProd: HostContextProd = (hostContext: any);
2888
+ namespace = hostContextProd;
2889
+ }
2890
+
2891
+ // Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
2892
+ if (namespace === SVG_NAMESPACE || props.itemProp != null) {
2893
+ if (__DEV__) {
2894
+ if (
2895
+ outsideHostContainerContext &&
2896
+ props.itemProp != null &&
2897
+ (type === 'meta' ||
2898
+ type === 'title' ||
2899
+ type === 'style' ||
2900
+ type === 'link' ||
2901
+ type === 'script')
2902
+ ) {
2903
+ console.error(
2904
+ 'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
2905
+ ' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
2906
+ ' Otherwise, try moving this tag into the <head> or <body> of the Document.',
2907
+ type,
2908
+ type,
2909
+ );
2910
+ }
2911
+ }
2912
+ return false;
2913
+ }
2914
+
2915
+ switch (type) {
2916
+ case 'meta':
2917
+ case 'title': {
2918
+ return true;
2919
+ }
2920
+ case 'style': {
2921
+ if (
2922
+ typeof props.precedence !== 'string' ||
2923
+ typeof props.href !== 'string' ||
2924
+ props.href === ''
2925
+ ) {
2926
+ if (__DEV__) {
2927
+ if (outsideHostContainerContext) {
2928
+ console.error(
2929
+ 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key.' +
2930
+ ' React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that' +
2931
+ ' does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' +
2932
+ ' Note that hoisting <style> tags is considered an advanced feature that most will not use directly.' +
2933
+ ' Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style>' +
2934
+ ' to the <style> tag.',
2935
+ );
2936
+ }
2937
+ }
2938
+ return false;
2939
+ }
2940
+ return true;
2941
+ }
2942
+ case 'link': {
2943
+ if (
2944
+ typeof props.rel !== 'string' ||
2945
+ typeof props.href !== 'string' ||
2946
+ props.href === '' ||
2947
+ props.onLoad ||
2948
+ props.onError
2949
+ ) {
2950
+ if (__DEV__) {
2951
+ if (
2952
+ props.rel === 'stylesheet' &&
2953
+ typeof props.precedence === 'string'
2954
+ ) {
2955
+ validateLinkPropsForStyleResource(props);
2956
+ }
2957
+ if (outsideHostContainerContext) {
2958
+ if (
2959
+ typeof props.rel !== 'string' ||
2960
+ typeof props.href !== 'string' ||
2961
+ props.href === ''
2962
+ ) {
2963
+ console.error(
2964
+ 'Cannot render a <link> outside the main document without a `rel` and `href` prop.' +
2965
+ ' Try adding a `rel` and/or `href` prop to this <link> or moving the link into the <head> tag',
2966
+ );
2967
+ } else if (props.onError || props.onLoad) {
2968
+ console.error(
2969
+ 'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
2970
+ ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
2971
+ ' somewhere in the <body>.',
2972
+ );
2973
+ }
2974
+ }
2975
+ }
2976
+ return false;
2977
+ }
2978
+ switch (props.rel) {
2979
+ case 'stylesheet': {
2980
+ const {precedence, disabled} = props;
2981
+ if (__DEV__) {
2982
+ if (typeof precedence !== 'string') {
2983
+ if (outsideHostContainerContext) {
2984
+ console.error(
2985
+ 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
2986
+ ' Consider adding precedence="default" or moving it into the root <head> tag.',
2987
+ );
2988
+ }
2989
+ }
2990
+ }
2991
+ return typeof precedence === 'string' && disabled == null;
2992
+ }
2993
+ default: {
2994
+ return true;
2995
+ }
2996
+ }
2997
+ }
2998
+ case 'script': {
2999
+ if (
3000
+ props.async !== true ||
3001
+ props.onLoad ||
3002
+ props.onError ||
3003
+ typeof props.src !== 'string' ||
3004
+ !props.src
3005
+ ) {
3006
+ if (__DEV__) {
3007
+ if (outsideHostContainerContext) {
3008
+ if (props.async !== true) {
3009
+ console.error(
3010
+ 'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
3011
+ ' Try adding async="" or moving it into the root <head> tag.',
3012
+ );
3013
+ } else if (props.onLoad || props.onError) {
3014
+ console.error(
3015
+ 'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
3016
+ ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
3017
+ ' somewhere in the <body>.',
3018
+ );
3019
+ } else {
3020
+ console.error(
3021
+ 'Cannot render a <script> outside the main document without `async={true}` and a non-empty `src` prop.' +
3022
+ ' Ensure there is a valid `src` and either make the script async or move it into the root <head> tag or' +
3023
+ ' somewhere in the <body>.',
3024
+ );
3025
+ }
3026
+ }
3027
+ }
3028
+ return false;
3029
+ }
3030
+ return true;
3031
+ }
3032
+ case 'noscript':
3033
+ case 'template': {
3034
+ if (__DEV__) {
3035
+ if (outsideHostContainerContext) {
3036
+ console.error(
3037
+ 'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
3038
+ type,
3039
+ );
3040
+ }
3041
+ }
3042
+ return false;
3043
+ }
3044
+ }
3045
+ return false;
3046
+}
3047
+
3048
+export function maySuspendCommit(type: Type, props: Props): boolean {
3049
+ return false;
3050
+}
3051
+
3052
+export function mayResourceSuspendCommit(resource: Resource): boolean {
3053
+ return (
3054
+ resource.type === 'stylesheet' &&
3055
+ (resource.state.loading & Inserted) === NotLoaded
3056
+ );
3057
+}
3058
+
3059
+export function preloadInstance(type: Type, props: Props): boolean {
3060
+ // Return true to indicate it's already loaded
3061
+ return true;
3062
+}
3063
+
3064
+export function preloadResource(resource: Resource): boolean {
3065
+ if (
3066
+ resource.type === 'stylesheet' &&
3067
+ (resource.state.loading & Settled) === NotLoaded
3068
+ ) {
3069
+ // we have not finished loading the underlying stylesheet yet.
3070
+ return false;
3071
+ }
3072
+ // Return true to indicate it's already loaded
3073
+ return true;
3074
+}
3075
+
3076
+type SuspendedState = {
3077
+ stylesheets: null | Map<StylesheetResource, HoistableRoot>,
3078
+ count: number,
3079
+ unsuspend: null | (() => void),
3080
+};
3081
+let suspendedState: null | SuspendedState = null;
3082
+
3083
+// We use a noop function when we begin suspending because if possible we want the
3084
+// waitfor step to finish synchronously. If it doesn't we'll return a function to
3085
+// provide the actual unsuspend function and that will get completed when the count
3086
+// hits zero or it will get cancelled if the root starts new work.
3087
+function noop() {}
3088
+
3089
+export function startSuspendingCommit(): void {
3090
+ suspendedState = {
3091
+ stylesheets: null,
3092
+ count: 0,
3093
+ unsuspend: noop,
3094
+ };
3095
+}
3096
+
3097
+export function suspendInstance(type: Type, props: Props): void {
3098
+ return;
3099
+}
3100
+
3101
+export function suspendResource(
3102
+ hoistableRoot: HoistableRoot,
3103
+ resource: Resource,
3104
+ props: any,
3105
+): void {
3106
+ if (suspendedState === null) {
3107
+ throw new Error(
3108
+ 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
3109
+ );
3110
+ }
3111
+ const state = suspendedState;
3112
+ if (resource.type === 'stylesheet') {
3113
+ if (typeof props.media === 'string') {
3114
+ // If we don't currently match media we avoid suspending on this resource
3115
+ // and let it insert on the mutation path
3116
+ if (matchMedia(props.media).matches === false) {
3117
+ return;
3118
+ }
3119
+ }
3120
+ if (resource.instance === null) {
3121
+ const qualifiedProps: StylesheetQualifyingProps = props;
3122
+ const key = getStyleKey(qualifiedProps.href);
3123
+
3124
+ // Attempt to hydrate instance from DOM
3125
+ let instance: null | Instance = hoistableRoot.querySelector(
3126
+ getStylesheetSelectorFromKey(key),
3127
+ );
3128
+ if (instance) {
3129
+ // If this instance has a loading state it came from the Fizz runtime.
3130
+ // If there is not loading state it is assumed to have been server rendered
3131
+ // as part of the preamble and therefore synchronously loaded. It could have
3132
+ // errored however which we still do not yet have a means to detect. For now
3133
+ // we assume it is loaded.
3134
+ const maybeLoadingState: ?Promise<mixed> = (instance: any)._p;
3135
+ if (
3136
+ maybeLoadingState !== null &&
3137
+ typeof maybeLoadingState === 'object' &&
3138
+ // $FlowFixMe[method-unbinding]
3139
+ typeof maybeLoadingState.then === 'function'
3140
+ ) {
3141
+ const loadingState = maybeLoadingState;
3142
+ state.count++;
3143
+ const ping = onUnsuspend.bind(state);
3144
+ loadingState.then(ping, ping);
3145
+ }
3146
+ resource.state.loading |= Inserted;
3147
+ resource.instance = instance;
3148
+ markNodeAsHoistable(instance);
3149
+ return;
3150
+ }
3151
+
3152
+ const ownerDocument = getDocumentFromRoot(hoistableRoot);
3153
+
3154
+ const stylesheetProps = stylesheetPropsFromRawProps(props);
3155
+ const preloadProps = preloadPropsMap.get(key);
3156
+ if (preloadProps) {
3157
+ adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
3158
+ }
3159
+
3160
+ // Construct and insert a new instance
3161
+ instance = ownerDocument.createElement('link');
3162
+ markNodeAsHoistable(instance);
3163
+ const linkInstance: HTMLLinkElement = (instance: any);
3164
+ // This Promise is a loading state used by the Fizz runtime. We need this incase there is a race
3165
+ // between this resource being rendered on the client and being rendered with a late completed boundary.
3166
+ (linkInstance: any)._p = new Promise((resolve, reject) => {
3167
+ linkInstance.onload = resolve;
3168
+ linkInstance.onerror = reject;
3169
+ });
3170
+ setInitialProperties(instance, 'link', stylesheetProps);
3171
+ resource.instance = instance;
3172
+ }
3173
+
3174
+ if (state.stylesheets === null) {
3175
+ state.stylesheets = new Map();
3176
+ }
3177
+ state.stylesheets.set(resource, hoistableRoot);
3178
+
3179
+ const preloadEl = resource.state.preload;
3180
+ if (preloadEl && (resource.state.loading & Settled) === NotLoaded) {
3181
+ state.count++;
3182
+ const ping = onUnsuspend.bind(state);
3183
+ preloadEl.addEventListener('load', ping);
3184
+ preloadEl.addEventListener('error', ping);
3185
+ }
3186
+ }
3187
+}
3188
+
3189
+export function waitForCommitToBeReady(): null | (Function => Function) {
3190
+ if (suspendedState === null) {
3191
+ throw new Error(
3192
+ 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
3193
+ );
3194
+ }
3195
+
3196
+ const state = suspendedState;
3197
+
3198
+ if (state.stylesheets && state.count === 0) {
3199
+ // We are not currently blocked but we have not inserted all stylesheets.
3200
+ // If this insertion happens and loads or errors synchronously then we can
3201
+ // avoid suspending the commit. To do this we check the count again immediately after
3202
+ insertSuspendedStylesheets(state, state.stylesheets);
3203
+ }
3204
+
3205
+ // We need to check the count again because the inserted stylesheets may have led to new
3206
+ // tasks to wait on.
3207
+ if (state.count > 0) {
3208
+ return commit => {
3209
+ unsuspendAfterTimeout(state);
3210
+ state.unsuspend = commit;
3211
+
3212
+ return () => (state.unsuspend = null);
3213
+ };
3214
+ }
3215
+ return null;
3216
+}
3217
+
3218
+function unsuspendAfterTimeout(state: SuspendedState) {
3219
+ setTimeout(() => {
3220
+ if (state.stylesheets) {
3221
+ insertSuspendedStylesheets(state, state.stylesheets);
3222
+ }
3223
+ if (state.unsuspend) {
3224
+ const unsuspend = state.unsuspend;
3225
+ state.unsuspend = null;
3226
+ unsuspend();
3227
+ }
3228
+ }, 500);
3229
+}
3230
+
3231
+function onUnsuspend(this: SuspendedState) {
3232
+ this.count--;
3233
+ if (this.count === 0) {
3234
+ if (this.stylesheets) {
3235
+ // If we haven't actually inserted the stylesheets yet we need to do so now before starting the commit.
3236
+ // The reason we do this after everything else has finished is because we want to have all the stylesheets
3237
+ // load synchronously right before mutating. Ideally the new styles will cause a single recalc only on the
3238
+ // new tree. When we filled up stylesheets we only inlcuded stylesheets with matching media attributes so we
3239
+ // wait for them to load before actually continuing. We expect this to increase the count above zero
3240
+ insertSuspendedStylesheets(this, this.stylesheets);
3241
+ } else if (this.unsuspend) {
3242
+ const unsuspend = this.unsuspend;
3243
+ this.unsuspend = null;
3244
+ unsuspend();
3245
+ }
3246
+ }
3247
+}
3248
+
3249
+// This is typecast to non-null because it will always be set before read.
3250
+// it is important that this not be used except when the stack guarantees it exists.
3251
+// Currentlyt his is only during insertSuspendedStylesheet.
3252
+let precedencesByRoot: Map<HoistableRoot, Map<string, Instance>> = (null: any);
3253
+
3254
+function insertSuspendedStylesheets(
3255
+ state: SuspendedState,
3256
+ resources: Map<StylesheetResource, HoistableRoot>,
3257
+): void {
3258
+ // We need to clear this out so we don't try to reinsert after the stylesheets have loaded
3259
+ state.stylesheets = null;
3260
+
3261
+ if (state.unsuspend === null) {
3262
+ // The suspended commit was cancelled. We don't need to insert any stylesheets.
3263
+ return;
3264
+ }
3265
+
3266
+ // Temporarily increment count. we don't want any synchronously loaded stylesheets to try to unsuspend
3267
+ // before we finish inserting all stylesheets.
3268
+ state.count++;
3269
+
3270
+ precedencesByRoot = new Map();
3271
+ resources.forEach(insertStylesheetIntoRoot, state);
3272
+ precedencesByRoot = (null: any);
3273
+
3274
+ // We can remove our temporary count and if we're still at zero we can unsuspend.
3275
+ // If we are in the synchronous phase before deciding if the commit should suspend and this
3276
+ // ends up hitting the unsuspend path it will just invoke the noop unsuspend.
3277
+ onUnsuspend.call(state);
3278
+}
3279
+
3280
+function insertStylesheetIntoRoot(
3281
+ this: SuspendedState,
3282
+ root: HoistableRoot,
3283
+ resource: StylesheetResource,
3284
+ map: Map<StylesheetResource, HoistableRoot>,
3285
+) {
3286
+ if (resource.state.loading & Inserted) {
3287
+ // This resource was inserted by another root committing. we don't need to insert it again
3288
+ return;
3289
+ }
3290
+
3291
+ let last;
3292
+ let precedences = precedencesByRoot.get(root);
3293
+ if (!precedences) {
3294
+ precedences = new Map();
3295
+ precedencesByRoot.set(root, precedences);
3296
+ const nodes = root.querySelectorAll(
3297
+ 'link[data-precedence],style[data-precedence]',
3298
+ );
3299
+ for (let i = 0; i < nodes.length; i++) {
3300
+ const node = nodes[i];
3301
+ if (
3302
+ node.nodeName === 'link' ||
3303
+ // We omit style tags with media="not all" because they are not in the right position
3304
+ // and will be hoisted by the Fizz runtime imminently.
3305
+ node.getAttribute('media') !== 'not all'
3306
+ ) {
3307
+ precedences.set('p' + node.dataset.precedence, node);
3308
+ last = node;
3309
+ }
3310
+ }
3311
+ if (last) {
3312
+ precedences.set('last', last);
3313
+ }
3314
+ } else {
3315
+ last = precedences.get('last');
3316
+ }
3317
+
3318
+ // We only call this after we have constructed an instance so we assume it here
3319
+ const instance: HTMLLinkElement = (resource.instance: any);
3320
+ // We will always have a precedence for stylesheet instances
3321
+ const precedence: string = (instance.getAttribute('data-precedence'): any);
3322
+
3323
+ const prior = precedences.get('p' + precedence) || last;
3324
+ if (prior === last) {
3325
+ precedences.set('last', instance);
3326
+ }
3327
+ precedences.set(precedence, instance);
3328
+
3329
+ this.count++;
3330
+ const onComplete = onUnsuspend.bind(this);
3331
+ instance.addEventListener('load', onComplete);
3332
+ instance.addEventListener('error', onComplete);
3333
+
3334
+ if (prior) {
3335
+ (prior.parentNode: any).insertBefore(instance, prior.nextSibling);
3336
+ } else {
3337
+ const parent =
3338
+ root.nodeType === DOCUMENT_NODE
3339
+ ? ((((root: any): Document).head: any): Element)
3340
+ : ((root: any): ShadowRoot);
3341
+ parent.insertBefore(instance, parent.firstChild);
3342
+ }
3343
+ resource.state.loading |= Inserted;
3344
+}