react-helmet-async
Advanced tools
| import React, { Component } from 'react'; | ||
| import type { HelmetProps } from './types'; | ||
| interface React19DispatcherProps extends HelmetProps { | ||
| /** | ||
| * The processed props including mapped children. These come from Helmet's | ||
| * mapChildrenToProps or the raw API props. | ||
| */ | ||
| [key: string]: any; | ||
| } | ||
| /** | ||
| * React 19+ Dispatcher: Instead of manual DOM manipulation, this component | ||
| * renders actual JSX elements. React 19 automatically hoists <title>, <meta>, | ||
| * <link>, <style>, and <script async> to <head>. | ||
| * | ||
| * For htmlAttributes and bodyAttributes, we still apply via direct DOM | ||
| * manipulation since React 19 doesn't handle those. | ||
| */ | ||
| export default class React19Dispatcher extends Component<React19DispatcherProps> { | ||
| componentDidMount(): void; | ||
| componentDidUpdate(): void; | ||
| componentWillUnmount(): void; | ||
| resolveTitle(): string | undefined; | ||
| renderTitle(): React.DetailedReactHTMLElement<{ | ||
| [key: string]: any; | ||
| }, HTMLElement> | null; | ||
| renderBase(): React.DetailedReactHTMLElement<{ | ||
| [key: string]: any; | ||
| }, HTMLElement> | null; | ||
| renderMeta(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null; | ||
| renderLink(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null; | ||
| renderScript(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null; | ||
| renderStyle(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null; | ||
| renderNoscript(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null; | ||
| render(): React.FunctionComponentElement<{ | ||
| children?: React.ReactNode; | ||
| }>; | ||
| } | ||
| export {}; |
| export declare const isReact19: boolean; |
| import { Component } from 'react'; | ||
| import type { HelmetServerState } from './types'; | ||
| export interface DispatcherContextProp { | ||
| setHelmet: (newState: HelmetServerState) => void; | ||
| setHelmet: (newState: HelmetServerState | null) => void; | ||
| helmetInstances: { | ||
@@ -21,4 +21,4 @@ get: () => HelmetDispatcher[]; | ||
| init(): void; | ||
| render(): any; | ||
| render(): null; | ||
| } | ||
| export {}; |
@@ -9,13 +9,13 @@ import type HelmetDispatcher from './Dispatcher'; | ||
| interface HelmetDataContext { | ||
| helmet: HelmetServerState; | ||
| helmet: HelmetServerState | null; | ||
| } | ||
| export declare const isDocument: boolean; | ||
| export default class HelmetData implements HelmetDataType { | ||
| instances: any[]; | ||
| instances: never[]; | ||
| canUseDOM: boolean; | ||
| context: HelmetDataContext; | ||
| value: { | ||
| setHelmet: (serverState: HelmetServerState) => void; | ||
| setHelmet: (serverState: HelmetServerState | null) => void; | ||
| helmetInstances: { | ||
| get: () => any[]; | ||
| get: () => HelmetDispatcher[]; | ||
| add: (instance: HelmetDispatcher) => void; | ||
@@ -22,0 +22,0 @@ remove: (instance: HelmetDispatcher) => void; |
+8
-25
@@ -1,10 +0,7 @@ | ||
| import type { PropsWithChildren, ReactElement, ReactNode } from 'react'; | ||
| import type { PropsWithChildren } from 'react'; | ||
| import React, { Component } from 'react'; | ||
| import type { HelmetProps } from './types'; | ||
| export * from './types'; | ||
| export type { Attributes, BodyProps, HelmetDatum, HelmetHTMLBodyDatum, HelmetHTMLElementDatum, HelmetProps, HelmetServerState, HelmetTags, HtmlProps, LinkProps, MetaProps, StateUpdate, TagList, TitleProps, } from './types'; | ||
| export { default as HelmetData } from './HelmetData'; | ||
| export { default as HelmetProvider } from './Provider'; | ||
| type Props = { | ||
| [key: string]: any; | ||
| }; | ||
| export declare class Helmet extends Component<PropsWithChildren<HelmetProps>> { | ||
@@ -17,23 +14,9 @@ static defaultProps: { | ||
| shouldComponentUpdate(nextProps: HelmetProps): boolean; | ||
| mapNestedChildrenToProps(child: ReactElement, nestedChildren: ReactNode): { | ||
| innerHTML: string | number | true | ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal; | ||
| cssText?: undefined; | ||
| } | { | ||
| cssText: string | number | true | ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal; | ||
| innerHTML?: undefined; | ||
| }; | ||
| flattenArrayTypeChildren(child: JSX.Element, arrayTypeChildren: { | ||
| [key: string]: JSX.Element[]; | ||
| }, newChildProps: Props, nestedChildren: ReactNode): {}; | ||
| mapObjectTypeChildren(child: JSX.Element, newProps: Props, newChildProps: Props, nestedChildren: ReactNode): {}; | ||
| mapArrayTypeChildrenToProps(arrayTypeChildren: { | ||
| [key: string]: JSX.Element; | ||
| }, newProps: Props): { | ||
| [x: string]: any; | ||
| }; | ||
| warnOnInvalidChildren(child: JSX.Element, nestedChildren: ReactNode): boolean; | ||
| mapChildrenToProps(children: ReactNode, newProps: Props): { | ||
| [x: string]: any; | ||
| }; | ||
| private mapNestedChildrenToProps; | ||
| private flattenArrayTypeChildren; | ||
| private mapObjectTypeChildren; | ||
| private mapArrayTypeChildrenToProps; | ||
| private warnOnInvalidChildren; | ||
| private mapChildrenToProps; | ||
| render(): React.JSX.Element; | ||
| } |
+202
-17
| // src/index.tsx | ||
| import React3, { Component as Component3 } from "react"; | ||
| import React5, { Component as Component4 } from "react"; | ||
| import fastCompare from "react-fast-compare"; | ||
@@ -7,3 +7,3 @@ import invariant from "invariant"; | ||
| // src/Provider.tsx | ||
| import React2, { Component } from "react"; | ||
| import React3, { Component } from "react"; | ||
@@ -376,4 +376,3 @@ // src/server.ts | ||
| let priorityMethods = { | ||
| toComponent: () => { | ||
| }, | ||
| toComponent: () => [], | ||
| toString: () => "" | ||
@@ -442,5 +441,10 @@ }; | ||
| // src/reactVersion.ts | ||
| import React2 from "react"; | ||
| var major = parseInt(React2.version.split(".")[0], 10); | ||
| var isReact19 = major >= 19; | ||
| // src/Provider.tsx | ||
| var defaultValue = {}; | ||
| var Context = React2.createContext(defaultValue); | ||
| var Context = React3.createContext(defaultValue); | ||
| var HelmetProvider = class _HelmetProvider extends Component { | ||
@@ -451,6 +455,13 @@ static canUseDOM = isDocument; | ||
| super(props); | ||
| this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM); | ||
| if (isReact19) { | ||
| this.helmetData = null; | ||
| } else { | ||
| this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM); | ||
| } | ||
| } | ||
| render() { | ||
| return /* @__PURE__ */ React2.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children); | ||
| if (isReact19) { | ||
| return /* @__PURE__ */ React3.createElement(React3.Fragment, null, this.props.children); | ||
| } | ||
| return /* @__PURE__ */ React3.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children); | ||
| } | ||
@@ -478,7 +489,4 @@ }; | ||
| } else if (attribute === "cssText" /* CSS_TEXT */) { | ||
| if (newElement.styleSheet) { | ||
| newElement.styleSheet.cssText = tag.cssText; | ||
| } else { | ||
| newElement.appendChild(document.createTextNode(tag.cssText)); | ||
| } | ||
| const cssText = tag.cssText; | ||
| newElement.appendChild(document.createTextNode(cssText)); | ||
| } else { | ||
@@ -624,4 +632,3 @@ const attr = attribute; | ||
| helmetInstances.get().map((instance) => { | ||
| const props = { ...instance.props }; | ||
| delete props.context; | ||
| const { context: _context, ...props } = instance.props; | ||
| return props; | ||
@@ -655,4 +662,179 @@ }) | ||
| // src/React19Dispatcher.tsx | ||
| import React4, { Component as Component3 } from "react"; | ||
| var react19Instances = []; | ||
| var toHtmlAttributes = (props) => { | ||
| const result = {}; | ||
| for (const key of Object.keys(props)) { | ||
| result[HTML_TAG_MAP[key] || key] = props[key]; | ||
| } | ||
| return result; | ||
| }; | ||
| var toReactProps = (attrs) => { | ||
| const result = {}; | ||
| for (const key of Object.keys(attrs)) { | ||
| const mapped = REACT_TAG_MAP[key]; | ||
| result[mapped || key] = attrs[key]; | ||
| } | ||
| return result; | ||
| }; | ||
| var applyAttributes = (tagName, attributes) => { | ||
| if (!isDocument) | ||
| return; | ||
| const el = document.getElementsByTagName(tagName)[0]; | ||
| if (!el) | ||
| return; | ||
| const managedAttr = "data-rh-managed"; | ||
| const prev = el.getAttribute(managedAttr); | ||
| const prevKeys = prev ? prev.split(",") : []; | ||
| const nextKeys = Object.keys(attributes); | ||
| for (const key of prevKeys) { | ||
| if (!nextKeys.includes(key)) { | ||
| el.removeAttribute(key); | ||
| } | ||
| } | ||
| for (const key of nextKeys) { | ||
| const value = attributes[key]; | ||
| if (value === void 0 || value === null || value === false) { | ||
| el.removeAttribute(key); | ||
| } else if (value === true) { | ||
| el.setAttribute(key, ""); | ||
| } else { | ||
| el.setAttribute(key, String(value)); | ||
| } | ||
| } | ||
| if (nextKeys.length > 0) { | ||
| el.setAttribute(managedAttr, nextKeys.join(",")); | ||
| } else { | ||
| el.removeAttribute(managedAttr); | ||
| } | ||
| }; | ||
| var syncAllAttributes = () => { | ||
| const htmlAttrs = {}; | ||
| const bodyAttrs = {}; | ||
| for (const instance of react19Instances) { | ||
| const { htmlAttributes, bodyAttributes } = instance.props; | ||
| if (htmlAttributes) { | ||
| Object.assign(htmlAttrs, toHtmlAttributes(htmlAttributes)); | ||
| } | ||
| if (bodyAttributes) { | ||
| Object.assign(bodyAttrs, toHtmlAttributes(bodyAttributes)); | ||
| } | ||
| } | ||
| applyAttributes("html" /* HTML */, htmlAttrs); | ||
| applyAttributes("body" /* BODY */, bodyAttrs); | ||
| }; | ||
| var React19Dispatcher = class extends Component3 { | ||
| componentDidMount() { | ||
| react19Instances.push(this); | ||
| syncAllAttributes(); | ||
| } | ||
| componentDidUpdate() { | ||
| syncAllAttributes(); | ||
| } | ||
| componentWillUnmount() { | ||
| const index = react19Instances.indexOf(this); | ||
| if (index !== -1) { | ||
| react19Instances.splice(index, 1); | ||
| } | ||
| syncAllAttributes(); | ||
| } | ||
| resolveTitle() { | ||
| const { title, titleTemplate, defaultTitle } = this.props; | ||
| if (title && titleTemplate) { | ||
| return titleTemplate.replace(/%s/g, () => Array.isArray(title) ? title.join("") : title); | ||
| } | ||
| return title || defaultTitle || void 0; | ||
| } | ||
| renderTitle() { | ||
| const title = this.resolveTitle(); | ||
| if (title === void 0) | ||
| return null; | ||
| const titleAttributes = this.props.titleAttributes || {}; | ||
| return React4.createElement("title" /* TITLE */, toReactProps(titleAttributes), title); | ||
| } | ||
| renderBase() { | ||
| const { base } = this.props; | ||
| if (!base) | ||
| return null; | ||
| return React4.createElement("base" /* BASE */, toReactProps(base)); | ||
| } | ||
| renderMeta() { | ||
| const { meta } = this.props; | ||
| if (!meta || !Array.isArray(meta)) | ||
| return null; | ||
| return meta.map( | ||
| (attrs, i) => React4.createElement("meta" /* META */, { | ||
| key: i, | ||
| ...toReactProps(attrs) | ||
| }) | ||
| ); | ||
| } | ||
| renderLink() { | ||
| const { link } = this.props; | ||
| if (!link || !Array.isArray(link)) | ||
| return null; | ||
| return link.map( | ||
| (attrs, i) => React4.createElement("link" /* LINK */, { | ||
| key: i, | ||
| ...toReactProps(attrs) | ||
| }) | ||
| ); | ||
| } | ||
| renderScript() { | ||
| const { script } = this.props; | ||
| if (!script || !Array.isArray(script)) | ||
| return null; | ||
| return script.map((attrs, i) => { | ||
| const { innerHTML, ...rest } = attrs; | ||
| const props = toReactProps(rest); | ||
| if (innerHTML) { | ||
| props.dangerouslySetInnerHTML = { __html: innerHTML }; | ||
| } | ||
| return React4.createElement("script" /* SCRIPT */, { key: i, ...props }); | ||
| }); | ||
| } | ||
| renderStyle() { | ||
| const { style } = this.props; | ||
| if (!style || !Array.isArray(style)) | ||
| return null; | ||
| return style.map((attrs, i) => { | ||
| const { cssText, ...rest } = attrs; | ||
| const props = toReactProps(rest); | ||
| if (cssText) { | ||
| props.dangerouslySetInnerHTML = { __html: cssText }; | ||
| } | ||
| return React4.createElement("style" /* STYLE */, { key: i, ...props }); | ||
| }); | ||
| } | ||
| renderNoscript() { | ||
| const { noscript } = this.props; | ||
| if (!noscript || !Array.isArray(noscript)) | ||
| return null; | ||
| return noscript.map((attrs, i) => { | ||
| const { innerHTML, ...rest } = attrs; | ||
| const props = toReactProps(rest); | ||
| if (innerHTML) { | ||
| props.dangerouslySetInnerHTML = { __html: innerHTML }; | ||
| } | ||
| return React4.createElement("noscript" /* NOSCRIPT */, { key: i, ...props }); | ||
| }); | ||
| } | ||
| render() { | ||
| return React4.createElement( | ||
| React4.Fragment, | ||
| null, | ||
| this.renderTitle(), | ||
| this.renderBase(), | ||
| this.renderMeta(), | ||
| this.renderLink(), | ||
| this.renderScript(), | ||
| this.renderStyle(), | ||
| this.renderNoscript() | ||
| ); | ||
| } | ||
| }; | ||
| // src/index.tsx | ||
| var Helmet = class extends Component3 { | ||
| var Helmet = class extends Component4 { | ||
| static defaultProps = { | ||
@@ -748,3 +930,3 @@ defer: true, | ||
| let arrayTypeChildren = {}; | ||
| React3.Children.forEach(children, (child) => { | ||
| React5.Children.forEach(children, (child) => { | ||
| if (!child || !child.props) { | ||
@@ -799,3 +981,6 @@ return; | ||
| } | ||
| return helmetData ? /* @__PURE__ */ React3.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ React3.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ React3.createElement(HelmetDispatcher, { ...newProps, context })); | ||
| if (isReact19) { | ||
| return /* @__PURE__ */ React5.createElement(React19Dispatcher, { ...newProps }); | ||
| } | ||
| return helmetData ? /* @__PURE__ */ React5.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ React5.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ React5.createElement(HelmetDispatcher, { ...newProps, context })); | ||
| } | ||
@@ -802,0 +987,0 @@ }; |
+205
-20
@@ -38,3 +38,3 @@ "use strict"; | ||
| module.exports = __toCommonJS(src_exports); | ||
| var import_react4 = __toESM(require("react")); | ||
| var import_react6 = __toESM(require("react")); | ||
| var import_react_fast_compare = __toESM(require("react-fast-compare")); | ||
@@ -44,3 +44,3 @@ var import_invariant = __toESM(require("invariant")); | ||
| // src/Provider.tsx | ||
| var import_react2 = __toESM(require("react")); | ||
| var import_react3 = __toESM(require("react")); | ||
@@ -413,4 +413,3 @@ // src/server.ts | ||
| let priorityMethods = { | ||
| toComponent: () => { | ||
| }, | ||
| toComponent: () => [], | ||
| toString: () => "" | ||
@@ -479,6 +478,11 @@ }; | ||
| // src/reactVersion.ts | ||
| var import_react2 = __toESM(require("react")); | ||
| var major = parseInt(import_react2.default.version.split(".")[0], 10); | ||
| var isReact19 = major >= 19; | ||
| // src/Provider.tsx | ||
| var defaultValue = {}; | ||
| var Context = import_react2.default.createContext(defaultValue); | ||
| var HelmetProvider = class _HelmetProvider extends import_react2.Component { | ||
| var Context = import_react3.default.createContext(defaultValue); | ||
| var HelmetProvider = class _HelmetProvider extends import_react3.Component { | ||
| static canUseDOM = isDocument; | ||
@@ -488,6 +492,13 @@ helmetData; | ||
| super(props); | ||
| this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM); | ||
| if (isReact19) { | ||
| this.helmetData = null; | ||
| } else { | ||
| this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM); | ||
| } | ||
| } | ||
| render() { | ||
| return /* @__PURE__ */ import_react2.default.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children); | ||
| if (isReact19) { | ||
| return /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, this.props.children); | ||
| } | ||
| return /* @__PURE__ */ import_react3.default.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children); | ||
| } | ||
@@ -497,3 +508,3 @@ }; | ||
| // src/Dispatcher.tsx | ||
| var import_react3 = require("react"); | ||
| var import_react4 = require("react"); | ||
| var import_shallowequal = __toESM(require("shallowequal")); | ||
@@ -516,7 +527,4 @@ | ||
| } else if (attribute === "cssText" /* CSS_TEXT */) { | ||
| if (newElement.styleSheet) { | ||
| newElement.styleSheet.cssText = tag.cssText; | ||
| } else { | ||
| newElement.appendChild(document.createTextNode(tag.cssText)); | ||
| } | ||
| const cssText = tag.cssText; | ||
| newElement.appendChild(document.createTextNode(cssText)); | ||
| } else { | ||
@@ -644,3 +652,3 @@ const attr = attribute; | ||
| // src/Dispatcher.tsx | ||
| var HelmetDispatcher = class extends import_react3.Component { | ||
| var HelmetDispatcher = class extends import_react4.Component { | ||
| rendered = false; | ||
@@ -663,4 +671,3 @@ shouldComponentUpdate(nextProps) { | ||
| helmetInstances.get().map((instance) => { | ||
| const props = { ...instance.props }; | ||
| delete props.context; | ||
| const { context: _context, ...props } = instance.props; | ||
| return props; | ||
@@ -694,4 +701,179 @@ }) | ||
| // src/React19Dispatcher.tsx | ||
| var import_react5 = __toESM(require("react")); | ||
| var react19Instances = []; | ||
| var toHtmlAttributes = (props) => { | ||
| const result = {}; | ||
| for (const key of Object.keys(props)) { | ||
| result[HTML_TAG_MAP[key] || key] = props[key]; | ||
| } | ||
| return result; | ||
| }; | ||
| var toReactProps = (attrs) => { | ||
| const result = {}; | ||
| for (const key of Object.keys(attrs)) { | ||
| const mapped = REACT_TAG_MAP[key]; | ||
| result[mapped || key] = attrs[key]; | ||
| } | ||
| return result; | ||
| }; | ||
| var applyAttributes = (tagName, attributes) => { | ||
| if (!isDocument) | ||
| return; | ||
| const el = document.getElementsByTagName(tagName)[0]; | ||
| if (!el) | ||
| return; | ||
| const managedAttr = "data-rh-managed"; | ||
| const prev = el.getAttribute(managedAttr); | ||
| const prevKeys = prev ? prev.split(",") : []; | ||
| const nextKeys = Object.keys(attributes); | ||
| for (const key of prevKeys) { | ||
| if (!nextKeys.includes(key)) { | ||
| el.removeAttribute(key); | ||
| } | ||
| } | ||
| for (const key of nextKeys) { | ||
| const value = attributes[key]; | ||
| if (value === void 0 || value === null || value === false) { | ||
| el.removeAttribute(key); | ||
| } else if (value === true) { | ||
| el.setAttribute(key, ""); | ||
| } else { | ||
| el.setAttribute(key, String(value)); | ||
| } | ||
| } | ||
| if (nextKeys.length > 0) { | ||
| el.setAttribute(managedAttr, nextKeys.join(",")); | ||
| } else { | ||
| el.removeAttribute(managedAttr); | ||
| } | ||
| }; | ||
| var syncAllAttributes = () => { | ||
| const htmlAttrs = {}; | ||
| const bodyAttrs = {}; | ||
| for (const instance of react19Instances) { | ||
| const { htmlAttributes, bodyAttributes } = instance.props; | ||
| if (htmlAttributes) { | ||
| Object.assign(htmlAttrs, toHtmlAttributes(htmlAttributes)); | ||
| } | ||
| if (bodyAttributes) { | ||
| Object.assign(bodyAttrs, toHtmlAttributes(bodyAttributes)); | ||
| } | ||
| } | ||
| applyAttributes("html" /* HTML */, htmlAttrs); | ||
| applyAttributes("body" /* BODY */, bodyAttrs); | ||
| }; | ||
| var React19Dispatcher = class extends import_react5.Component { | ||
| componentDidMount() { | ||
| react19Instances.push(this); | ||
| syncAllAttributes(); | ||
| } | ||
| componentDidUpdate() { | ||
| syncAllAttributes(); | ||
| } | ||
| componentWillUnmount() { | ||
| const index = react19Instances.indexOf(this); | ||
| if (index !== -1) { | ||
| react19Instances.splice(index, 1); | ||
| } | ||
| syncAllAttributes(); | ||
| } | ||
| resolveTitle() { | ||
| const { title, titleTemplate, defaultTitle } = this.props; | ||
| if (title && titleTemplate) { | ||
| return titleTemplate.replace(/%s/g, () => Array.isArray(title) ? title.join("") : title); | ||
| } | ||
| return title || defaultTitle || void 0; | ||
| } | ||
| renderTitle() { | ||
| const title = this.resolveTitle(); | ||
| if (title === void 0) | ||
| return null; | ||
| const titleAttributes = this.props.titleAttributes || {}; | ||
| return import_react5.default.createElement("title" /* TITLE */, toReactProps(titleAttributes), title); | ||
| } | ||
| renderBase() { | ||
| const { base } = this.props; | ||
| if (!base) | ||
| return null; | ||
| return import_react5.default.createElement("base" /* BASE */, toReactProps(base)); | ||
| } | ||
| renderMeta() { | ||
| const { meta } = this.props; | ||
| if (!meta || !Array.isArray(meta)) | ||
| return null; | ||
| return meta.map( | ||
| (attrs, i) => import_react5.default.createElement("meta" /* META */, { | ||
| key: i, | ||
| ...toReactProps(attrs) | ||
| }) | ||
| ); | ||
| } | ||
| renderLink() { | ||
| const { link } = this.props; | ||
| if (!link || !Array.isArray(link)) | ||
| return null; | ||
| return link.map( | ||
| (attrs, i) => import_react5.default.createElement("link" /* LINK */, { | ||
| key: i, | ||
| ...toReactProps(attrs) | ||
| }) | ||
| ); | ||
| } | ||
| renderScript() { | ||
| const { script } = this.props; | ||
| if (!script || !Array.isArray(script)) | ||
| return null; | ||
| return script.map((attrs, i) => { | ||
| const { innerHTML, ...rest } = attrs; | ||
| const props = toReactProps(rest); | ||
| if (innerHTML) { | ||
| props.dangerouslySetInnerHTML = { __html: innerHTML }; | ||
| } | ||
| return import_react5.default.createElement("script" /* SCRIPT */, { key: i, ...props }); | ||
| }); | ||
| } | ||
| renderStyle() { | ||
| const { style } = this.props; | ||
| if (!style || !Array.isArray(style)) | ||
| return null; | ||
| return style.map((attrs, i) => { | ||
| const { cssText, ...rest } = attrs; | ||
| const props = toReactProps(rest); | ||
| if (cssText) { | ||
| props.dangerouslySetInnerHTML = { __html: cssText }; | ||
| } | ||
| return import_react5.default.createElement("style" /* STYLE */, { key: i, ...props }); | ||
| }); | ||
| } | ||
| renderNoscript() { | ||
| const { noscript } = this.props; | ||
| if (!noscript || !Array.isArray(noscript)) | ||
| return null; | ||
| return noscript.map((attrs, i) => { | ||
| const { innerHTML, ...rest } = attrs; | ||
| const props = toReactProps(rest); | ||
| if (innerHTML) { | ||
| props.dangerouslySetInnerHTML = { __html: innerHTML }; | ||
| } | ||
| return import_react5.default.createElement("noscript" /* NOSCRIPT */, { key: i, ...props }); | ||
| }); | ||
| } | ||
| render() { | ||
| return import_react5.default.createElement( | ||
| import_react5.default.Fragment, | ||
| null, | ||
| this.renderTitle(), | ||
| this.renderBase(), | ||
| this.renderMeta(), | ||
| this.renderLink(), | ||
| this.renderScript(), | ||
| this.renderStyle(), | ||
| this.renderNoscript() | ||
| ); | ||
| } | ||
| }; | ||
| // src/index.tsx | ||
| var Helmet = class extends import_react4.Component { | ||
| var Helmet = class extends import_react6.Component { | ||
| static defaultProps = { | ||
@@ -787,3 +969,3 @@ defer: true, | ||
| let arrayTypeChildren = {}; | ||
| import_react4.default.Children.forEach(children, (child) => { | ||
| import_react6.default.Children.forEach(children, (child) => { | ||
| if (!child || !child.props) { | ||
@@ -838,4 +1020,7 @@ return; | ||
| } | ||
| return helmetData ? /* @__PURE__ */ import_react4.default.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ import_react4.default.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ import_react4.default.createElement(HelmetDispatcher, { ...newProps, context })); | ||
| if (isReact19) { | ||
| return /* @__PURE__ */ import_react6.default.createElement(React19Dispatcher, { ...newProps }); | ||
| } | ||
| return helmetData ? /* @__PURE__ */ import_react6.default.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ import_react6.default.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ import_react6.default.createElement(HelmetDispatcher, { ...newProps, context })); | ||
| } | ||
| }; |
@@ -8,3 +8,3 @@ import type { PropsWithChildren } from 'react'; | ||
| context?: { | ||
| helmet?: HelmetServerState; | ||
| helmet?: HelmetServerState | null; | ||
| }; | ||
@@ -14,3 +14,3 @@ } | ||
| static canUseDOM: boolean; | ||
| helmetData: HelmetData; | ||
| helmetData: HelmetData | null; | ||
| constructor(props: PropsWithChildren<ProviderProps>); | ||
@@ -17,0 +17,0 @@ render(): React.JSX.Element; |
+2
-1
@@ -0,5 +1,6 @@ | ||
| import React from 'react'; | ||
| import type { MappedServerState } from './types'; | ||
| declare const mapStateOnServer: (props: MappedServerState) => { | ||
| priority: { | ||
| toComponent: () => void; | ||
| toComponent: () => React.ReactElement[]; | ||
| toString: () => string; | ||
@@ -6,0 +7,0 @@ }; |
+1
-2
@@ -28,3 +28,3 @@ import type { HTMLAttributes, JSX } from 'react'; | ||
| toString(): string; | ||
| toComponent(): React.Component<any>; | ||
| toComponent(): React.ReactElement[]; | ||
| } | ||
@@ -49,3 +49,2 @@ export interface HelmetHTMLBodyDatum { | ||
| title: HelmetDatum; | ||
| titleAttributes: HelmetDatum; | ||
| priority: HelmetDatum; | ||
@@ -52,0 +51,0 @@ } |
+29
-10
| { | ||
| "name": "react-helmet-async", | ||
| "version": "2.0.5", | ||
| "description": "Thread-safe Helmet for React 16+ and friends", | ||
| "version": "3.0.0", | ||
| "description": "Thread-safe Helmet for React 16–18, with native support for React 19+", | ||
| "sideEffects": false, | ||
| "main": "./lib/index.js", | ||
| "module": "./lib/index.esm.js", | ||
| "typings": "./lib/index.d.ts", | ||
| "types": "./lib/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./lib/index.d.ts", | ||
| "default": "./lib/index.esm.js" | ||
| }, | ||
| "require": { | ||
| "types": "./lib/index.d.ts", | ||
| "default": "./lib/index.js" | ||
| } | ||
| } | ||
| }, | ||
| "repository": "http://github.com/staylor/react-helmet-async", | ||
@@ -23,2 +35,3 @@ "author": "Scott Taylor <scott.c.taylor@mac.com>", | ||
| "@commitlint/config-conventional": "18.4.3", | ||
| "@playwright/test": "^1.58.2", | ||
| "@remix-run/eslint-config": "2.3.1", | ||
@@ -31,2 +44,3 @@ "@testing-library/jest-dom": "6.1.5", | ||
| "@types/react": "18.2.39", | ||
| "@types/react-dom": "^18.3.7", | ||
| "@types/shallowequal": "1.1.5", | ||
@@ -40,2 +54,3 @@ "@vitejs/plugin-react": "4.2.0", | ||
| "jsdom": "22.1.0", | ||
| "playwright": "^1.58.2", | ||
| "prettier": "3.1.0", | ||
@@ -52,3 +67,3 @@ "raf": "3.4.1", | ||
| "peerDependencies": { | ||
| "react": "^16.6.0 || ^17.0.0 || ^18.0.0" | ||
| "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" | ||
| }, | ||
@@ -58,10 +73,14 @@ "scripts": { | ||
| "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint --report-unused-disable-directives .", | ||
| "lint-fix": "yarn lint --fix", | ||
| "lint-fix": "pnpm lint --fix", | ||
| "test": "vitest run", | ||
| "test-watch": "yarn test --watch", | ||
| "test-update": "yarn test -u", | ||
| "compile": "yarn run clean && NODE_ENV=production tsx build.ts && yarn types", | ||
| "prepare": "yarn compile && husky install", | ||
| "types": "tsc src/index.tsx --jsx react --declaration --esModuleInterop --allowJs --emitDeclarationOnly --outDir lib" | ||
| "test:e2e:server": "vitest run --config e2e/vitest.config.ts", | ||
| "test:e2e:browser": "playwright test --config e2e/playwright.config.ts", | ||
| "test:e2e": "pnpm run test:e2e:server && pnpm run test:e2e:browser", | ||
| "test:all": "pnpm test && pnpm run test:e2e", | ||
| "test-watch": "pnpm test -- --watch", | ||
| "test-update": "pnpm test -- -u", | ||
| "compile": "pnpm run clean && NODE_ENV=production tsx build.ts && pnpm run types", | ||
| "prepare": "pnpm run compile && husky install", | ||
| "types": "tsc --project tsconfig.build.json" | ||
| } | ||
| } |
+44
-8
| # react-helmet-async | ||
| [](https://circleci.com/gh/staylor/react-helmet-async) | ||
| [](https://github.com/staylor/react-helmet-async/actions/workflows/ci.yml) | ||
@@ -12,5 +12,16 @@ [Announcement post on Times Open blog](https://open.nytimes.com/the-future-of-meta-tag-management-for-modern-react-development-ec26a7dc9183) | ||
| ## React 19 | ||
| React 19 has built-in support for hoisting `<title>`, `<meta>`, `<link>`, `<style>`, and `<script>` elements to `<head>`. Starting with version 3.0.0, this package detects the React version at runtime: | ||
| - **React 19+**: `<Helmet>` renders actual DOM elements and lets React handle hoisting them to `<head>`. `<HelmetProvider>` becomes a transparent passthrough. The existing API is fully compatible — you do not need to change any code. | ||
| - **React 16–18**: The existing behavior is preserved. `<Helmet>` collects all instances, deduplicates tags, and applies changes to the DOM via manual manipulation (client) or serializes them for the response (server). | ||
| > **Note:** `htmlAttributes` and `bodyAttributes` do not have a React 19 equivalent, so they are still applied via direct DOM manipulation on both code paths. | ||
| If you are starting a new React 19 project and do not need `htmlAttributes`/`bodyAttributes`, SSR `context` serialization, `onChangeClientState`, `prioritizeSeoTags`, or `titleTemplate` support, you may not need this package at all — React 19's built-in metadata handling may be sufficient. | ||
| ## Usage | ||
| **New is 1.0.0:** No more default export! `import { Helmet } from 'react-helmet-async'` | ||
| **New in 1.0.0:** No more default export! `import { Helmet } from 'react-helmet-async'` | ||
@@ -21,3 +32,3 @@ The main way that this package differs from `react-helmet` is that it requires using a Provider to encapsulate Helmet state for your React tree. If you use libraries like Redux or Apollo, you are already familiar with this paradigm: | ||
| import React from 'react'; | ||
| import ReactDOM from 'react-dom'; | ||
| import { createRoot } from 'react-dom/client'; | ||
| import { Helmet, HelmetProvider } from 'react-helmet-async'; | ||
@@ -37,6 +48,3 @@ | ||
| ReactDOM.hydrate( | ||
| app, | ||
| document.getElementById(‘app’) | ||
| ); | ||
| createRoot(document.getElementById('app')).render(app); | ||
| ``` | ||
@@ -74,2 +82,4 @@ | ||
| > **React 19 SSR note:** When using React 19, `<title>`, `<meta>`, and `<link>` tags rendered inside `<Helmet>` are included directly in the React render output and hoisted to `<head>` by React itself. The `context` object will not be populated with helmet state on React 19. If you rely on the `context` for server rendering, you can render these tags directly in your component tree instead and let React 19 handle them natively. | ||
| ## Streams | ||
@@ -124,2 +134,4 @@ | ||
| > **React 19:** React 19's `renderToReadableStream` natively handles `<title>`, `<meta>`, and `<link>` hoisting during streaming, so the manual context extraction shown above is not necessary. | ||
| ## Usage in Jest | ||
@@ -134,2 +146,4 @@ While testing in using jest, if there is a need to emulate SSR, the following string is required to have the test behave the way they are expected to. | ||
| > This is only relevant for React 16–18. On React 19, `HelmetProvider` is a passthrough and `canUseDOM` has no effect. | ||
| ## Prioritizing tags for SEO | ||
@@ -182,2 +196,4 @@ | ||
| > **React 19:** The `prioritizeSeoTags` flag has no effect on React 19, since tags are rendered as regular JSX elements and their order in `<head>` is determined by React's rendering order. | ||
| ## Usage without Context | ||
@@ -190,3 +206,3 @@ You can optionally use `<Helmet>` outside a context by manually creating a stateful `HelmetData` instance, and passing that stateful object to each `<Helmet>` instance: | ||
| import { renderToString } from 'react-dom/server'; | ||
| import { Helmet, HelmetProvider, HelmetData } from 'react-helmet-async'; | ||
| import { Helmet, HelmetData } from 'react-helmet-async'; | ||
@@ -210,4 +226,24 @@ const helmetData = new HelmetData({}); | ||
| > **React 19:** The `helmetData` prop is ignored on React 19, since `<Helmet>` renders elements directly without the need for external state management. | ||
| ## Compatibility | ||
| | React Version | Behavior | | ||
| |---|---| | ||
| | 16.6+ | Full support via `HelmetProvider` context and manual DOM updates | | ||
| | 17.x | Full support via `HelmetProvider` context and manual DOM updates | | ||
| | 18.x | Full support via `HelmetProvider` context and manual DOM updates | | ||
| | 19.x+ | Renders native JSX elements; React handles `<head>` hoisting | | ||
| ## Development | ||
| ```bash | ||
| pnpm install | ||
| pnpm test # unit tests | ||
| pnpm run test:e2e # server + browser e2e tests | ||
| pnpm run test:all # everything | ||
| ``` | ||
| ## License | ||
| Licensed under the Apache 2.0 License, Copyright © 2018 Scott Taylor |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
101600
17.7%16
14.29%2320
20.08%241
17.56%29
11.54%