@dr.pogodin/react-helmet
Version:
Thread-safe Helmet for React 19+ and friends
211 lines (205 loc) • 7.34 kB
JavaScript
import { c as _c } from "react/compiler-runtime";
import { Children, use, useEffect, useId } from 'react';
import { Context } from './Provider';
import { REACT_TAG_MAP, TAG_NAMES, VALID_TAG_NAMES } from './constants';
import { cloneProps, mergeProps, pushToPropArray } from './utils';
function assertChildType(childType, nestedChildren) {
if (typeof childType !== 'string') {
throw Error('You may be attempting to nest <Helmet> components within each other, which is not allowed. Refer to our API for more information.');
}
if (!VALID_TAG_NAMES.includes(childType)) {
throw Error(`Only elements types ${VALID_TAG_NAMES.join(', ')} are allowed. Helmet does not support rendering <${childType}> elements. Refer to our API for more information.`);
}
if (!nestedChildren || typeof nestedChildren === 'string' || Array.isArray(nestedChildren)
// TODO: This piece of the check is wrong when parent is a fragment,
// and thus children may not be an array of strings.
// && nestedChildren.every((item) => typeof item === 'string')
) return;
throw Error(`Helmet expects a string as a child of <${childType}>. Did you forget to wrap your children in braces? ( <${childType}>{\`\`}</${childType}> ) Refer to our API for more information.`);
}
/**
* Given a string key, it checks it against the legacy mapping between supported
* HTML attribute names and their corresponding React prop names (for the names
* that are different). If found in the mapping, it prints a warning to console
* and returns the mapped prop name. Otherwise, it just returns the key as is,
* assuming it is already a valid React prop name.
*/
function getPropName(key) {
const res = REACT_TAG_MAP[key];
if (res) {
// eslint-disable-next-line no-console
console.warn(`"${key}" is not a valid JSX prop, replace it by "${res}"`);
}
return res ?? key;
}
/**
* Given children and props of a <Helmet> component, it reduces them to a single
* props object.
*
* TODO: I guess, it should be further refactored, to make it cleaner...
* though, it should perfectly work as is, so not a huge priority for now.
*/
function reduceChildrenAndProps(props) {
// NOTE: `props` are clonned, thus it is safe to push additional items to
// array values of `res`, and to re-assign non-array values of `res`, without
// the risk to mutate the original `props` object.
const res = cloneProps(props);
// TODO: This is a temporary block, for compatibility with legacy library.
for (const item of Object.values(props)) {
if (Array.isArray(item)) {
for (const it of item) {
// TODO: This condition is actually needed to prevent some test failures,
// I guess, something is messed up with related types?
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (it) {
for (const key of Object.keys(it)) {
const p = getPropName(key);
if (p !== key) {
it[p] = it[key];
delete it[key];
}
}
}
}
} else if (item && typeof item === 'object') {
const it = item;
for (const key of Object.keys(it)) {
const p = getPropName(key);
if (p !== key) {
it[p] = it[key];
delete it[key];
}
}
}
}
// eslint-disable-next-line complexity
Children.forEach(props.children, child => {
if (child === undefined || child === null) return;
if (typeof child !== 'object' || !('props' in child)) {
throw Error(`"${typeof child}" is not a valid <Helmet> descendant`);
}
let nestedChildren;
const childProps = {};
if (child.props) {
for (const [key, value] of Object.entries(child.props)) {
if (key === 'children') nestedChildren = value;else childProps[getPropName(key)] = value;
}
}
let {
type
} = child;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
if (typeof type === 'symbol') type = type.toString();
assertChildType(type, nestedChildren);
function assertStringChild(child2) {
if (typeof child2 !== 'string') {
// TODO: We want to throw, but the legacy code did not, so we won't for
// now.
// eslint-disable-next-line no-console
console.error(`child of ${type} element should be a string`);
/*
throw Error(
// NOTE: assertChildType() above guarantees that `type` is a string,
// although it is not expressed in a way TypeScript can automatically
// pick up.
);
*/
}
}
switch (type) {
case TAG_NAMES.BASE:
res.base = childProps;
break;
case TAG_NAMES.BODY:
res.bodyAttributes = childProps;
break;
case TAG_NAMES.FRAGMENT:
mergeProps(res, reduceChildrenAndProps({
children: nestedChildren
}));
break;
case TAG_NAMES.HTML:
res.htmlAttributes = childProps;
break;
case TAG_NAMES.LINK:
case TAG_NAMES.META:
if (nestedChildren) {
throw Error(`<${type} /> elements are self-closing and can not contain children. Refer to our API for more information.`);
}
pushToPropArray(res, type, childProps);
break;
case TAG_NAMES.NOSCRIPT:
case TAG_NAMES.SCRIPT:
if (nestedChildren !== undefined) {
assertStringChild(nestedChildren);
childProps.innerHTML = nestedChildren;
}
pushToPropArray(res, type, childProps);
break;
case TAG_NAMES.STYLE:
assertStringChild(nestedChildren);
childProps.cssText = nestedChildren;
pushToPropArray(res, type, childProps);
break;
case TAG_NAMES.TITLE:
res.titleAttributes = childProps;
if (typeof nestedChildren === 'string') res.title = nestedChildren;
// When title contains {} expressions the children are an array of
// strings, and other values.
else if (Array.isArray(nestedChildren)) res.title = nestedChildren.join('');
break;
case TAG_NAMES.HEAD:
default:
{
// TODO: Perhaps, we should remove HEAD entry from TAG_NAMES?
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const bad = type;
}
}
});
delete res.children;
return res;
}
const Helmet = props => {
const $ = _c(8);
const context = use(Context);
if (!context) {
throw Error("<Helmet> component must be within a <HelmetProvider> children tree");
}
const id = useId();
context.update(id, reduceChildrenAndProps(props));
let t0;
if ($[0] !== context || $[1] !== id || $[2] !== props) {
t0 = () => {
context.update(id, reduceChildrenAndProps(props));
context.clientApply();
};
$[0] = context;
$[1] = id;
$[2] = props;
$[3] = t0;
} else {
t0 = $[3];
}
useEffect(t0);
let t1;
let t2;
if ($[4] !== context || $[5] !== id) {
t1 = () => () => {
context.update(id, undefined);
context.clientApply();
};
t2 = [context, id];
$[4] = context;
$[5] = id;
$[6] = t1;
$[7] = t2;
} else {
t1 = $[6];
t2 = $[7];
}
useEffect(t1, t2);
return null;
};
export default Helmet;
//# sourceMappingURL=Helmet.js.map