Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1c25833

Browse files
committed
fix(unhead): deduplicate matching tags inside same render cycle
Tags without an explicit dedup key (like `script[src]` and `link[href]`) were keyed by their unique position index during tag resolution, so identical tags from separate entries were never deduplicated within the same render cycle. Now falls back to `hashTag()` (content-based hash) instead of position index, so matching tags collide and deduplicate correctly. The hash is precomputed during normalization and cached on `_h` to avoid recomputation in the DOM renderer. Closes #666
1 parent aaa7f3a commit 1c25833

6 files changed

Lines changed: 59 additions & 4 deletions

File tree

packages/unhead/src/client/renderDOMHead.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function _renderDOMHead<T extends Unhead<any>>(head: T, options: RenderDomHeadOp
125125
const dupeKeyCounter = new Map<string, number>()
126126
for (const tag of rawTags) {
127127
const count = dupeKeyCounter.get(tag._d!) || 0
128-
const id = (count ? `${tag._d}:${count}` : tag._d) || hashTag(tag)
128+
const id = (count ? `${tag._d}:${count}` : tag._d) || tag._h!
129129
const ctx = { tag, id, shouldRender: true } as DomRenderTagContext
130130
if (tag._d && isMetaArrayDupeKey(tag._d))
131131
dupeKeyCounter.set(tag._d, count + 1)

packages/unhead/src/utils/dedupe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@ export function dedupeKey<T extends HeadTag>(tag: T): string | undefined {
3737

3838
export function hashTag(tag: HeadTag) {
3939
return tag._h || tag._d || tag.textContent || tag.innerHTML
40-
|| `${tag.tag}:${Object.entries(tag.props).map(([k, v]) => `${k}:${v}`).join()}`
40+
|| `${tag.tag}:${Object.entries(tag.props).map(([k, v]) => `${k}:${String(v)}`).join()}`
4141
}

packages/unhead/src/utils/resolve.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { HeadTag, Unhead } from '../types'
22
import { UsesMergeStrategy, ValidHeadTags } from './const'
3-
import { dedupeKey, isMetaArrayDupeKey } from './dedupe'
3+
import { dedupeKey, hashTag, isMetaArrayDupeKey } from './dedupe'
44
import { callHook } from './hooks'
55
import { normalizeEntryToTags } from './normalize'
66

@@ -19,7 +19,7 @@ export interface ResolveTagsOptions {
1919
export function dedupeTags(ctx: ResolveTagsContext): boolean {
2020
let hasFlatMeta = false
2121
for (const next of ctx.tags.sort(sortTags)) {
22-
const k = String(next._d || next._p)
22+
const k = next._d || hashTag(next)
2323
const prev = ctx.tagMap.get(k)
2424
if (!prev) {
2525
ctx.tagMap.set(k, next)
@@ -111,6 +111,8 @@ export function resolveTags(head: Unhead<any>, options?: ResolveTagsOptions): He
111111
t._w = weightFn(t)
112112
t._p = (e._i << 10) + i
113113
t._d = dedupeKey(t)
114+
if (!t._d)
115+
t._h = hashTag(t)
114116
return t
115117
})
116118
}

packages/unhead/test/unit/client/resolveTags.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ describe('resolveTags', () => {
7171
},
7272
{
7373
"_d": undefined,
74+
"_h": "script:src:https://cdn.example.com/script.js",
7475
"_p": 1026,
7576
"_w": 100,
7677
"props": {
@@ -89,6 +90,7 @@ describe('resolveTags', () => {
8990
},
9091
{
9192
"_d": undefined,
93+
"_h": "link:rel:icon,type:image/x-icon,href:https://cdn.example.com/favicon.ico",
9294
"_p": 1028,
9395
"_w": 100,
9496
"props": {
@@ -123,6 +125,7 @@ describe('resolveTags', () => {
123125
[
124126
{
125127
"_d": undefined,
128+
"_h": "script:src:https://cdn.example.com/script2.js",
126129
"_p": 2048,
127130
"_w": 100,
128131
"props": {
@@ -175,6 +178,7 @@ describe('resolveTags', () => {
175178
},
176179
{
177180
"_d": undefined,
181+
"_h": "script:src:https://cdn.example.com/script2.js",
178182
"_p": 1026,
179183
"_w": 100,
180184
"props": {
@@ -193,6 +197,7 @@ describe('resolveTags', () => {
193197
},
194198
{
195199
"_d": undefined,
200+
"_h": "link:rel:icon,type:image/x-icon,href:https://cdn.example.com/favicon.ico",
196201
"_p": 1028,
197202
"_w": 100,
198203
"props": {

packages/unhead/test/unit/e2e/deduping.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,52 @@ describe('unhead e2e deduping', () => {
178178
179179
180180
181+
</body></html>"
182+
`)
183+
})
184+
185+
it('duplicate script src and link href across entries', async () => {
186+
const ssrHead = createClientHeadWithContext()
187+
ssrHead.push({
188+
script: [{ src: 'https://example.com/app.js', async: true }],
189+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
190+
})
191+
ssrHead.push({
192+
script: [{ src: 'https://example.com/app.js', async: true }],
193+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
194+
})
195+
196+
const data = renderSSRHead(ssrHead)
197+
expect(data.headTags).toMatchInlineSnapshot(`
198+
"<script src="https://example.com/app.js" async></script>
199+
<link rel="stylesheet" href="https://example.com/style.css">"
200+
`)
201+
202+
const dom = useDom(data)
203+
const csrHead = createClientHeadWithContext()
204+
csrHead.push({
205+
script: [{ src: 'https://example.com/app.js', async: true }],
206+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
207+
})
208+
csrHead.push({
209+
script: [{ src: 'https://example.com/app.js', async: true }],
210+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
211+
})
212+
renderDOMHead(csrHead, { document: dom.window.document })
213+
214+
expect(dom.serialize()).toMatchInlineSnapshot(`
215+
"<!DOCTYPE html><html><head>
216+
<script src="https://example.com/app.js" async=""></script>
217+
<link rel="stylesheet" href="https://example.com/style.css">
218+
</head>
219+
<body>
220+
221+
<div>
222+
<h1>hello world</h1>
223+
</div>
224+
225+
226+
181227
</body></html>"
182228
`)
183229
})

packages/unhead/test/unit/server/tagPriority.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('tag priority', () => {
2626
[
2727
{
2828
"_d": undefined,
29+
"_h": "script:src:/very-important-script.js",
2930
"_p": 1024,
3031
"_w": 42,
3132
"props": {
@@ -36,6 +37,7 @@ describe('tag priority', () => {
3637
},
3738
{
3839
"_d": undefined,
40+
"_h": "script:src:/not-important-script.js",
3941
"_p": 2048,
4042
"_w": 50,
4143
"props": {

0 commit comments

Comments
 (0)