UNPKG

@dr.pogodin/react-helmet

Version:

Thread-safe Helmet for React 19+ and friends

211 lines (205 loc) 7.34 kB
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