Skip to content

fix(unhead): dispatch tag:normalise hook - #860

Closed
harlan-zw wants to merge 2 commits into
mainfrom
fix/tag-normalise-hook
Closed

fix(unhead): dispatch tag:normalise hook#860
harlan-zw wants to merge 2 commits into
mainfrom
fix/tag-normalise-hook

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Related to #822

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

tag:normalise is part of the public hook API, but resolveTags() never dispatched it. Dispatch the hook before weights and dedupe keys are calculated, then invalidate cached and precomputed tags when hook registrations change.

Summary by CodeRabbit

  • Bug Fixes
    • Improved tag:normalise handling so it runs before tag weighting and deduplication.
    • Mutations from tag:normalise now reliably update rendered headTags, including after the hook is registered post-render and for default/precomputed tags.
    • Tag resolution now better invalidates and recomputes cached tag results when normalisation hooks are present.
  • Tests
    • Added unit coverage for tag:normalise ordering, cache invalidation on late registration, and mutation of default-derived tags.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9be85c16-6fdb-49d9-89b7-5db3c19451ee

📥 Commits

Reviewing files that changed from the base of the PR and between 5d71c06 and cf70dcc.

📒 Files selected for processing (2)
  • packages/unhead/src/utils/resolve.ts
  • packages/unhead/test/unit/hooks/mutating-hooks.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/unhead/src/utils/resolve.ts
  • packages/unhead/test/unit/hooks/mutating-hooks.test.ts

📝 Walkthrough

Walkthrough

tag:normalise hooks now mutate tags before weighting and deduplication, invalidate cached resolutions when registered, and process precomputed default tags. Unit tests cover ordering, cache invalidation, and default-tag mutation.

Changes

Tag normalisation

Layer / File(s) Summary
Resolver normalisation and cache invalidation
packages/unhead/src/utils/resolve.ts
resolveTags applies tag:normalise to each tag before weighting and deduplication, snapshots entries when needed, bypasses the shared-tags fast path, and includes hook counts in cache invalidation.
Normalisation behavior coverage
packages/unhead/test/unit/hooks/mutating-hooks.test.ts
Tests verify mutation ordering, cache invalidation after hook registration, and normalisation of precomputed default tags.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • unjs/unhead#776: Adjusts defensive cloning and snapshot behavior for mutating hooks in the same resolver.
  • unjs/unhead#828: Introduces precomputed tags that interact with the updated normalisation fast path.
  • unjs/unhead#838: Refactors cloning and snapshotting in the same tag-resolution pipeline.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, conventional, and accurately describes the main change: dispatching the tag:normalise hook.
Description check ✅ Passed The description includes a linked issue, change type, and a clear summary of why the hook dispatch and cache invalidation were needed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tag-normalise-hook

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size

⚠️ 12 bundles grew · net +0.6 kB gz

Bundle Gzipped Δ
Client (Minimal) 5.3 kB → 5.4 kB 🔴 +53 B (+1.0%)
Client (Full) 8.6 kB → 8.7 kB 🔴 +55 B (+0.6%)
Client (Self-Contained) 5.6 kB → 5.7 kB 🔴 +57 B (+1.0%)
Server (Minimal) 4.8 kB → 4.9 kB 🔴 +54 B (+1.1%)
Server (Self-Contained) 5.2 kB → 5.2 kB 🔴 +58 B (+1.1%)
Vue Client (Minimal) 5.8 kB → 5.9 kB 🔴 +55 B (+0.9%)
Vue Client (Full) 9.4 kB → 9.5 kB 🔴 +59 B (+0.6%)
Vue Server (Minimal) 5.3 kB → 5.4 kB 🔴 +51 B (+0.9%)
React Client (Minimal) 5.7 kB → 5.8 kB 🔴 +57 B (+1.0%)
React Client (Full) 9.5 kB → 9.6 kB 🔴 +52 B (+0.5%)
React Server (Minimal) 5.1 kB → 5.2 kB 🔴 +57 B (+1.1%)
Schema.org (Minimal) 9.9 kB → 9.9 kB 🔴 +52 B (+0.5%)
All bundles (14)
Bundle Gzipped Brotli Raw
Core
Client (Minimal) 5.4 kB 4.9 kB 13.5 kB 🔴
Client (Full) 8.7 kB 7.9 kB 22.7 kB 🔴
Client (Self-Contained) 5.7 kB 5.1 kB 14.3 kB 🔴
Server (Minimal) 4.9 kB 4.4 kB 12.3 kB 🔴
Server (Self-Contained) 5.2 kB 4.7 kB 13 kB 🔴
Vue
Vue Client (Minimal) 5.9 kB 5.3 kB 14.6 kB 🔴
Vue Client (Full) 9.5 kB 8.6 kB 24.7 kB 🔴
Vue Server (Minimal) 5.4 kB 4.9 kB 13.3 kB 🔴
React
React Client (Minimal) 5.8 kB 5.2 kB 14.6 kB 🔴
React Client (Full) 9.6 kB 8.7 kB 25.2 kB 🔴
React Server (Minimal) 5.2 kB 4.7 kB 13 kB 🔴
Schema.org
Schema.org (Minimal) 9.9 kB 9 kB 27.4 kB 🔴
Schema.org Imports 0.1 kB 0.1 kB 0.1 kB
Schema.org Vue Meta 0.4 kB 0.3 kB 0.8 kB

