/**
* Copyright (c) Meta Platforms, Inc. and 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 hasOwnProperty from 'shared/hasOwnProperty';
type Props = {[string]: mixed};
export function warnOnMissingHrefAndRel(
pendingProps: Props,
currentProps: ?Props,
) {
if (__DEV__) {
if (currentProps != null) {
const originalResourceName =
typeof currentProps.href === 'string'
? `Resource with href "${currentProps.href}"`
: 'Resource';
const originalRelStatement = getValueDescriptorExpectingEnumForWarning(
currentProps.rel,
);
const pendingRel = getValueDescriptorExpectingEnumForWarning(
pendingProps.rel,
);
const pendingHref = getValueDescriptorExpectingEnumForWarning(
pendingProps.href,
);
if (typeof pendingProps.rel !== 'string') {
console.error(
'A previously rendered as a %s with rel "%s" but was updated with an invalid rel: %s. When a link' +
' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the anymore.',
originalResourceName,
originalRelStatement,
pendingRel,
);
} else if (typeof pendingProps.href !== 'string') {
console.error(
'A previously rendered as a %s but was updated with an invalid href prop: %s. When a link' +
' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the anymore.',
originalResourceName,
pendingHref,
);
}
} else {
const pendingRel = getValueDescriptorExpectingEnumForWarning(
pendingProps.rel,
);
const pendingHref = getValueDescriptorExpectingEnumForWarning(
pendingProps.href,
);
if (typeof pendingProps.rel !== 'string') {
console.error(
'A is rendering with an invalid rel: %s. When a link' +
' does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the anymore.',
pendingRel,
);
} else if (typeof pendingProps.href !== 'string') {
console.error(
'A is rendering with an invalid href: %s. When a link' +
' does not have a valid href prop it is not represented in the DOM. If this is intentional, instead' +
' do not render the anymore.',
pendingHref,
);
}
}
}
}
export function validatePreloadResourceDifference(
originalProps: any,
originalImplicit: boolean,
latestProps: any,
latestImplicit: boolean,
) {
if (__DEV__) {
const {href} = originalProps;
const originalWarningName = getResourceNameForWarning(
'preload',
originalProps,
originalImplicit,
);
const latestWarningName = getResourceNameForWarning(
'preload',
latestProps,
latestImplicit,
);
if (latestProps.as !== originalProps.as) {
console.error(
'A %s is using the same href "%s" as a %s. This is always an error and React will only keep the first preload' +
' for any given href, discarding subsequent instances. To fix, find where you are using this href in link' +
' tags or in calls to ReactDOM.preload() or ReactDOM.preinit() and either make the Resource types agree or' +
' update the hrefs to be distinct for different Resource types.',
latestWarningName,
href,
originalWarningName,
);
} else {
let missingProps = null;
let extraProps = null;
let differentProps = null;
if (originalProps.media != null && latestProps.media == null) {
missingProps = missingProps || {};
missingProps.media = originalProps.media;
}
for (const propName in latestProps) {
const propValue = latestProps[propName];
const originalValue = originalProps[propName];
if (propValue != null && propValue !== originalValue) {
if (originalValue == null) {
extraProps = extraProps || {};
extraProps[propName] = propValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: originalValue,
latest: propValue,
};
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
}
export function validateStyleResourceDifference(
originalProps: any,
latestProps: any,
) {
if (__DEV__) {
const {href} = originalProps;
// eslint-disable-next-line no-labels
const originalWarningName = getResourceNameForWarning(
'style',
originalProps,
false,
);
const latestWarningName = getResourceNameForWarning(
'style',
latestProps,
false,
);
let missingProps = null;
let extraProps = null;
let differentProps = null;
if (originalProps.media != null && latestProps.media == null) {
missingProps = missingProps || {};
missingProps.media = originalProps.media;
}
for (let propName in latestProps) {
const propValue = latestProps[propName];
const originalValue = originalProps[propName];
if (propValue != null && propValue !== originalValue) {
propName = propName === 'data-precedence' ? 'precedence' : propName;
if (originalValue == null) {
extraProps = extraProps || {};
extraProps[propName] = propValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: originalValue,
latest: propValue,
};
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
export function validateScriptResourceDifference(
originalProps: any,
latestProps: any,
) {
if (__DEV__) {
const {src} = originalProps;
// eslint-disable-next-line no-labels
const originalWarningName = getResourceNameForWarning(
'script',
originalProps,
false,
);
const latestWarningName = getResourceNameForWarning(
'script',
latestProps,
false,
);
let extraProps = null;
let differentProps = null;
for (const propName in latestProps) {
const propValue = latestProps[propName];
const originalValue = originalProps[propName];
if (propValue != null && propValue !== originalValue) {
if (originalValue == null) {
extraProps = extraProps || {};
extraProps[propName] = propValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: originalValue,
latest: propValue,
};
}
}
}
if (extraProps || differentProps) {
warnDifferentProps(
src,
'src',
originalWarningName,
latestWarningName,
extraProps,
null,
differentProps,
);
}
}
}
export function validateStyleAndHintProps(
preloadProps: any,
styleProps: any,
implicitPreload: boolean,
) {
if (__DEV__) {
const {href} = preloadProps;
const originalWarningName = getResourceNameForWarning(
'preload',
preloadProps,
implicitPreload,
);
const latestWarningName = getResourceNameForWarning(
'style',
styleProps,
false,
);
if (preloadProps.as !== 'style') {
console.error(
'While creating a %s for href "%s" a %s for this same href was found. When preloading a stylesheet the' +
' "as" prop must be of type "style". This most likely ocurred by rendering a preload link with an incorrect' +
' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
latestWarningName,
href,
originalWarningName,
);
}
let missingProps = null;
let extraProps = null;
let differentProps = null;
for (const propName in styleProps) {
const styleValue = styleProps[propName];
const preloadValue = preloadProps[propName];
switch (propName) {
// Check for difference on specific props that cross over or influence
// the relationship between the preload and stylesheet
case 'crossOrigin':
case 'referrerPolicy':
case 'media':
case 'title': {
if (
preloadValue !== styleValue &&
!(preloadValue == null && styleValue == null)
) {
if (styleValue == null) {
missingProps = missingProps || {};
missingProps[propName] = preloadValue;
} else if (preloadValue == null) {
extraProps = extraProps || {};
extraProps[propName] = styleValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: preloadValue,
latest: styleValue,
};
}
}
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
export function validateScriptAndHintProps(
preloadProps: any,
scriptProps: any,
implicitPreload: boolean,
) {
if (__DEV__) {
const {href} = preloadProps;
const originalWarningName = getResourceNameForWarning(
'preload',
preloadProps,
implicitPreload,
);
const latestWarningName = getResourceNameForWarning(
'script',
scriptProps,
false,
);
if (preloadProps.as !== 'script') {
console.error(
'While creating a %s for href "%s" a %s for this same url was found. When preloading a script the' +
' "as" prop must be of type "script". This most likely ocurred by rendering a preload link with an incorrect' +
' "as" prop or by calling ReactDOM.preload with an incorrect "as" option.',
latestWarningName,
href,
originalWarningName,
);
}
let missingProps = null;
let extraProps = null;
let differentProps = null;
for (const propName in scriptProps) {
const scriptValue = scriptProps[propName];
const preloadValue = preloadProps[propName];
switch (propName) {
// Check for difference on specific props that cross over or influence
// the relationship between the preload and stylesheet
case 'crossOrigin':
case 'referrerPolicy':
case 'integrity': {
if (
preloadValue !== scriptValue &&
!(preloadValue == null && scriptValue == null)
) {
if (scriptValue == null) {
missingProps = missingProps || {};
missingProps[propName] = preloadValue;
} else if (preloadValue == null) {
extraProps = extraProps || {};
extraProps[propName] = scriptValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: preloadValue,
latest: scriptValue,
};
}
}
}
}
}
if (missingProps || extraProps || differentProps) {
warnDifferentProps(
href,
'href',
originalWarningName,
latestWarningName,
extraProps,
missingProps,
differentProps,
);
}
}
}
function warnDifferentProps(
url: string,
urlPropKey: string,
originalName: string,
latestName: string,
extraProps: ?{[string]: any},
missingProps: ?{[string]: any},
differentProps: ?{[string]: {original: any, latest: any}},
): void {
if (__DEV__) {
const juxtaposedNameStatement =
latestName === originalName
? 'an earlier instance of this Resource'
: `a ${originalName} with the same ${urlPropKey}`;
let comparisonStatement = '';
if (missingProps !== null && typeof missingProps === 'object') {
for (const propName in missingProps) {
comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
}
}
if (extraProps !== null && typeof extraProps === 'object') {
for (const propName in extraProps) {
comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
}
}
if (differentProps !== null && typeof differentProps === 'object') {
for (const propName in differentProps) {
comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
}
}
console.error(
'A %s with %s "%s" has props that disagree with those found on %s. Resources always use the props' +
' that were provided the first time they are encountered so any differences will be ignored. Please' +
' update Resources that share an %s to have props that agree. The differences are described below.%s',
latestName,
urlPropKey,
url,
juxtaposedNameStatement,
urlPropKey,
comparisonStatement,
);
}
}
function getResourceNameForWarning(
type: string,
props: Object,
implicit: boolean,
) {
if (__DEV__) {
switch (type) {
case 'style': {
return 'style Resource';
}
case 'script': {
return 'script Resource';
}
case 'preload': {
if (implicit) {
return `preload for a ${props.as} Resource`;
}
return `preload Resource (as "${props.as}")`;
}
}
}
return 'Resource';
}
export function validateURLKeyedUpdatedProps(
pendingProps: Props,
currentProps: Props,
resourceType: 'style' | 'script' | 'href',
urlPropKey: 'href' | 'src',
): boolean {
if (__DEV__) {
// This function should never be called if we don't have /srcs so we don't bother considering
// Whether they are null or undefined
if (pendingProps[urlPropKey] === currentProps[urlPropKey]) {
// If we have the same href/src we need all other props to be the same
let missingProps;
let extraProps;
let differentProps;
const allProps = Array.from(
new Set(Object.keys(currentProps).concat(Object.keys(pendingProps))),
);
for (let i = 0; i < allProps.length; i++) {
const propName = allProps[i];
const pendingValue = pendingProps[propName];
const currentValue = currentProps[propName];
if (
pendingValue !== currentValue &&
!(pendingValue == null && currentValue == null)
) {
if (pendingValue == null) {
missingProps = missingProps || {};
missingProps[propName] = currentValue;
} else if (currentValue == null) {
extraProps = extraProps || {};
extraProps[propName] = pendingValue;
} else {
differentProps = differentProps || {};
differentProps[propName] = {
original: currentValue,
latest: pendingValue,
};
}
}
}
if (missingProps || extraProps || differentProps) {
const latestWarningName = getResourceNameForWarning(
resourceType,
currentProps,
false,
);
let comparisonStatement = '';
if (missingProps !== null && typeof missingProps === 'object') {
for (const propName in missingProps) {
comparisonStatement += `\n ${propName}: missing or null in latest props, "${missingProps[propName]}" in original props`;
}
}
if (extraProps !== null && typeof extraProps === 'object') {
for (const propName in extraProps) {
comparisonStatement += `\n ${propName}: "${extraProps[propName]}" in latest props, missing or null in original props`;
}
}
if (differentProps !== null && typeof differentProps === 'object') {
for (const propName in differentProps) {
comparisonStatement += `\n ${propName}: "${differentProps[propName].latest}" in latest props, "${differentProps[propName].original}" in original props`;
}
}
console.error(
'A %s with %s "%s" recieved new props with different values from the props used' +
' when this Resource was first rendered. React will only use the props provided when' +
' this resource was first rendered until a new %s is provided. Unlike conventional' +
' DOM elements, Resources instances do not have a one to one correspondence with Elements' +
' in the DOM and as such, every instance of a Resource for a single Resource identifier' +
' (%s) must have props that agree with each other. The differences are described below.%s',
latestWarningName,
urlPropKey,
currentProps[urlPropKey],
urlPropKey,
urlPropKey,
comparisonStatement,
);
return true;
}
}
}
return false;
}
export function validateLinkPropsForStyleResource(props: Props): boolean {
if (__DEV__) {
// This should only be called when we know we are opting into Resource semantics (i.e. precedence is not null)
const {href, onLoad, onError, disabled} = props;
const allProps = ['onLoad', 'onError', 'disabled'];
const includedProps = [];
if (onLoad) includedProps.push('onLoad');
if (onError) includedProps.push('onError');
if (disabled != null) includedProps.push('disabled');
const allPropsUnionPhrase = propNamesListJoin(allProps, 'or');
let includedPropsPhrase = propNamesListJoin(includedProps, 'and');
includedPropsPhrase += includedProps.length === 1 ? ' prop' : ' props';
if (includedProps.length) {
console.error(
'A link (rel="stylesheet") element with href "%s" has the precedence prop but also included the %s.' +
' When using %s React will opt out of Resource behavior. If you meant for this' +
' element to be treated as a Resource remove the %s. Otherwise remove the precedence prop.',
href,
includedPropsPhrase,
allPropsUnionPhrase,
includedPropsPhrase,
);
return true;
}
}
return false;
}
function propNamesListJoin(
list: Array,
combinator: 'and' | 'or',
): string {
switch (list.length) {
case 0:
return '';
case 1:
return list[0];
case 2:
return list[0] + ' ' + combinator + ' ' + list[1];
default:
return (
list.slice(0, -1).join(', ') +
', ' +
combinator +
' ' +
list[list.length - 1]
);
}
}
export function validateLinkPropsForPreloadResource(linkProps: any) {
if (__DEV__) {
const {href, as} = linkProps;
if (as === 'font') {
const name = getResourceNameForWarning('preload', linkProps, false);
if (!hasOwnProperty.call(linkProps, 'crossOrigin')) {
console.error(
'A %s with href "%s" did not specify the crossOrigin prop. Font preloads must always use' +
' anonymouse CORS mode. To fix add an empty string, "anonymous", or any other string' +
' value except "use-credentials" for the crossOrigin prop of all font preloads.',
name,
href,
);
} else if (linkProps.crossOrigin === 'use-credentials') {
console.error(
'A %s with href "%s" specified a crossOrigin value of "use-credentials". Font preloads must always use' +
' anonymouse CORS mode. To fix use an empty string, "anonymous", or any other string' +
' value except "use-credentials" for the crossOrigin prop of all font preloads.',
name,
href,
);
}
}
}
}
export function validatePreloadArguments(href: mixed, options: mixed) {
if (__DEV__) {
if (!href || typeof href !== 'string') {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
console.error(
'ReactDOM.preload() expected the first argument to be a string representing an href but found %s instead.',
typeOfArg,
);
} else if (typeof options !== 'object' || options === null) {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
console.error(
'ReactDOM.preload() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
typeOfArg,
href,
);
} else {
const as = options.as;
switch (as) {
// Font specific validation of options
case 'font': {
if (options.crossOrigin === 'use-credentials') {
console.error(
'ReactDOM.preload() was called with an "as" type of "font" and with a "crossOrigin" option of "use-credentials".' +
' Fonts preloading must use crossOrigin "anonymous" to be functional. Please update your font preload to omit' +
' the crossOrigin option or change it to any other value than "use-credentials" (Browsers default all other values' +
' to anonymous mode). The href for the preload call where this warning originated is "%s"',
href,
);
}
break;
}
case 'script':
case 'style': {
break;
}
// We have an invalid as type and need to warn
default: {
const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
console.error(
'ReactDOM.preload() expected a valid "as" type in the options (second) argument but found %s instead.' +
' Please use one of the following valid values instead: %s. The href for the preload call where this' +
' warning originated is "%s".',
typeOfAs,
'"style", "font", or "script"',
href,
);
}
}
}
}
}
export function validatePreinitArguments(href: mixed, options: mixed) {
if (__DEV__) {
if (!href || typeof href !== 'string') {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(href);
console.error(
'ReactDOM.preinit() expected the first argument to be a string representing an href but found %s instead.',
typeOfArg,
);
} else if (typeof options !== 'object' || options === null) {
const typeOfArg = getValueDescriptorExpectingObjectForWarning(options);
console.error(
'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. The href for the preload call where this warning originated is "%s".',
typeOfArg,
href,
);
} else {
const as = options.as;
switch (as) {
case 'style':
case 'script': {
break;
}
// We have an invalid as type and need to warn
default: {
const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
console.error(
'ReactDOM.preinit() expected the second argument to be an options argument containing at least an "as" property' +
' specifying the Resource type. It found %s instead. Currently, valid resource types for for preinit are "style"' +
' and "script". The href for the preinit call where this warning originated is "%s".',
typeOfAs,
href,
);
}
}
}
}
}
function getValueDescriptorExpectingObjectForWarning(thing: any): string {
return thing === null
? 'null'
: thing === undefined
? 'undefined'
: thing === ''
? 'an empty string'
: `something with type "${typeof thing}"`;
}
function getValueDescriptorExpectingEnumForWarning(thing: any): string {
return thing === null
? 'null'
: thing === undefined
? 'undefined'
: thing === ''
? 'an empty string'
: typeof thing === 'string'
? JSON.stringify(thing)
: `something with type "${typeof thing}"`;
}