@unhead/dom 1.0.21 → 1.1.0

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/dist/index.cjs CHANGED
@@ -1,39 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const TagsWithInnerContent = ["script", "style", "noscript"];
4
- const HasElementTags = [
5
- "base",
6
- "meta",
7
- "link",
8
- "style",
9
- "script",
10
- "noscript"
11
- ];
3
+ const shared = require('@unhead/shared');
12
4
 
13
- const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
14
- function tagDedupeKey(tag, fn) {
15
- const { props, tag: tagName } = tag;
16
- if (UniqueTags.includes(tagName))
17
- return tagName;
18
- if (tagName === "link" && props.rel === "canonical")
19
- return "canonical";
20
- if (props.charset)
21
- return "charset";
22
- const name = ["id"];
23
- if (tagName === "meta")
24
- name.push(...["name", "property", "http-equiv"]);
25
- for (const n of name) {
26
- if (typeof props[n] !== "undefined") {
27
- const val = String(props[n]);
28
- if (fn && !fn(val))
29
- return false;
30
- return `${tagName}:${n}:${val}`;
31
- }
32
- }
33
- return false;
34
- }
35
-
36
- const setAttrs = (ctx, markSideEffect) => {
5
+ const setAttrs = (ctx, newEntry = false, markSideEffect) => {
37
6
  const { tag, $el } = ctx;
38
7
  if (!$el)
39
8
  return;
@@ -54,94 +23,103 @@ const setAttrs = (ctx, markSideEffect) => {
54
23
  }
55
24
  if (markSideEffect && !k.startsWith("data-h-"))
56
25
  markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
57
- if ($el.getAttribute(k) !== value)
26
+ if (newEntry || $el.getAttribute(k) !== value)
58
27
  $el.setAttribute(k, value);
59
28
  });
60
- if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
61
- $el.innerHTML = tag.children || "";
29
+ if (shared.TagsWithInnerContent.includes(tag.tag)) {
30
+ if (tag.textContent && tag.textContent !== $el.textContent)
31
+ $el.textContent = tag.textContent;
32
+ else if (tag.innerHTML && tag.innerHTML !== $el.innerHTML)
33
+ $el.innerHTML = tag.innerHTML;
34
+ }
62
35
  };
63
36
 
64
- function hashCode(s) {
65
- let h = 9;
66
- for (let i = 0; i < s.length; )
67
- h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
68
- return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
69
- }
70
-
37
+ let prevHash = false;
71
38
  async function renderDOMHead(head, options = {}) {
72
- const ctx = { shouldRender: true };
73
- await head.hooks.callHook("dom:beforeRender", ctx);
74
- if (!ctx.shouldRender)
39
+ const beforeRenderCtx = { shouldRender: true };
40
+ await head.hooks.callHook("dom:beforeRender", beforeRenderCtx);
41
+ if (!beforeRenderCtx.shouldRender)
75
42
  return;
76
- const dom = options.document || window.document;
43
+ const dom = options.document || head.resolvedOptions.document || window.document;
44
+ const tagContexts = (await head.resolveTags()).map(setupTagRenderCtx);
45
+ if (head.resolvedOptions.experimentalHashHydration) {
46
+ prevHash = prevHash || head._hash || false;
47
+ if (prevHash) {
48
+ const hash = shared.computeHashes(tagContexts.map((ctx) => ctx.tag._h));
49
+ if (prevHash === hash)
50
+ return;
51
+ prevHash = hash;
52
+ }
53
+ }
77
54
  const staleSideEffects = head._popSideEffectQueue();
78
55
  head.headEntries().map((entry) => entry._sde).forEach((sde) => {
79
56
  Object.entries(sde).forEach(([key, fn]) => {
80
57
  staleSideEffects[key] = fn;
81
58
  });
82
59
  });
83
- const preRenderTag = async (tag) => {
60
+ const markSideEffect = (ctx, key, fn) => {
61
+ key = `${ctx.renderId}:${key}`;
62
+ if (ctx.entry)
63
+ ctx.entry._sde[key] = fn;
64
+ delete staleSideEffects[key];
65
+ };
66
+ function setupTagRenderCtx(tag) {
84
67
  const entry = head.headEntries().find((e) => e._i === tag._e);
85
68
  const renderCtx = {
86
- renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
69
+ renderId: tag._d || shared.hashTag(tag),
87
70
  $el: null,
88
71
  shouldRender: true,
89
72
  tag,
90
73
  entry,
91
- staleSideEffects
74
+ markSideEffect: (key, fn) => markSideEffect(renderCtx, key, fn)
92
75
  };
93
- await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
94
76
  return renderCtx;
95
- };
77
+ }
96
78
  const renders = [];
97
79
  const pendingRenders = {
98
80
  body: [],
99
81
  head: []
100
82
  };
101
- const markSideEffect = (ctx2, key, fn) => {
102
- key = `${ctx2.renderId}:${key}`;
103
- if (ctx2.entry)
104
- ctx2.entry._sde[key] = fn;
105
- delete staleSideEffects[key];
106
- };
107
- const markEl = (ctx2) => {
108
- head._elMap[ctx2.renderId] = ctx2.$el;
109
- renders.push(ctx2);
110
- markSideEffect(ctx2, "el", () => {
111
- ctx2.$el?.remove();
112
- delete head._elMap[ctx2.renderId];
83
+ const markEl = (ctx) => {
84
+ head._elMap[ctx.renderId] = ctx.$el;
85
+ renders.push(ctx);
86
+ markSideEffect(ctx, "el", () => {
87
+ ctx.$el?.remove();
88
+ delete head._elMap[ctx.renderId];
113
89
  });
114
90
  };
115
- for (const t of await head.resolveTags()) {
116
- const ctx2 = await preRenderTag(t);
117
- if (!ctx2.shouldRender)
91
+ for (const ctx of tagContexts) {
92
+ await head.hooks.callHook("dom:beforeRenderTag", ctx);
93
+ if (!ctx.shouldRender)
118
94
  continue;
119
- const { tag } = ctx2;
95
+ const { tag } = ctx;
120
96
  if (tag.tag === "title") {
121
- dom.title = tag.children || "";
122
- renders.push(ctx2);
97
+ dom.title = tag.textContent || "";
98
+ renders.push(ctx);
123
99
  continue;
124
100
  }
125
101
  if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
126
- ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
127
- setAttrs(ctx2, markSideEffect);
128
- renders.push(ctx2);
102
+ ctx.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
103
+ setAttrs(ctx, false, markSideEffect);
104
+ renders.push(ctx);
129
105
  continue;
130
106
  }
131
- ctx2.$el = head._elMap[ctx2.renderId];
132
- if (!ctx2.$el && tag._hash) {
133
- ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
134
- }
135
- if (ctx2.$el) {
136
- if (ctx2.tag._d)
137
- setAttrs(ctx2);
138
- markEl(ctx2);
107
+ ctx.$el = head._elMap[ctx.renderId];
108
+ if (!ctx.$el && tag.key)
109
+ ctx.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._h}]`);
110
+ if (ctx.$el) {
111
+ if (ctx.tag._d)
112
+ setAttrs(ctx);
113
+ markEl(ctx);
139
114
  continue;
140
115
  }
141
- ctx2.$el = dom.createElement(tag.tag);
142
- setAttrs(ctx2);
143
- pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
116
+ pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx);
144
117
  }
118
+ const fragments = {
119
+ bodyClose: void 0,
120
+ bodyOpen: void 0,
121
+ head: void 0
122
+ };
145
123
  Object.entries(pendingRenders).forEach(([pos, queue]) => {
146
124
  if (!queue.length)
147
125
  return;
@@ -150,42 +128,45 @@ async function renderDOMHead(head, options = {}) {
150
128
  return;
151
129
  for (const $el of [...children].reverse()) {
152
130
  const elTag = $el.tagName.toLowerCase();
153
- if (!HasElementTags.includes(elTag))
131
+ if (!shared.HasElementTags.includes(elTag))
154
132
  continue;
155
- const dedupeKey = tagDedupeKey({
156
- tag: elTag,
157
- // convert attributes to object
158
- props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
159
- });
160
- const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode?.(ctx2.$el)));
133
+ const props = $el.getAttributeNames().reduce((props2, name) => ({ ...props2, [name]: $el.getAttribute(name) }), {});
134
+ const tmpTag = { tag: elTag, props };
135
+ if ($el.innerHTML)
136
+ tmpTag.innerHTML = $el.innerHTML;
137
+ const tmpRenderId = shared.hashTag(tmpTag);
138
+ let matchIdx = queue.findIndex((ctx) => ctx?.renderId === tmpRenderId);
139
+ if (matchIdx === -1) {
140
+ const tmpDedupeKey = shared.tagDedupeKey(tmpTag);
141
+ matchIdx = queue.findIndex((ctx) => ctx?.tag._d && ctx.tag._d === tmpDedupeKey);
142
+ }
161
143
  if (matchIdx !== -1) {
162
- const ctx2 = queue[matchIdx];
163
- ctx2.$el = $el;
164
- setAttrs(ctx2);
165
- markEl(ctx2);
144
+ const ctx = queue[matchIdx];
145
+ ctx.$el = $el;
146
+ setAttrs(ctx);
147
+ markEl(ctx);
166
148
  delete queue[matchIdx];
167
149
  }
168
150
  }
169
- queue.forEach((ctx2) => {
170
- if (!ctx2.$el)
171
- return;
172
- switch (ctx2.tag.tagPosition) {
173
- case "bodyClose":
174
- dom.body.appendChild(ctx2.$el);
175
- break;
176
- case "bodyOpen":
177
- dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
178
- break;
179
- case "head":
180
- default:
181
- dom.head.appendChild(ctx2.$el);
182
- break;
151
+ queue.forEach((ctx) => {
152
+ const pos2 = ctx.tag.tagPosition || "head";
153
+ fragments[pos2] = fragments[pos2] || dom.createDocumentFragment();
154
+ if (!ctx.$el) {
155
+ ctx.$el = dom.createElement(ctx.tag.tag);
156
+ setAttrs(ctx, true);
183
157
  }
184
- markEl(ctx2);
158
+ fragments[pos2].appendChild(ctx.$el);
159
+ markEl(ctx);
185
160
  });
186
161
  });
187
- for (const ctx2 of renders)
188
- await head.hooks.callHook("dom:renderTag", ctx2);
162
+ if (fragments.head)
163
+ dom.head.appendChild(fragments.head);
164
+ if (fragments.bodyOpen)
165
+ dom.body.insertBefore(fragments.bodyOpen, dom.body.firstChild);
166
+ if (fragments.bodyClose)
167
+ dom.body.appendChild(fragments.bodyClose);
168
+ for (const ctx of renders)
169
+ await head.hooks.callHook("dom:renderTag", ctx);
189
170
  Object.values(staleSideEffects).forEach((fn) => fn());
190
171
  }
191
172
  exports.domUpdatePromise = null;
@@ -198,6 +179,27 @@ async function debouncedRenderDOMHead(head, options = {}) {
198
179
  return exports.domUpdatePromise = exports.domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
199
180
  }
200
181
 
182
+ const PatchDomOnEntryUpdatesPlugin = (options) => {
183
+ return shared.defineHeadPlugin({
184
+ hooks: {
185
+ "entries:updated": function(head) {
186
+ if (typeof options?.document === "undefined" && typeof window === "undefined")
187
+ return;
188
+ let delayFn = options?.delayFn;
189
+ if (!delayFn && typeof requestAnimationFrame !== "undefined")
190
+ delayFn = requestAnimationFrame;
191
+ debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
192
+ }
193
+ }
194
+ });
195
+ };
196
+
197
+ function maybeGetSSRHash(document) {
198
+ return document?.head.querySelector('meta[name="unhead:ssr"]')?.getAttribute("content") || false;
199
+ }
200
+
201
+ exports.PatchDomOnEntryUpdatesPlugin = PatchDomOnEntryUpdatesPlugin;
201
202
  exports.debouncedRenderDOMHead = debouncedRenderDOMHead;
202
- exports.hashCode = hashCode;
203
+ exports.maybeGetSSRHash = maybeGetSSRHash;
203
204
  exports.renderDOMHead = renderDOMHead;
205
+ exports.setAttrs = setAttrs;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Unhead } from '@unhead/schema';
1
+ import * as _unhead_schema from '@unhead/schema';
2
+ import { Unhead, DomRenderTagContext } from '@unhead/schema';
2
3
 
3
4
  interface RenderDomHeadOptions {
4
5
  /**
@@ -6,6 +7,12 @@ interface RenderDomHeadOptions {
6
7
  */
7
8
  document?: Document;
8
9
  }
10
+ interface DebouncedRenderDomHeadOptions extends RenderDomHeadOptions {
11
+ /**
12
+ * Specify a custom delay function for delaying the render.
13
+ */
14
+ delayFn?: (fn: () => void) => void;
15
+ }
9
16
  /**
10
17
  * Render the head tags to the DOM.
11
18
  */
@@ -17,10 +24,18 @@ declare let domUpdatePromise: Promise<void> | null;
17
24
  /**
18
25
  * Queue a debounced update of the DOM head.
19
26
  */
20
- declare function debouncedRenderDOMHead<T extends Unhead<any>>(head: T, options?: RenderDomHeadOptions & {
27
+ declare function debouncedRenderDOMHead<T extends Unhead<any>>(head: T, options?: DebouncedRenderDomHeadOptions): Promise<void>;
28
+
29
+ interface TriggerDomPatchingOnUpdatesPluginOptions extends RenderDomHeadOptions {
21
30
  delayFn?: (fn: () => void) => void;
22
- }): Promise<void>;
31
+ }
32
+ declare const PatchDomOnEntryUpdatesPlugin: (options?: TriggerDomPatchingOnUpdatesPluginOptions) => _unhead_schema.HeadPlugin;
33
+
34
+ /**
35
+ * Set attributes on a DOM element, while adding entry side effects.
36
+ */
37
+ declare const setAttrs: (ctx: DomRenderTagContext, newEntry?: boolean, markSideEffect?: ((ctx: DomRenderTagContext, k: string, fn: () => void) => void) | undefined) => void;
23
38
 
24
- declare function hashCode(s: string): string;
39
+ declare function maybeGetSSRHash(document: Document): string | false;
25
40
 
26
- export { RenderDomHeadOptions, debouncedRenderDOMHead, domUpdatePromise, hashCode, renderDOMHead };
41
+ export { DebouncedRenderDomHeadOptions, PatchDomOnEntryUpdatesPlugin, RenderDomHeadOptions, TriggerDomPatchingOnUpdatesPluginOptions, debouncedRenderDOMHead, domUpdatePromise, maybeGetSSRHash, renderDOMHead, setAttrs };
package/dist/index.mjs CHANGED
@@ -1,37 +1,6 @@
1
- const TagsWithInnerContent = ["script", "style", "noscript"];
2
- const HasElementTags = [
3
- "base",
4
- "meta",
5
- "link",
6
- "style",
7
- "script",
8
- "noscript"
9
- ];
1
+ import { TagsWithInnerContent, computeHashes, hashTag, HasElementTags, tagDedupeKey, defineHeadPlugin } from '@unhead/shared';
10
2
 
11
- const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
12
- function tagDedupeKey(tag, fn) {
13
- const { props, tag: tagName } = tag;
14
- if (UniqueTags.includes(tagName))
15
- return tagName;
16
- if (tagName === "link" && props.rel === "canonical")
17
- return "canonical";
18
- if (props.charset)
19
- return "charset";
20
- const name = ["id"];
21
- if (tagName === "meta")
22
- name.push(...["name", "property", "http-equiv"]);
23
- for (const n of name) {
24
- if (typeof props[n] !== "undefined") {
25
- const val = String(props[n]);
26
- if (fn && !fn(val))
27
- return false;
28
- return `${tagName}:${n}:${val}`;
29
- }
30
- }
31
- return false;
32
- }
33
-
34
- const setAttrs = (ctx, markSideEffect) => {
3
+ const setAttrs = (ctx, newEntry = false, markSideEffect) => {
35
4
  const { tag, $el } = ctx;
36
5
  if (!$el)
37
6
  return;
@@ -52,94 +21,103 @@ const setAttrs = (ctx, markSideEffect) => {
52
21
  }
53
22
  if (markSideEffect && !k.startsWith("data-h-"))
54
23
  markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
55
- if ($el.getAttribute(k) !== value)
24
+ if (newEntry || $el.getAttribute(k) !== value)
56
25
  $el.setAttribute(k, value);
57
26
  });
58
- if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
59
- $el.innerHTML = tag.children || "";
27
+ if (TagsWithInnerContent.includes(tag.tag)) {
28
+ if (tag.textContent && tag.textContent !== $el.textContent)
29
+ $el.textContent = tag.textContent;
30
+ else if (tag.innerHTML && tag.innerHTML !== $el.innerHTML)
31
+ $el.innerHTML = tag.innerHTML;
32
+ }
60
33
  };
61
34
 
62
- function hashCode(s) {
63
- let h = 9;
64
- for (let i = 0; i < s.length; )
65
- h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
66
- return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
67
- }
68
-
35
+ let prevHash = false;
69
36
  async function renderDOMHead(head, options = {}) {
70
- const ctx = { shouldRender: true };
71
- await head.hooks.callHook("dom:beforeRender", ctx);
72
- if (!ctx.shouldRender)
37
+ const beforeRenderCtx = { shouldRender: true };
38
+ await head.hooks.callHook("dom:beforeRender", beforeRenderCtx);
39
+ if (!beforeRenderCtx.shouldRender)
73
40
  return;
74
- const dom = options.document || window.document;
41
+ const dom = options.document || head.resolvedOptions.document || window.document;
42
+ const tagContexts = (await head.resolveTags()).map(setupTagRenderCtx);
43
+ if (head.resolvedOptions.experimentalHashHydration) {
44
+ prevHash = prevHash || head._hash || false;
45
+ if (prevHash) {
46
+ const hash = computeHashes(tagContexts.map((ctx) => ctx.tag._h));
47
+ if (prevHash === hash)
48
+ return;
49
+ prevHash = hash;
50
+ }
51
+ }
75
52
  const staleSideEffects = head._popSideEffectQueue();
76
53
  head.headEntries().map((entry) => entry._sde).forEach((sde) => {
77
54
  Object.entries(sde).forEach(([key, fn]) => {
78
55
  staleSideEffects[key] = fn;
79
56
  });
80
57
  });
81
- const preRenderTag = async (tag) => {
58
+ const markSideEffect = (ctx, key, fn) => {
59
+ key = `${ctx.renderId}:${key}`;
60
+ if (ctx.entry)
61
+ ctx.entry._sde[key] = fn;
62
+ delete staleSideEffects[key];
63
+ };
64
+ function setupTagRenderCtx(tag) {
82
65
  const entry = head.headEntries().find((e) => e._i === tag._e);
83
66
  const renderCtx = {
84
- renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
67
+ renderId: tag._d || hashTag(tag),
85
68
  $el: null,
86
69
  shouldRender: true,
87
70
  tag,
88
71
  entry,
89
- staleSideEffects
72
+ markSideEffect: (key, fn) => markSideEffect(renderCtx, key, fn)
90
73
  };
91
- await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
92
74
  return renderCtx;
93
- };
75
+ }
94
76
  const renders = [];
95
77
  const pendingRenders = {
96
78
  body: [],
97
79
  head: []
98
80
  };
99
- const markSideEffect = (ctx2, key, fn) => {
100
- key = `${ctx2.renderId}:${key}`;
101
- if (ctx2.entry)
102
- ctx2.entry._sde[key] = fn;
103
- delete staleSideEffects[key];
104
- };
105
- const markEl = (ctx2) => {
106
- head._elMap[ctx2.renderId] = ctx2.$el;
107
- renders.push(ctx2);
108
- markSideEffect(ctx2, "el", () => {
109
- ctx2.$el?.remove();
110
- delete head._elMap[ctx2.renderId];
81
+ const markEl = (ctx) => {
82
+ head._elMap[ctx.renderId] = ctx.$el;
83
+ renders.push(ctx);
84
+ markSideEffect(ctx, "el", () => {
85
+ ctx.$el?.remove();
86
+ delete head._elMap[ctx.renderId];
111
87
  });
112
88
  };
113
- for (const t of await head.resolveTags()) {
114
- const ctx2 = await preRenderTag(t);
115
- if (!ctx2.shouldRender)
89
+ for (const ctx of tagContexts) {
90
+ await head.hooks.callHook("dom:beforeRenderTag", ctx);
91
+ if (!ctx.shouldRender)
116
92
  continue;
117
- const { tag } = ctx2;
93
+ const { tag } = ctx;
118
94
  if (tag.tag === "title") {
119
- dom.title = tag.children || "";
120
- renders.push(ctx2);
95
+ dom.title = tag.textContent || "";
96
+ renders.push(ctx);
121
97
  continue;
122
98
  }
123
99
  if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
124
- ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
125
- setAttrs(ctx2, markSideEffect);
126
- renders.push(ctx2);
100
+ ctx.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
101
+ setAttrs(ctx, false, markSideEffect);
102
+ renders.push(ctx);
127
103
  continue;
128
104
  }
129
- ctx2.$el = head._elMap[ctx2.renderId];
130
- if (!ctx2.$el && tag._hash) {
131
- ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
132
- }
133
- if (ctx2.$el) {
134
- if (ctx2.tag._d)
135
- setAttrs(ctx2);
136
- markEl(ctx2);
105
+ ctx.$el = head._elMap[ctx.renderId];
106
+ if (!ctx.$el && tag.key)
107
+ ctx.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._h}]`);
108
+ if (ctx.$el) {
109
+ if (ctx.tag._d)
110
+ setAttrs(ctx);
111
+ markEl(ctx);
137
112
  continue;
138
113
  }
139
- ctx2.$el = dom.createElement(tag.tag);
140
- setAttrs(ctx2);
141
- pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
114
+ pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx);
142
115
  }
116
+ const fragments = {
117
+ bodyClose: void 0,
118
+ bodyOpen: void 0,
119
+ head: void 0
120
+ };
143
121
  Object.entries(pendingRenders).forEach(([pos, queue]) => {
144
122
  if (!queue.length)
145
123
  return;
@@ -150,40 +128,43 @@ async function renderDOMHead(head, options = {}) {
150
128
  const elTag = $el.tagName.toLowerCase();
151
129
  if (!HasElementTags.includes(elTag))
152
130
  continue;
153
- const dedupeKey = tagDedupeKey({
154
- tag: elTag,
155
- // convert attributes to object
156
- props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
157
- });
158
- const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode?.(ctx2.$el)));
131
+ const props = $el.getAttributeNames().reduce((props2, name) => ({ ...props2, [name]: $el.getAttribute(name) }), {});
132
+ const tmpTag = { tag: elTag, props };
133
+ if ($el.innerHTML)
134
+ tmpTag.innerHTML = $el.innerHTML;
135
+ const tmpRenderId = hashTag(tmpTag);
136
+ let matchIdx = queue.findIndex((ctx) => ctx?.renderId === tmpRenderId);
137
+ if (matchIdx === -1) {
138
+ const tmpDedupeKey = tagDedupeKey(tmpTag);
139
+ matchIdx = queue.findIndex((ctx) => ctx?.tag._d && ctx.tag._d === tmpDedupeKey);
140
+ }
159
141
  if (matchIdx !== -1) {
160
- const ctx2 = queue[matchIdx];
161
- ctx2.$el = $el;
162
- setAttrs(ctx2);
163
- markEl(ctx2);
142
+ const ctx = queue[matchIdx];
143
+ ctx.$el = $el;
144
+ setAttrs(ctx);
145
+ markEl(ctx);
164
146
  delete queue[matchIdx];
165
147
  }
166
148
  }
167
- queue.forEach((ctx2) => {
168
- if (!ctx2.$el)
169
- return;
170
- switch (ctx2.tag.tagPosition) {
171
- case "bodyClose":
172
- dom.body.appendChild(ctx2.$el);
173
- break;
174
- case "bodyOpen":
175
- dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
176
- break;
177
- case "head":
178
- default:
179
- dom.head.appendChild(ctx2.$el);
180
- break;
149
+ queue.forEach((ctx) => {
150
+ const pos2 = ctx.tag.tagPosition || "head";
151
+ fragments[pos2] = fragments[pos2] || dom.createDocumentFragment();
152
+ if (!ctx.$el) {
153
+ ctx.$el = dom.createElement(ctx.tag.tag);
154
+ setAttrs(ctx, true);
181
155
  }
182
- markEl(ctx2);
156
+ fragments[pos2].appendChild(ctx.$el);
157
+ markEl(ctx);
183
158
  });
184
159
  });
185
- for (const ctx2 of renders)
186
- await head.hooks.callHook("dom:renderTag", ctx2);
160
+ if (fragments.head)
161
+ dom.head.appendChild(fragments.head);
162
+ if (fragments.bodyOpen)
163
+ dom.body.insertBefore(fragments.bodyOpen, dom.body.firstChild);
164
+ if (fragments.bodyClose)
165
+ dom.body.appendChild(fragments.bodyClose);
166
+ for (const ctx of renders)
167
+ await head.hooks.callHook("dom:renderTag", ctx);
187
168
  Object.values(staleSideEffects).forEach((fn) => fn());
188
169
  }
189
170
  let domUpdatePromise = null;
@@ -196,4 +177,23 @@ async function debouncedRenderDOMHead(head, options = {}) {
196
177
  return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
197
178
  }
198
179
 
199
- export { debouncedRenderDOMHead, domUpdatePromise, hashCode, renderDOMHead };
180
+ const PatchDomOnEntryUpdatesPlugin = (options) => {
181
+ return defineHeadPlugin({
182
+ hooks: {
183
+ "entries:updated": function(head) {
184
+ if (typeof options?.document === "undefined" && typeof window === "undefined")
185
+ return;
186
+ let delayFn = options?.delayFn;
187
+ if (!delayFn && typeof requestAnimationFrame !== "undefined")
188
+ delayFn = requestAnimationFrame;
189
+ debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
190
+ }
191
+ }
192
+ });
193
+ };
194
+
195
+ function maybeGetSSRHash(document) {
196
+ return document?.head.querySelector('meta[name="unhead:ssr"]')?.getAttribute("content") || false;
197
+ }
198
+
199
+ export { PatchDomOnEntryUpdatesPlugin, debouncedRenderDOMHead, domUpdatePromise, maybeGetSSRHash, renderDOMHead, setAttrs };
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@unhead/dom",
3
3
  "type": "module",
4
- "version": "1.0.21",
5
- "packageManager": "pnpm@7.25.1",
4
+ "version": "1.1.0",
5
+ "packageManager": "pnpm@7.27.1",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/harlan-zw",
9
- "homepage": "https://github.com/harlan-zw/unhead#readme",
9
+ "homepage": "https://unhead.harlanzw.com",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/harlan-zw/unhead.git",
12
+ "url": "git+https://github.com/unjs/unhead.git",
13
13
  "directory": "packages/dom"
14
14
  },
15
15
  "bugs": {
16
- "url": "https://github.com/harlan-zw/unhead/issues"
16
+ "url": "https://github.com/unjs/unhead/issues"
17
17
  },
18
18
  "sideEffects": false,
19
19
  "exports": {
@@ -30,10 +30,8 @@
30
30
  "dist"
31
31
  ],
32
32
  "dependencies": {
33
- "@unhead/schema": "1.0.21"
34
- },
35
- "devDependencies": {
36
- "zhead": "^1.1.0"
33
+ "@unhead/schema": "1.1.0",
34
+ "@unhead/shared": "1.1.0"
37
35
  },
38
36
  "scripts": {
39
37
  "build": "unbuild .",