⚡ Performance (directional)

No significant change (within CI noise)

All benchmarks (14)
Benchmark PR Δ RME
SSR render (CPU) 0.362 ms ~ noise ±9.7%
SSR render (wall) 0.264 ms ~ noise ±4.8%
SSR allocated / render 243.7 KiB ~ noise ±5.0%
Schema.org cached render (CPU) 0.334 ms ~ noise ±7.1%
Schema.org cached render (wall) 0.249 ms ~ noise ±2.6%
Schema.org cached allocated / render 147.2 KiB ~ noise ±0.1%
Streaming wrapStream drain (CPU) 0.240 ms ~ noise ±4.7%
Streaming wrapStream drain (wall) 0.154 ms ~ noise ±4.7%
Streaming allocated / drain 149.4 KiB ~ noise ±0.4%
Streaming suspense chunk (CPU) 0.011 ms ~ noise ±3.4%
Streaming allocated / suspense chunk 4.6 KiB ~ noise ±0.6%
CSR DOM mutations / nav 38 ~ noise
CSR re-render (CPU) 0.809 ms ~ noise ±4.6%
CSR re-render (wall) 0.460 ms ~ noise ±2.2%

Baseline: main @ 3c4f3c0 · 2026-07-19 · gzipped is the headline size metric · perf is directional (shared-runner, gated)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/unhead/src/utils/resolve.ts (1)

27-29: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider strengthening the cache invalidation hash.

Currently, the cache count is determined by a simple sum of the lengths of the registered hooks. If a listener is added to one hook and another listener is simultaneously removed from a different hook, the sum remains identical, which would fail to invalidate the cache.

While this is likely a rare edge case and follows the pre-existing pattern, you can eliminate this collision risk by using a bit-shift offset for each hook type to ensure a unique signature.

💡 Proposed refactor to use bit-shifting
-  const count = (hooks['entries:resolve']?.length || 0)
-    + (hooks['entries:normalize']?.length || 0)
-    + (hooks['tag:normalise']?.length || 0)
+  const count = (hooks['entries:resolve']?.length || 0)
+    + ((hooks['entries:normalize']?.length || 0) << 10)
+    + ((hooks['tag:normalise']?.length || 0) << 20)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/unhead/src/utils/resolve.ts` around lines 27 - 29, Strengthen the
cache signature calculation in the hook-count logic by assigning each hook type
a distinct bit-shifted offset before combining their listener counts. Update the
`count` expression for `entries:resolve`, `entries:normalize`, and
`tag:normalise` so changes between hook groups cannot produce the same
signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/unhead/src/utils/resolve.ts`:
- Around line 27-29: Strengthen the cache signature calculation in the
hook-count logic by assigning each hook type a distinct bit-shifted offset
before combining their listener counts. Update the `count` expression for
`entries:resolve`, `entries:normalize`, and `tag:normalise` so changes between
hook groups cannot produce the same signature.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 732fffeb-bf4d-41f2-840a-7e1128917b14

📥 Commits

Reviewing files that changed from the base of the PR and between 3c4f3c0 and 5d71c06.

📒 Files selected for processing (2)
  • packages/unhead/src/utils/resolve.ts
  • packages/unhead/test/unit/hooks/mutating-hooks.test.ts

@harlan-zw

Copy link
Copy Markdown
Collaborator Author

Closing after checking the v4 migration. tag:normalise is intentionally removed there because it was never called; ordered tags:resolve is the supported replacement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant