@lomray/react-head-manager 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +1 -0
- package/index.js +1 -1
- package/manager.d.ts +25 -7
- package/manager.js +1 -1
- package/manager.js.map +1 -1
- package/package.json +21 -16
- package/tag-status.d.ts +6 -0
- package/tag-status.js +2 -0
- package/tag-status.js.map +1 -0
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export{Meta,MetaManagerProvider,useMetaManager}from"./context.js";export{default as Manager}from"./manager.js";
|
|
1
|
+
export{Meta,MetaManagerProvider,useMetaManager}from"./context.js";export{default as TagStatus}from"./tag-status.js";export{default as Manager}from"./manager.js";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/manager.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { ReactElement, ReactNode } from 'react';
|
|
3
|
-
|
|
4
|
-
init = "init",
|
|
5
|
-
synced = "synced",
|
|
6
|
-
drain = "drain"
|
|
7
|
-
}
|
|
3
|
+
import TagStatus from "./tag-status.js";
|
|
8
4
|
interface IMetaManagerTags {
|
|
9
|
-
html: Map<string,
|
|
10
|
-
|
|
5
|
+
html: Map<string, {
|
|
6
|
+
props: Record<string, any>;
|
|
7
|
+
order: number;
|
|
8
|
+
}>;
|
|
9
|
+
body: Map<string, {
|
|
10
|
+
props: Record<string, any>;
|
|
11
|
+
order: number;
|
|
12
|
+
}>;
|
|
11
13
|
meta: Map<string, {
|
|
12
14
|
element?: ReactElement;
|
|
13
15
|
domElement?: HTMLElement;
|
|
@@ -74,6 +76,15 @@ declare class Manager {
|
|
|
74
76
|
* Remove children, dangerouslySetInnerHTML etc.
|
|
75
77
|
*/
|
|
76
78
|
static cleanupElementProps(props: Record<string, any>): Record<string, any>;
|
|
79
|
+
/**
|
|
80
|
+
* Sort elements or props
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* Sort elements or props
|
|
84
|
+
*/
|
|
85
|
+
protected sortTags<T extends Map<string, {
|
|
86
|
+
order: number;
|
|
87
|
+
}>>(elements: T): T;
|
|
77
88
|
/**
|
|
78
89
|
* Get meta tags
|
|
79
90
|
*/
|
|
@@ -126,6 +137,13 @@ declare class Manager {
|
|
|
126
137
|
element: ReactElement;
|
|
127
138
|
elementProps: Record<string, any>;
|
|
128
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* Get element order
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* Get element order
|
|
145
|
+
*/
|
|
146
|
+
protected getElementOrder(elementProps: Record<string, any>, type: string, key?: string): number;
|
|
129
147
|
/**
|
|
130
148
|
* Push react elements to meta state
|
|
131
149
|
*/
|
package/manager.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"html-react-parser";import t,{Children as s}from"react";
|
|
1
|
+
import e from"html-react-parser";import t,{Fragment as r,Children as s}from"react";import i from"./tag-status.js";class n{static rootContainerId="root";isServer="undefined"==typeof window;tags;tagsDefinitions={"meta[charset]":{order:10},"meta[name='viewport']":{order:20},title:{order:100},base:{order:110},meta:{order:200},link:{order:300},script:{order:400},noscript:{order:500},style:{order:600}};reservedAttributes={order:"data-order"};syncTimerId=null;constructor(e={}){this.tags={html:new Map(e?.html),body:new Map(e?.body),meta:new Map(e?.meta),containers:new Set(e?.containers)}}setTagsDefinitions(e){this.tagsDefinitions={...this.tagsDefinitions,...e}}static cleanupElementProps(e){const{children:t,dangerouslySetInnerHTML:r,...s}=e;return s}sortTags(e){return new Map([...e.entries()].sort((([,e],[,t])=>e.order-t.order)))}getTags(){const{meta:e,body:t,html:r,containers:s}=this.tags;return{html:this.sortTags(r),body:this.sortTags(t),meta:this.sortTags(e),containers:s}}replaceAttribute(e,t){return"meta"===e&&"httpequiv"===t.toLowerCase()?"http-equiv":t}createDomElement(e){if(this.isServer)return;const{type:t,props:r}=e,s=document.createElement(t);return this.applyDomElementAttributes(s,r),s}buildKeyByProps(e,t={},r=!1){let s="";for(const e of["id","name","href","src"])if(t[e]){s=`[${e}='${t[e]}']`;break}if(s||r||(s=Object.entries(t).map((([e,t])=>"string"==typeof t&&`[${e}]`)).filter(Boolean).join("")),s)return`${e}${s}`}getDefaultKey(e,t,r){return`${e}-${t}-${r}-not-unique`}isNotUniqueTag(e){return e.endsWith("-not-unique")}cloneElement(e){const{type:r}=e,s={...e?.props??{}};return Object.values(this.reservedAttributes).forEach((e=>{s[e]&&delete s[e]})),"title"===r&&Array.isArray(s.children)&&(s.children=s.children.join("")),{element:t.createElement(r,s),elementProps:s}}getElementOrder(e,t,r="unknown"){return(e[this.reservedAttributes.order]?Number(e[this.reservedAttributes.order]):void 0)??this.tagsDefinitions[r]?.order??this.tagsDefinitions[t]?.order??1e3}pushElements(e,t,o=!0,a=i.init){const d=e&&"object"==typeof e&&"type"in e&&e.type===r?e.props.children:e;s.forEach(d,((e,r)=>{if(!e||"object"!=typeof e||!("type"in e)||!e.type||"string"!=typeof e.type)return;const{type:s}=e,{element:d,elementProps:c}=this.cloneElement(e);let h=this.tagsDefinitions[s]?.key??s;switch(s){case"title":break;case"meta":const{charSet:e,httpEquiv:i}=c;h=e?"meta[charset]":i?"meta[httpEquiv]":this.buildKeyByProps(s,c);break;case"html":case"body":if(!o&&this.tags[s].has(h))return;return void this.tags[s].set(t,{props:n.cleanupElementProps(c),order:t===n.rootContainerId?1:this.getElementOrder(c,s,h)});default:h=this.buildKeyByProps(s,c,!0)??this.getDefaultKey(s,t,r)}!o&&this.tags.meta.has(h)||this.isNotUniqueTag(h)&&this.tags.meta.get(h)?.status===i.synced||this.tags.meta.set(h,{element:this.isServer?d:void 0,domElement:t===n.rootContainerId?void 0:this.createDomElement(d),order:this.getElementOrder(c,s,h),containerId:t,status:a})})),this.tags.containers.add(t)}getRootTagProps(e){return e.size?[...e.values()].reduce(((e,t)=>({...e,...t.props})),{}):{}}applyDomElementAttributes(e,t={}){const r=e.tagName.toLowerCase(),s=Object.values(this.reservedAttributes);Object.entries(t).forEach((([t,i])=>{switch(t){case"children":return void(e.innerHTML=i);case"style":return Object.entries(i).forEach((([t,r])=>{e.style[t]=r}))}s.includes(t)||e.setAttribute(this.replaceAttribute(r,t),i)}))}syncMeta(e){const{meta:t,html:r,body:s}=this.getTags();this.syncTimerId&&clearTimeout(this.syncTimerId);for(const{name:e,value:t}of[{name:"html",value:r},{name:"body",value:s}]){const r=this.getRootTagProps(t),s=document.querySelector(e);(s?.getAttributeNames()??[]).forEach((e=>{r[e]||s.removeAttribute(e)})),Object.keys(r).length&&s&&this.applyDomElementAttributes(s,r)}if(!t.size||this.isServer)return;const n=document.getElementsByTagName("head")?.[0];let o;t.forEach(((r,s)=>{const{status:a,domElement:d}=r;if(a===i.synced||!d||e&&e!==r.containerId)return void(o=d);if(a===i.drain)return o=d.previousSibling,d?.remove(),void t.delete(s);if(r.status=i.synced,!this.isNotUniqueTag(s)){const e=document.querySelector(s);if(e)return o=d,void e.replaceWith(d)}const c=o&&o.nextSibling;c?n.insertBefore(d,c):n.append(d),o=d}))}pushTags(e,t,r=!0){const s=this.tags.containers.has(t);this.pushElements(e,t,r,s?i.synced:i.init),this.isServer||s||this.syncMeta()}removeTags(e){this.tags.meta.forEach(((t,r)=>{if(t.containerId===e&&t.containerId!==n.rootContainerId)return this.isNotUniqueTag(r)?(t.domElement?.remove(),void this.tags.meta.delete(r)):void(t.status=i.drain)})),e!==n.rootContainerId&&(this.tags.body.delete(e),this.tags.html.delete(e),this.tags.containers.delete(e),this.syncTimerId=setTimeout((()=>this.syncMeta()),500))}analyzeClientHead(){if(this.isServer)return;for(const t of["html","body"]){const r=document.getElementsByTagName(t)?.[0].cloneNode(!1)?.outerHTML;this.pushElements(e(r),n.rootContainerId,!1)}const t=document.getElementsByTagName("head")?.[0],r=e(t?.innerHTML??"");this.pushElements(r,n.rootContainerId,!1,i.synced);const s=[...this.tags.meta.values()];t.childNodes.forEach(((e,t)=>{const r=s[t];r&&(r.domElement=e)}))}}export{n as default};
|
|
2
2
|
//# sourceMappingURL=manager.js.map
|
package/manager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sources":["../src/manager.ts"],"sourcesContent":["import htmlParser from 'html-react-parser';\nimport type { ReactElement, ReactNode } from 'react';\nimport React, { Children } from 'react';\n\nenum TagStatus {\n init = 'init',\n synced = 'synced',\n drain = 'drain',\n}\n\nexport interface IMetaManagerTags {\n html: Map<string, Record<string, any>>; // containerId => styles\n body: Map<string, Record<string, any>>; // containerId => styles\n meta: Map<\n string,\n {\n element?: ReactElement;\n domElement?: HTMLElement;\n order: number;\n containerId: string;\n status: TagStatus;\n }\n >;\n containers: Set<string>;\n}\n\nexport interface IMetaManagerTagsDefinitions {\n [type: string]: {\n key?: string;\n order: number;\n };\n}\n\n/**\n * Meta tags manager\n */\nclass Manager {\n /**\n * Root container id\n */\n public static rootContainerId = 'root';\n\n /**\n * Detect server side\n */\n public isServer = typeof window === 'undefined';\n\n /**\n * Meta tags state\n */\n protected tags: IMetaManagerTags;\n\n /**\n * Tags definitions\n */\n protected tagsDefinitions: IMetaManagerTagsDefinitions = {\n 'meta[charset]': {\n order: 10,\n },\n \"meta[name='viewport']\": {\n order: 20,\n },\n title: {\n order: 100,\n },\n base: {\n order: 110,\n },\n meta: {\n order: 200,\n },\n link: {\n order: 300,\n },\n script: {\n order: 400,\n },\n noscript: {\n order: 500,\n },\n style: {\n order: 600,\n },\n };\n\n /**\n * System tag attributes\n */\n protected reservedAttributes = {\n order: 'data-order',\n };\n\n /**\n * Scheduled synchronization\n */\n protected syncTimerId: null | NodeJS.Timeout = null;\n\n /**\n * @constructor\n */\n constructor(tags: Partial<Manager['tags']> = {}) {\n this.tags = {\n html: new Map(tags?.html),\n body: new Map(tags?.body),\n meta: new Map(tags?.meta),\n containers: new Set(tags?.containers),\n };\n }\n\n /**\n * Set tags definitions\n */\n public setTagsDefinitions(definitions: IMetaManagerTagsDefinitions): void {\n this.tagsDefinitions = { ...this.tagsDefinitions, ...definitions };\n }\n\n /**\n * Remove children, dangerouslySetInnerHTML etc.\n */\n public static cleanupElementProps(props: Record<string, any>): Record<string, any> {\n const { children: _, dangerouslySetInnerHTML: __, ...restProps } = props;\n\n return restProps;\n }\n\n /**\n * Get meta tags\n */\n public getTags(): IMetaManagerTags {\n const { meta, body, html, containers } = this.tags;\n const sortedTags = new Map(\n [...meta.entries()].sort(([, tagA], [, tagB]) => tagA.order - tagB.order),\n );\n\n return {\n body,\n html,\n meta: sortedTags,\n containers,\n };\n }\n\n /**\n * Replace react attribute to valid DOM attribute\n */\n protected replaceAttribute(tagName: string, attribute: string): string {\n if (tagName === 'meta' && attribute.toLowerCase() === 'httpequiv') {\n return 'http-equiv';\n }\n\n return attribute;\n }\n\n /**\n * Create DOM element from React element\n */\n protected createDomElement(element: ReactElement): HTMLElement | undefined {\n if (this.isServer) {\n return;\n }\n\n const { type, props } = element;\n const domElement = document.createElement(type as string);\n\n this.applyDomElementAttributes(domElement, props as Record<string, string>);\n\n return domElement;\n }\n\n /**\n * Make element key by element props\n */\n protected buildKeyByProps(\n type: string,\n props: Record<string, any> = {},\n isSkipProps = false,\n ): string | undefined {\n let key = '';\n\n // try to build unique key by unique props\n for (const uniqueAttr of ['id', 'name', 'href', 'src']) {\n if (props[uniqueAttr]) {\n key = `[${uniqueAttr}='${props[uniqueAttr] as string}']`;\n\n break;\n }\n }\n\n // try to build by props\n if (!key && !isSkipProps) {\n key = Object.entries(props)\n .map(([k, v]) => {\n if (typeof v === 'string') {\n return `[${k}]`;\n }\n\n return false;\n })\n .filter(Boolean)\n .join('');\n }\n\n if (!key) {\n return undefined;\n }\n\n return `${type}${key}`;\n }\n\n /**\n * Get default tag key for not unique elements\n */\n protected getDefaultKey(type: string, containerId: string, index: number): string {\n return `${type}-${containerId}-${index}-not-unique`;\n }\n\n /**\n * Check if tag is not unique\n */\n protected isNotUniqueTag(key: string): boolean {\n return key.endsWith('-not-unique');\n }\n\n /**\n * Clone react element\n */\n protected cloneElement(element: ReactElement): {\n element: ReactElement;\n elementProps: Record<string, any>;\n } {\n const { type } = element;\n const props = { ...(element?.props ?? {}) } as Record<string, any>;\n\n // remove system attributes\n Object.values(this.reservedAttributes).forEach((attrName) => {\n if (props[attrName]) {\n delete props[attrName];\n }\n });\n\n // fix multiple nodes for title\n if (type === 'title' && Array.isArray(props.children)) {\n props.children = props.children.join('');\n }\n\n return {\n element: React.createElement(type, props),\n elementProps: props,\n };\n }\n\n /**\n * Push react elements to meta state\n */\n protected pushElements(\n elements: ReactNode,\n containerId: string,\n isReplace = true,\n status = TagStatus.init,\n ): void {\n Children.forEach(elements, (child, index) => {\n // skip unsupported elements\n if (!child || typeof child !== 'object' || !('type' in child) || !child.type) {\n return;\n }\n\n const { type } = child;\n const { element, elementProps } = this.cloneElement(child);\n const elementOrder = elementProps[this.reservedAttributes.order]\n ? Number(elementProps[this.reservedAttributes.order])\n : undefined;\n\n let order = elementOrder ?? this.tagsDefinitions[type as string]?.order ?? 1000;\n let key = this.tagsDefinitions[type as string]?.key ?? (type as string);\n\n switch (type) {\n case 'title':\n break;\n\n case 'meta':\n const { charSet, httpEquiv } = elementProps;\n\n if (charSet) {\n key = `meta[charset]`;\n } else if (httpEquiv) {\n key = `meta[httpEquiv]`;\n } else {\n key = this.buildKeyByProps(type, elementProps)!;\n }\n\n break;\n\n case 'html':\n case 'body':\n if (!isReplace && this.tags[type].has(key)) {\n return;\n }\n\n this.tags[type].set(containerId, Manager.cleanupElementProps(elementProps));\n\n return;\n\n case 'link':\n case 'script':\n case 'noscript':\n case 'style':\n default:\n key =\n this.buildKeyByProps(type as string, elementProps, true) ??\n this.getDefaultKey(type as string, containerId, index);\n break;\n }\n\n // skip replace existed meta tag (e.g. for push existed tags from parsed html, @see ServerManager)\n if (!isReplace && this.tags.meta.has(key)) {\n return;\n }\n\n // skip push already existed in head not unique tags\n if (this.isNotUniqueTag(key) && this.tags.meta.get(key)?.status === TagStatus.synced) {\n return;\n }\n\n // custom order\n if (this.tagsDefinitions[key]?.order) {\n order = this.tagsDefinitions[key]?.order;\n }\n\n this.tags.meta.set(key, {\n element: this.isServer ? element : undefined, // keep element only for server render\n domElement:\n /**\n * create DOM element only for client side\n * generate DOM element for root container inside @see this.analyzeClientHead\n */\n containerId === Manager.rootContainerId ? undefined : this.createDomElement(element),\n order,\n containerId,\n status,\n });\n });\n\n this.tags.containers.add(containerId);\n }\n\n /**\n * Get html or body props\n */\n public getRootTagProps(props: Manager['tags']['html']): Record<string, any> {\n if (!props.size) {\n return {};\n }\n\n return [...props.values()].reduce(\n (res, val) => ({\n ...res,\n ...val,\n }),\n {},\n );\n }\n\n /**\n * Apply props to dom element\n */\n protected applyDomElementAttributes(element: Element, props: Record<string, any> = {}): void {\n const tagName = element.tagName.toLowerCase();\n const reservedAttributes = Object.values(this.reservedAttributes);\n\n // apply attributes\n Object.entries(props).forEach(([name, value]) => {\n switch (name) {\n case 'children':\n element.innerHTML = value;\n\n return;\n\n case 'style':\n return Object.entries(value as Record<string, string>).forEach(\n ([styleName, styleValue]) => {\n element['style'][styleName] = styleValue;\n },\n );\n }\n\n if (reservedAttributes[name]) {\n return;\n }\n\n element.setAttribute(this.replaceAttribute(tagName, name), value as string);\n });\n }\n\n /**\n * Synchronize local meta tags with head meta tags\n */\n protected syncMeta(containerId?: string): void {\n const { meta, html, body } = this.getTags();\n\n if (this.syncTimerId) {\n clearTimeout(this.syncTimerId);\n }\n\n // apply html, body props\n for (const { name, value } of [\n { name: 'html', value: html },\n { name: 'body', value: body },\n ]) {\n const tagProps = this.getRootTagProps(value);\n const domElement = document.querySelector(name);\n const existAttributes = domElement?.getAttributeNames() ?? [];\n\n // remove drained props\n existAttributes.forEach((attrName) => {\n if (tagProps[attrName]) {\n return;\n }\n\n domElement!.removeAttribute(attrName);\n });\n\n if (!Object.keys(tagProps).length || !domElement) {\n continue;\n }\n\n this.applyDomElementAttributes(domElement, tagProps);\n }\n\n if (!meta.size || this.isServer) {\n return;\n }\n\n const head = document.getElementsByTagName('head')?.[0];\n let prevElement: HTMLElement | ChildNode | null | undefined;\n\n meta.forEach((metaTag, key) => {\n const { status, domElement } = metaTag;\n\n if (\n status === TagStatus.synced ||\n !domElement ||\n (containerId && containerId !== metaTag.containerId)\n ) {\n prevElement = domElement;\n\n return;\n }\n\n // remove tags\n if (status === TagStatus.drain) {\n prevElement = domElement.previousSibling;\n domElement?.remove();\n meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.synced;\n\n if (!this.isNotUniqueTag(key)) {\n const exist = document.querySelector(key);\n\n if (exist) {\n prevElement = domElement;\n exist.replaceWith(domElement);\n\n return;\n }\n }\n\n const beforeElement = prevElement && prevElement.nextSibling;\n\n if (beforeElement) {\n head.insertBefore(domElement, beforeElement);\n } else {\n head.append(domElement);\n }\n\n prevElement = domElement;\n });\n }\n\n /**\n * Push new meta tags\n */\n public pushTags(elements: ReactNode, containerId: string, isReplace = true): void {\n const isAdded = this.tags.containers.has(containerId);\n\n this.pushElements(\n elements,\n containerId,\n isReplace,\n isAdded ? TagStatus.synced : TagStatus.init,\n );\n\n // skip sync already synced tags\n if (this.isServer || isAdded) {\n return;\n }\n\n this.syncMeta();\n }\n\n /**\n * Remove meta tags\n */\n public removeTags(containerId: string): void {\n this.tags.meta.forEach((metaTag, key) => {\n if (metaTag.containerId !== containerId || metaTag.containerId === Manager.rootContainerId) {\n return;\n }\n\n // remove immediately not unique tags\n if (this.isNotUniqueTag(key)) {\n metaTag.domElement?.remove();\n this.tags.meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.drain;\n });\n\n if (containerId === Manager.rootContainerId) {\n return;\n }\n\n this.tags.body.delete(containerId);\n this.tags.html.delete(containerId);\n this.tags.containers.delete(containerId);\n this.syncTimerId = setTimeout(() => this.syncMeta(), 500);\n }\n\n /**\n * Initial analyze client head meta tags\n */\n public analyzeClientHead(): void {\n if (this.isServer) {\n return;\n }\n\n // parse default attributes for root tags\n for (const tagName of ['html', 'body']) {\n const htmlTag = document.getElementsByTagName(tagName)?.[0].cloneNode(false)?.[\n 'outerHTML'\n ] as string;\n\n this.pushElements(htmlParser(htmlTag), Manager.rootContainerId, false);\n }\n\n // parse default meta tags\n const head = document.getElementsByTagName('head')?.[0];\n const reactElements = htmlParser(head?.innerHTML ?? '');\n\n this.pushElements(reactElements, Manager.rootContainerId, false, TagStatus.synced);\n\n const meta = [...this.tags.meta.values()];\n\n // attach real dom node to virtual tags\n head.childNodes.forEach((node, i) => {\n const existedElem = meta[i];\n\n if (!existedElem) {\n return;\n }\n\n existedElem.domElement = node as HTMLElement;\n });\n }\n}\n\nexport default Manager;\n"],"names":["TagStatus","Manager","static","isServer","window","tags","tagsDefinitions","order","title","base","meta","link","script","noscript","style","reservedAttributes","syncTimerId","constructor","this","html","Map","body","containers","Set","setTagsDefinitions","definitions","props","children","_","dangerouslySetInnerHTML","__","restProps","getTags","entries","sort","tagA","tagB","replaceAttribute","tagName","attribute","toLowerCase","createDomElement","element","type","domElement","document","createElement","applyDomElementAttributes","buildKeyByProps","isSkipProps","key","uniqueAttr","Object","map","k","v","filter","Boolean","join","getDefaultKey","containerId","index","isNotUniqueTag","endsWith","cloneElement","values","forEach","attrName","Array","isArray","React","elementProps","pushElements","elements","isReplace","status","init","Children","child","Number","undefined","charSet","httpEquiv","has","set","cleanupElementProps","get","synced","rootContainerId","add","getRootTagProps","size","reduce","res","val","name","value","innerHTML","styleName","styleValue","setAttribute","syncMeta","clearTimeout","tagProps","querySelector","getAttributeNames","removeAttribute","keys","length","head","getElementsByTagName","prevElement","metaTag","drain","previousSibling","remove","delete","exist","replaceWith","beforeElement","nextSibling","insertBefore","append","pushTags","isAdded","removeTags","setTimeout","analyzeClientHead","htmlTag","cloneNode","htmlParser","reactElements","childNodes","node","i","existedElem"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"manager.js","sources":["../src/manager.ts"],"sourcesContent":["import htmlParser from 'html-react-parser';\nimport type { ReactElement, ReactNode } from 'react';\nimport React, { Fragment, Children } from 'react';\nimport TagStatus from './tag-status';\n\nexport interface IMetaManagerTags {\n html: Map<\n string,\n {\n props: Record<string, any>;\n order: number;\n }\n >;\n body: Map<\n string,\n {\n props: Record<string, any>;\n order: number;\n }\n >; // containerId => styles\n meta: Map<\n string,\n {\n element?: ReactElement;\n domElement?: HTMLElement;\n order: number;\n containerId: string;\n status: TagStatus;\n }\n >;\n containers: Set<string>;\n}\n\nexport interface IMetaManagerTagsDefinitions {\n [type: string]: {\n key?: string;\n order: number;\n };\n}\n\n/**\n * Meta tags manager\n */\nclass Manager {\n /**\n * Root container id\n */\n public static rootContainerId = 'root';\n\n /**\n * Detect server side\n */\n public isServer = typeof window === 'undefined';\n\n /**\n * Meta tags state\n */\n protected tags: IMetaManagerTags;\n\n /**\n * Tags definitions\n */\n protected tagsDefinitions: IMetaManagerTagsDefinitions = {\n 'meta[charset]': {\n order: 10,\n },\n \"meta[name='viewport']\": {\n order: 20,\n },\n title: {\n order: 100,\n },\n base: {\n order: 110,\n },\n meta: {\n order: 200,\n },\n link: {\n order: 300,\n },\n script: {\n order: 400,\n },\n noscript: {\n order: 500,\n },\n style: {\n order: 600,\n },\n };\n\n /**\n * System tag attributes\n */\n protected reservedAttributes = {\n order: 'data-order',\n };\n\n /**\n * Scheduled synchronization\n */\n protected syncTimerId: null | NodeJS.Timeout = null;\n\n /**\n * @constructor\n */\n constructor(tags: Partial<Manager['tags']> = {}) {\n this.tags = {\n html: new Map(tags?.html),\n body: new Map(tags?.body),\n meta: new Map(tags?.meta),\n containers: new Set(tags?.containers),\n };\n }\n\n /**\n * Set tags definitions\n */\n public setTagsDefinitions(definitions: IMetaManagerTagsDefinitions): void {\n this.tagsDefinitions = { ...this.tagsDefinitions, ...definitions };\n }\n\n /**\n * Remove children, dangerouslySetInnerHTML etc.\n */\n public static cleanupElementProps(props: Record<string, any>): Record<string, any> {\n const { children: _, dangerouslySetInnerHTML: __, ...restProps } = props;\n\n return restProps;\n }\n\n /**\n * Sort elements or props\n */\n protected sortTags<T extends Map<string, { order: number }>>(elements: T): T {\n return new Map(\n [...elements.entries()].sort(([, tagA], [, tagB]) => tagA.order - tagB.order),\n ) as T;\n }\n\n /**\n * Get meta tags\n */\n public getTags(): IMetaManagerTags {\n const { meta, body, html, containers } = this.tags;\n\n return {\n html: this.sortTags(html),\n body: this.sortTags(body),\n meta: this.sortTags(meta),\n containers,\n };\n }\n\n /**\n * Replace react attribute to valid DOM attribute\n */\n protected replaceAttribute(tagName: string, attribute: string): string {\n if (tagName === 'meta' && attribute.toLowerCase() === 'httpequiv') {\n return 'http-equiv';\n }\n\n return attribute;\n }\n\n /**\n * Create DOM element from React element\n */\n protected createDomElement(element: ReactElement): HTMLElement | undefined {\n if (this.isServer) {\n return;\n }\n\n const { type, props } = element;\n const domElement = document.createElement(type as string);\n\n this.applyDomElementAttributes(domElement, props as Record<string, string>);\n\n return domElement;\n }\n\n /**\n * Make element key by element props\n */\n protected buildKeyByProps(\n type: string,\n props: Record<string, any> = {},\n isSkipProps = false,\n ): string | undefined {\n let key = '';\n\n // try to build unique key by unique props\n for (const uniqueAttr of ['id', 'name', 'href', 'src']) {\n if (props[uniqueAttr]) {\n key = `[${uniqueAttr}='${props[uniqueAttr] as string}']`;\n\n break;\n }\n }\n\n // try to build by props\n if (!key && !isSkipProps) {\n key = Object.entries(props)\n .map(([k, v]) => {\n if (typeof v === 'string') {\n return `[${k}]`;\n }\n\n return false;\n })\n .filter(Boolean)\n .join('');\n }\n\n if (!key) {\n return undefined;\n }\n\n return `${type}${key}`;\n }\n\n /**\n * Get default tag key for not unique elements\n */\n protected getDefaultKey(type: string, containerId: string, index: number): string {\n return `${type}-${containerId}-${index}-not-unique`;\n }\n\n /**\n * Check if tag is not unique\n */\n protected isNotUniqueTag(key: string): boolean {\n return key.endsWith('-not-unique');\n }\n\n /**\n * Clone react element\n */\n protected cloneElement(element: ReactElement): {\n element: ReactElement;\n elementProps: Record<string, any>;\n } {\n const { type } = element;\n const props = { ...(element?.props ?? {}) } as Record<string, any>;\n\n // remove system attributes\n Object.values(this.reservedAttributes).forEach((attrName) => {\n if (props[attrName]) {\n delete props[attrName];\n }\n });\n\n // fix multiple nodes for title\n if (type === 'title' && Array.isArray(props.children)) {\n props.children = props.children.join('');\n }\n\n return {\n element: React.createElement(type, props),\n elementProps: props,\n };\n }\n\n /**\n * Get element order\n */\n protected getElementOrder(\n elementProps: Record<string, any>,\n type: string,\n key: string = 'unknown',\n ): number {\n const elementOrder = elementProps[this.reservedAttributes.order]\n ? Number(elementProps[this.reservedAttributes.order])\n : undefined;\n\n return (\n elementOrder ?? this.tagsDefinitions[key]?.order ?? this.tagsDefinitions[type]?.order ?? 1000\n );\n }\n\n /**\n * Push react elements to meta state\n */\n protected pushElements(\n elements: ReactNode,\n containerId: string,\n isReplace = true,\n status = TagStatus.init,\n ): void {\n // unwrap fragment\n const clearElements: ReactNode =\n elements && typeof elements === 'object' && 'type' in elements && elements.type === Fragment\n ? elements.props.children\n : elements;\n\n Children.forEach(clearElements, (child, index) => {\n // skip unsupported elements\n if (\n !child ||\n typeof child !== 'object' ||\n !('type' in child) ||\n !child.type ||\n typeof child.type !== 'string'\n ) {\n return;\n }\n\n const { type } = child;\n const { element, elementProps } = this.cloneElement(child);\n let key = this.tagsDefinitions[type]?.key ?? type;\n\n switch (type) {\n case 'title':\n break;\n\n case 'meta':\n const { charSet, httpEquiv } = elementProps;\n\n if (charSet) {\n key = `meta[charset]`;\n } else if (httpEquiv) {\n key = `meta[httpEquiv]`;\n } else {\n key = this.buildKeyByProps(type, elementProps)!;\n }\n\n break;\n\n case 'html':\n case 'body':\n if (!isReplace && this.tags[type].has(key)) {\n return;\n }\n\n this.tags[type].set(containerId, {\n props: Manager.cleanupElementProps(elementProps),\n order:\n containerId === Manager.rootContainerId\n ? 1\n : this.getElementOrder(elementProps, type, key),\n });\n\n return;\n\n case 'link':\n case 'script':\n case 'noscript':\n case 'style':\n default:\n key =\n this.buildKeyByProps(type, elementProps, true) ??\n this.getDefaultKey(type, containerId, index);\n break;\n }\n\n // skip replace existed meta tag (e.g. for push existed tags from parsed html, @see ServerManager)\n if (!isReplace && this.tags.meta.has(key)) {\n return;\n }\n\n // skip push already existed in head not unique tags\n if (this.isNotUniqueTag(key) && this.tags.meta.get(key)?.status === TagStatus.synced) {\n return;\n }\n\n this.tags.meta.set(key, {\n element: this.isServer ? element : undefined, // keep element only for server render\n domElement:\n /**\n * create DOM element only for client side\n * generate DOM element for root container inside @see this.analyzeClientHead\n */\n containerId === Manager.rootContainerId ? undefined : this.createDomElement(element),\n order: this.getElementOrder(elementProps, type, key),\n containerId,\n status,\n });\n });\n\n this.tags.containers.add(containerId);\n }\n\n /**\n * Get html or body props\n */\n public getRootTagProps(props: Manager['tags']['html']): Record<string, any> {\n if (!props.size) {\n return {};\n }\n\n return [...props.values()].reduce(\n (res, val) => ({\n ...res,\n ...val.props,\n }),\n {},\n );\n }\n\n /**\n * Apply props to dom element\n */\n protected applyDomElementAttributes(element: Element, props: Record<string, any> = {}): void {\n const tagName = element.tagName.toLowerCase();\n const reservedAttributes = Object.values(this.reservedAttributes);\n\n // apply attributes\n Object.entries(props).forEach(([name, value]) => {\n switch (name) {\n case 'children':\n element.innerHTML = value;\n\n return;\n\n case 'style':\n return Object.entries(value as Record<string, string>).forEach(\n ([styleName, styleValue]) => {\n // @ts-ignore\n element['style'][styleName] = styleValue;\n },\n );\n }\n\n if (reservedAttributes.includes(name)) {\n return;\n }\n\n element.setAttribute(this.replaceAttribute(tagName, name), value as string);\n });\n }\n\n /**\n * Synchronize local meta tags with head meta tags\n */\n protected syncMeta(containerId?: string): void {\n const { meta, html, body } = this.getTags();\n\n if (this.syncTimerId) {\n clearTimeout(this.syncTimerId);\n }\n\n // apply html, body props\n for (const { name, value } of [\n { name: 'html', value: html },\n { name: 'body', value: body },\n ]) {\n const tagProps = this.getRootTagProps(value);\n const domElement = document.querySelector(name);\n const existAttributes = domElement?.getAttributeNames() ?? [];\n\n // remove drained props\n existAttributes.forEach((attrName) => {\n if (tagProps[attrName]) {\n return;\n }\n\n domElement!.removeAttribute(attrName);\n });\n\n if (!Object.keys(tagProps).length || !domElement) {\n continue;\n }\n\n this.applyDomElementAttributes(domElement, tagProps);\n }\n\n if (!meta.size || this.isServer) {\n return;\n }\n\n const head = document.getElementsByTagName('head')?.[0];\n let prevElement: HTMLElement | ChildNode | null | undefined;\n\n meta.forEach((metaTag, key) => {\n const { status, domElement } = metaTag;\n\n if (\n status === TagStatus.synced ||\n !domElement ||\n (containerId && containerId !== metaTag.containerId)\n ) {\n prevElement = domElement;\n\n return;\n }\n\n // remove tags\n if (status === TagStatus.drain) {\n prevElement = domElement.previousSibling;\n domElement?.remove();\n meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.synced;\n\n if (!this.isNotUniqueTag(key)) {\n const exist = document.querySelector(key);\n\n if (exist) {\n prevElement = domElement;\n exist.replaceWith(domElement);\n\n return;\n }\n }\n\n const beforeElement = prevElement && prevElement.nextSibling;\n\n if (beforeElement) {\n head.insertBefore(domElement, beforeElement);\n } else {\n head.append(domElement);\n }\n\n prevElement = domElement;\n });\n }\n\n /**\n * Push new meta tags\n */\n public pushTags(elements: ReactNode, containerId: string, isReplace = true): void {\n const isAdded = this.tags.containers.has(containerId);\n\n this.pushElements(\n elements,\n containerId,\n isReplace,\n isAdded ? TagStatus.synced : TagStatus.init,\n );\n\n // skip sync already synced tags\n if (this.isServer || isAdded) {\n return;\n }\n\n this.syncMeta();\n }\n\n /**\n * Remove meta tags\n */\n public removeTags(containerId: string): void {\n this.tags.meta.forEach((metaTag, key) => {\n if (metaTag.containerId !== containerId || metaTag.containerId === Manager.rootContainerId) {\n return;\n }\n\n // remove immediately not unique tags\n if (this.isNotUniqueTag(key)) {\n metaTag.domElement?.remove();\n this.tags.meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.drain;\n });\n\n if (containerId === Manager.rootContainerId) {\n return;\n }\n\n this.tags.body.delete(containerId);\n this.tags.html.delete(containerId);\n this.tags.containers.delete(containerId);\n this.syncTimerId = setTimeout(() => this.syncMeta(), 500);\n }\n\n /**\n * Initial analyze client head meta tags\n */\n public analyzeClientHead(): void {\n if (this.isServer) {\n return;\n }\n\n // parse default attributes for root tags\n for (const tagName of ['html', 'body']) {\n // @ts-ignore\n const htmlTag = document.getElementsByTagName(tagName)?.[0].cloneNode(false)?.[\n 'outerHTML'\n ] as string;\n\n this.pushElements(htmlParser(htmlTag), Manager.rootContainerId, false);\n }\n\n // parse default meta tags\n const head = document.getElementsByTagName('head')?.[0];\n const reactElements = htmlParser(head?.innerHTML ?? '');\n\n this.pushElements(reactElements, Manager.rootContainerId, false, TagStatus.synced);\n\n const meta = [...this.tags.meta.values()];\n\n // attach real dom node to virtual tags\n head.childNodes.forEach((node, i) => {\n const existedElem = meta[i];\n\n if (!existedElem) {\n return;\n }\n\n existedElem.domElement = node as HTMLElement;\n });\n }\n}\n\nexport default Manager;\n"],"names":["Manager","static","isServer","window","tags","tagsDefinitions","order","title","base","meta","link","script","noscript","style","reservedAttributes","syncTimerId","constructor","this","html","Map","body","containers","Set","setTagsDefinitions","definitions","props","children","_","dangerouslySetInnerHTML","__","restProps","sortTags","elements","entries","sort","tagA","tagB","getTags","replaceAttribute","tagName","attribute","toLowerCase","createDomElement","element","type","domElement","document","createElement","applyDomElementAttributes","buildKeyByProps","isSkipProps","key","uniqueAttr","Object","map","k","v","filter","Boolean","join","getDefaultKey","containerId","index","isNotUniqueTag","endsWith","cloneElement","values","forEach","attrName","Array","isArray","React","elementProps","getElementOrder","Number","undefined","pushElements","isReplace","status","TagStatus","init","clearElements","Fragment","Children","child","charSet","httpEquiv","has","set","cleanupElementProps","rootContainerId","get","synced","add","getRootTagProps","size","reduce","res","val","name","value","innerHTML","styleName","styleValue","includes","setAttribute","syncMeta","clearTimeout","tagProps","querySelector","getAttributeNames","removeAttribute","keys","length","head","getElementsByTagName","prevElement","metaTag","drain","previousSibling","remove","delete","exist","replaceWith","beforeElement","nextSibling","insertBefore","append","pushTags","isAdded","removeTags","setTimeout","analyzeClientHead","htmlTag","cloneNode","htmlParser","reactElements","childNodes","node","i","existedElem"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lomray/react-head-manager",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "React meta tags manager with SSR and Suspense support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -35,38 +35,43 @@
|
|
|
35
35
|
"release": "npm run build && cd lib && npm publish",
|
|
36
36
|
"lint:check": "eslint --ext \".ts,.tsx\" \"src/**/*.{ts,tsx,*.ts,*tsx}\"",
|
|
37
37
|
"lint:format": "eslint --fix --ext \".ts,.tsx\" \"src/**/*.{ts,tsx,*.ts,*tsx}\"",
|
|
38
|
-
"ts:check": "tsc --project ./tsconfig.json --skipLibCheck --noemit"
|
|
38
|
+
"ts:check": "tsc --project ./tsconfig.json --skipLibCheck --noemit",
|
|
39
|
+
"test": "vitest run"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"html-react-parser": "^5.
|
|
42
|
+
"html-react-parser": "^5.1.4"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@commitlint/cli": "
|
|
45
|
-
"@commitlint/config-conventional": "
|
|
45
|
+
"@commitlint/cli": "18.6.0",
|
|
46
|
+
"@commitlint/config-conventional": "18.6.0",
|
|
46
47
|
"@lomray/eslint-config": "^4.0.1",
|
|
47
48
|
"@lomray/prettier-config": "^2.0.1",
|
|
48
|
-
"@rollup/plugin-json": "^6.1.0",
|
|
49
49
|
"@rollup/plugin-terser": "^0.4.4",
|
|
50
|
-
"@types/react": "^18.2.
|
|
51
|
-
"@types/react-dom": "^18.2.
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
50
|
+
"@types/react": "^18.2.55",
|
|
51
|
+
"@types/react-dom": "^18.2.19",
|
|
52
|
+
"@types/sinon": "^17.0.3",
|
|
53
|
+
"@types/sinon-chai": "^3.2.12",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
55
|
+
"@vitest/coverage-v8": "^1.2.2",
|
|
54
56
|
"eslint": "^8.56.0",
|
|
55
57
|
"eslint-config-prettier": "^9.1.0",
|
|
56
58
|
"eslint-plugin-import": "^2.29.1",
|
|
57
59
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
|
58
60
|
"eslint-plugin-prettier": "^5.1.3",
|
|
59
|
-
"husky": "^
|
|
60
|
-
"
|
|
61
|
-
"
|
|
61
|
+
"husky": "^9.0.10",
|
|
62
|
+
"jsdom": "^24.0.0",
|
|
63
|
+
"lint-staged": "^15.2.2",
|
|
64
|
+
"prettier": "^3.2.5",
|
|
62
65
|
"react": "^18.2.0",
|
|
63
|
-
"rollup": "^4.
|
|
66
|
+
"rollup": "^4.10.0",
|
|
64
67
|
"rollup-plugin-copy": "^3.5.0",
|
|
65
|
-
"rollup-plugin-folder-input": "^1.0.1",
|
|
66
68
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
67
69
|
"rollup-plugin-ts": "^3.4.5",
|
|
68
70
|
"semantic-release": "^21.1.2",
|
|
69
|
-
"
|
|
71
|
+
"sinon": "^17.0.1",
|
|
72
|
+
"sinon-chai": "^3.7.0",
|
|
73
|
+
"typescript": "^5.3.3",
|
|
74
|
+
"vitest": "^1.2.2"
|
|
70
75
|
},
|
|
71
76
|
"peerDependencies": {
|
|
72
77
|
"@lomray/consistent-suspense": ">=2.0.1",
|
package/tag-status.d.ts
ADDED
package/tag-status.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-status.js","sources":["../src/tag-status.ts"],"sourcesContent":["enum TagStatus {\n init = 'init',\n synced = 'synced',\n drain = 'drain',\n}\n\nexport default TagStatus;\n"],"names":["TagStatus","TagStatus$1"],"mappings":"AAAA,IAAKA,GAAL,SAAKA,GACHA,EAAA,KAAA,OACAA,EAAA,OAAA,SACAA,EAAA,MAAA,OACD,CAJD,CAAKA,IAAAA,EAIJ,CAAA,IAED,IAAAC,EAAeD"}
|