refactor(bundler)!: named Unhead export, ctx-based transforms, dev-mode validate#733
Conversation
…e validate
Foundational changes that landed alongside the in-progress devtools work.
Splits the bundler refactor + DX improvements out from the devtools branch
so they can be reviewed and merged independently.
## Bundler
- BREAKING: `default` export → named `Unhead` export on `@unhead/bundler/{vite,webpack}`
and on every framework wrapper (`@unhead/{react,solid-js,svelte,vue}/vite`).
Existing imports must change from `import unhead from ...` to
`import { Unhead } from ...`.
- New `VitePluginOptions` interface (extends `UnpluginOptions`) with a
`validate` flag that injects `ValidatePlugin` in dev so head-tag warnings
surface in the console without manual setup. Internal `_framework` field
lets framework wrappers identify themselves to the runtime.
- New `CreateHeadTransform` + `createHeadTransformContext` foundation: a
single transform handles `createHead()` wrapping and lets other plugins
register runtime plugins via shared context.
- New `SSRStaticReplace` transform: replaces `head.ssr` with a static
boolean per environment so the dead branch tree-shakes cleanly.
- `UseSeoMetaTransform` now preserves the second argument (e.g.
`useSeoMeta(meta, { head })`) when rewriting to `useHead`.
- Tests: `createHeadTransform.test.ts` and updated `useSeoMetaTransform.test.ts`.
## Framework wrappers
- Vue / React / Solid / Svelte vite plugins updated for the named export
and forward `_framework` so the bundler can resolve framework-scoped
runtime plugins (`@unhead/vue/plugins`, etc.).
- `packages-aliased/addons` deprecation shim re-exports the new named export.
## Vue
- Re-exports `useServerHead`, `useServerHeadSafe`, `useServerSeoMeta` as
deprecated aliases for the non-`Server` variants (v2 compat).
- Re-exports `resolveUnrefHeadInput` from `utils` (v2 compat).
## Schema.org
- Exports `schemaAutoImports` from the package root so consumers can wire
it into `unplugin-auto-import` without reaching into subpaths.
## Docs
- Reorganises `1.guides/2.advanced/{vite-plugin,client-only-tags,extending-unhead}`
into a new `1.guides/build-plugins/` section: overview, tree-shaking,
seo-meta-transform, minify-transform.
- Migration guide moves out of `content/` to top-level `6.migration-guide/`.
- Adds `7.api/plugins.md` and updates `use-head` / `use-seo-meta` API pages.
## Examples
- `vite-ssr-vue-prerender`: switches to the named `Unhead` import.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR unifies and renames build plugins to a named Changes
Sequence Diagram(s)sequenceDiagram
participant DevFile as Source File
participant Vite as Vite Dev Server/Build
participant UnheadPlugin as Unhead() (bundler plugin)
participant Transform as CreateHeadTransform
participant SSRReplace as SSRStaticReplace
participant Runtime as Head Runtime
DevFile->>Vite: file request / build
Vite->>UnheadPlugin: apply plugin pipeline (dev or build)
UnheadPlugin->>Transform: parse file, find createHead()
Transform->>UnheadPlugin: inject imports + IIFE registrations (env-filtered, __ROOT__ substituted)
UnheadPlugin->>SSRReplace: replace `head.ssr` tokens for SSR builds
UnheadPlugin->>Vite: return transformed code + sourcemap
Vite->>Runtime: serve transformed code (dev) or bundle (prod)
Runtime->>Head Runtime: runtime plugin registrations invoked at createHead call
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Bundle Size Analysis
|
Two [email protected] instances were resolved due to a peer-dep variation on the bundler package's optional `esbuild` peer, causing TS to see two incompatible Plugin types in the new bundler transforms. `pnpm update && pnpm dedupe` collapses them onto a single resolution and bumps vitest to 4.1.4.
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/head/7.api/composables/3.use-seo-meta.md (1)
27-28:⚠️ Potential issue | 🟠 MajorInvalid package name in documentation examples.
Multiple examples import from
@unhead/dynamic-import, but this package does not exist in the repository. Based on the package exports, this should be the framework-specific package or the baseunheadpackage.For framework-agnostic examples, use
unhead. For framework-specific examples (Vue/React/Solid), use the respective framework package like@unhead/vue,@unhead/react, or@unhead/solid-js.📝 Suggested fixes for each section
Lines 27, 44 (Basic Usage, Complete SEO Setup):
-import { useSeoMeta } from '@unhead/dynamic-import' +import { useSeoMeta } from 'unhead'Line 78 (Vue example):
-import { useSeoMeta } from '@unhead/dynamic-import' +import { useSeoMeta } from '@unhead/vue'Line 96 (React example):
-import { useSeoMeta } from '@unhead/dynamic-import' +import { useSeoMeta } from '@unhead/react'Line 118 (Solid example):
-import { useSeoMeta } from '@unhead/dynamic-import' +import { useSeoMeta } from '@unhead/solid-js'Also applies to: 44-44, 78-78, 96-96, 118-118
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/head/7.api/composables/3.use-seo-meta.md` around lines 27 - 28, The docs import is using a non-existent package; update the import sources for useSeoMeta so examples import from the correct package: for framework-agnostic examples import useSeoMeta from "unhead", and for framework-specific examples import useSeoMeta from "@unhead/vue", "@unhead/react", or "@unhead/solid-js" respectively (apply this change to the instances showing useSeoMeta at the Basic Usage / Complete SEO Setup and the Vue/React/Solid example sections).docs/0.angular/schema-org/guides/get-started/0.installation.md (1)
138-138:⚠️ Potential issue | 🟡 MinorDocumentation text refers to "Nuxt" in Angular guide.
Line 138 says "Your Nuxt app is now serving basic Schema.org" but this is the Angular Schema.org installation guide. This appears to be a copy-paste error.
📝 Suggested fix
-Your Nuxt app is now serving basic Schema.org, congrats! 🎉 +Your Angular app is now serving basic Schema.org, congrats! 🎉🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/0.angular/schema-org/guides/get-started/0.installation.md` at line 138, The documentation line containing the phrase "Your Nuxt app is now serving basic Schema.org" is incorrect for the Angular guide; update that sentence to reference Angular (e.g., "Your Angular app is now serving basic Schema.org") so the guide consistently refers to Angular. Locate the exact string "Your Nuxt app is now serving basic Schema.org" in the get-started installation content and replace "Nuxt" with "Angular" (or reword to "Your Angular app") to correct the copy-paste error.packages/bundler/src/unplugin/UseSeoMetaTransform.ts (1)
48-48:⚠️ Potential issue | 🟡 MinorBug:
imports: falseoption is not honored.The expression
options.imports || truewill always evaluate totruewhenimportsis explicitly set tofalse. Use nullish coalescing to respect explicitfalsevalues.🐛 Proposed fix
- options.imports = options.imports || true + options.imports = options.imports ?? true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/bundler/src/unplugin/UseSeoMetaTransform.ts` at line 48, The options.imports assignment currently uses truthy fallback which ignores an explicit false; update the assignment in UseSeoMetaTransform (where options.imports is set) to use nullish coalescing so explicit false is honored (e.g., replace "options.imports = options.imports || true" with "options.imports = options.imports ?? true" or equivalent) and ensure the rest of the code treats false as a valid value.
♻️ Duplicate comments (1)
docs/head/1.guides/plugins/infer-seo-meta-tags.md (1)
101-101:⚠️ Potential issue | 🔴 CriticalDuplicate: Incorrect import path for InferSeoMetaPlugin.
Same issue as line 48 - the import path should be
unhead/pluginsinstead of@unhead/dynamic-import/plugins.📦 Proposed fix
-import { InferSeoMetaPlugin } from '@unhead/dynamic-import/plugins' +import { InferSeoMetaPlugin } from 'unhead/plugins'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/head/1.guides/plugins/infer-seo-meta-tags.md` at line 101, The import for InferSeoMetaPlugin is using the wrong module path; replace the import statement that references '@unhead/dynamic-import/plugins' with the correct 'unhead/plugins' path so that the symbol InferSeoMetaPlugin is imported from unhead/plugins (this is a duplicate of the earlier incorrect import near line 48, so update both occurrences).
🧹 Nitpick comments (2)
docs/head/7.api/plugins.md (1)
6-6: Consider hyphenating "hooks-based" for grammatical correctness."Hooks based architecture" should be "hooks-based architecture" when used as a compound modifier before a noun.
✏️ Suggested fix
-Unhead uses a hooks based architecture powered by [unjs/hookable](https://github.com/unjs/hookable). Plugins let you tap into different parts of the head tag management lifecycle. +Unhead uses a hooks-based architecture powered by [unjs/hookable](https://github.com/unjs/hookable). Plugins let you tap into different parts of the head tag management lifecycle.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/head/7.api/plugins.md` at line 6, Update the phrasing "hooks based architecture" to the hyphenated form "hooks-based architecture" in the Unhead description so the compound modifier is grammatically correct; locate the sentence starting with "Unhead uses a hooks based architecture" and replace it with "Unhead uses a hooks-based architecture" (preserving the rest of the sentence and the reference to unjs/hookable).packages/bundler/src/unplugin/CreateHeadTransform.ts (1)
93-95: Deduplicate injected imports before prepending.If two registrations inject the same import alias/source pair, Line 93–Line 95 will emit duplicate import statements, which can produce invalid output. A small dedupe step would make this safer.
Proposed refactor
- for (const reg of envRegistrations) { - s.prepend(`import { ${reg.import.name} as ${reg.import.as} } from '${reg.import.source}';\n`) - } + const importStmts = new Set( + envRegistrations.map( + reg => `import { ${reg.import.name} as ${reg.import.as} } from '${reg.import.source}';`, + ), + ) + if (importStmts.size) + s.prepend(`${[...importStmts].join('\n')}\n`)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/bundler/src/unplugin/CreateHeadTransform.ts` around lines 93 - 95, The loop that calls s.prepend for each entry in envRegistrations can emit duplicate import lines; before the for-loop dedupe entries by their import alias and source (use reg.import.as and reg.import.source as the uniqueness key), collect unique registrations, then iterate that deduped list to call s.prepend so each import statement is emitted only once; update the code referencing envRegistrations, reg.import.name, reg.import.as and reg.import.source accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/head/1.guides/build-plugins/0.overview.md`:
- Around line 87-95: The docs list a devtools option but the plugin types lack
it; either remove the `devtools` row and the related bulleted reference in the
overview documentation, or add `devtools` to the plugin API by extending the
VitePluginOptions interface with a `devtools?: boolean | object` (or appropriate
type) and update the plugin implementation to read and honor this option
(references: VitePluginOptions and the plugin's option-handling logic).
In `@docs/head/1.guides/build-plugins/1.tree-shaking.md`:
- Around line 63-73: The example uses the old default name unhead but the
library now exports the named constructor Unhead; update the snippet to call
Unhead({...}) instead of unhead({...}) so the example matches the new named
export (reference the Unhead function in the docs snippet and replace any
occurrences of unhead with Unhead).
In `@docs/head/1.guides/build-plugins/2.seo-meta-transform.md`:
- Around line 59-73: The example uses the old lowercase factory name `unhead`;
update it to the new named export `Unhead` so the snippet matches the PR's named
export convention—replace the `unhead({...})` invocation with `Unhead({...})` in
the `transformSeoMeta` example and ensure the rest of the options (`imports`,
`importPaths`, `filter`) remain unchanged.
In `@docs/head/1.guides/build-plugins/3.minify-transform.md`:
- Around line 71-85: The example uses the wrong function name casing: change the
call from unhead({...}) to the named export Unhead({...}) so the code example
matches the current API (replace the identifier "unhead" with "Unhead" in the
minify example block).
In `@docs/head/1.guides/plugins/6.template-params.md`:
- Around line 20-21: The docs import the non-existent package
'@unhead/dynamic-import'; replace it by importing TemplateParamsPlugin from
'unhead/plugins' and import createHead from the appropriate entry for the
intended runtime: use 'unhead/server' if this guide is server-side or
'unhead/client' (or framework-specific client like 'unhead/vue/client' or
'unhead/react/client') if it’s client-side; update the example imports for
createHead and TemplateParamsPlugin (referencing createHead and
TemplateParamsPlugin) to use those real package paths so the snippet matches the
target runtime.
In `@docs/head/1.guides/plugins/canonical.md`:
- Line 57: The doc examples use two different import paths for CanonicalPlugin;
standardize them to the actual plugin location by replacing imports like "import
{ CanonicalPlugin } from '@unhead/dynamic-import/plugins'" with "import {
CanonicalPlugin } from 'unhead/plugins'" wherever CanonicalPlugin is referenced
(e.g., the examples around the current imports and later examples), ensuring all
occurrences use the single canonical path "unhead/plugins".
In `@docs/head/1.guides/plugins/infer-seo-meta-tags.md`:
- Line 48: The documentation import for InferSeoMetaPlugin is wrong; update the
import statement that currently references `@unhead/dynamic-import/plugins` to
import InferSeoMetaPlugin from unhead/plugins instead so the symbol
InferSeoMetaPlugin resolves correctly in examples and avoids import errors for
users.
In `@docs/head/1.guides/plugins/minify.md`:
- Line 25: The import paths in the docs are wrong: replace any imports
referencing '@unhead/dynamic-import/plugins' and '@unhead/dynamic-import/minify'
with the correct unscoped package imports (e.g., change the import that brings
in MinifyPlugin from '@unhead/dynamic-import/plugins' to import it from
'unhead/plugins', and similarly change any imports for minify utilities from
'@unhead/dynamic-import/minify' to 'unhead/minify'); update all occurrences in
this guide so symbols like MinifyPlugin and any minify-related identifiers
import from 'unhead' subpaths rather than the non-existent scoped package.
In `@docs/head/1.guides/plugins/validate.md`:
- Line 26: The import path for ValidatePlugin is incorrect — update the import
statement that currently references ValidatePlugin from
'@unhead/dynamic-import/plugins' to import ValidatePlugin from 'unhead/plugins';
search for other occurrences of '@unhead/dynamic-import/*' in docs and replace
them with the corresponding 'unhead/*' plugin imports to avoid "package not
found" errors.
In `@docs/head/7.api/composables/0.use-head.md`:
- Line 370: Update the TypeScript example imports that currently reference the
non-existent path '@unhead/dynamic-import/types' so they import the types from
'unhead/types' instead; specifically change the imports for GenericLink and
GenericScript to use 'unhead/types' (replace the import source for the
GenericLink and GenericScript type imports).
In `@docs/head/7.api/plugins.md`:
- Around line 30-38: The example incorrectly calls head.resolveTags(); change it
to use the utility function resolveTags(head) instead: update the hook in
defineHeadPlugin (the 'entries:updated' handler) to call resolveTags(head) and
ensure you add the import for resolveTags from 'unhead/utils'; remove the
incorrect head.resolveTags() usage and replace it with resolveTags(head) so the
example matches the actual API.
In `@packages/bundler/src/unplugin/CreateHeadTransform.ts`:
- Around line 71-77: The current matching uses only the callee name and causes
false positives; update CreateHeadTransform.ts so before using HEAD_FACTORIES
you verify the callee actually refers to an import from Unhead: when callee is
an Identifier, resolve its binding via path.scope.getBinding(callee.name) and
ensure the binding.path is an ImportSpecifier/ImportDefaultSpecifier whose
parent.source.value matches Unhead module names; when callee is a
MemberExpression, ensure the object is an Identifier bound to an
ImportNamespaceSpecifier from an Unhead source and the property.name matches a
HEAD_FACTORIES entry; only proceed with rewriting/injecting imports/plugins if
the binding origin confirms the symbol comes from Unhead.
In `@packages/bundler/src/unplugin/SSRStaticReplace.ts`:
- Around line 43-48: The vite plugin's apply hook currently returns false which
disables the plugin; update the vite.apply implementation so that when
env.isSsrBuild sets ssr = true the hook returns true (not false) to allow the
plugin to run during SSR builds; locate the vite: { apply(_config: UserConfig,
env: ConfigEnv) { ... } } block and change the return value so the plugin is
enabled (preserve the ssr side effect on the ssr variable).
---
Outside diff comments:
In `@docs/0.angular/schema-org/guides/get-started/0.installation.md`:
- Line 138: The documentation line containing the phrase "Your Nuxt app is now
serving basic Schema.org" is incorrect for the Angular guide; update that
sentence to reference Angular (e.g., "Your Angular app is now serving basic
Schema.org") so the guide consistently refers to Angular. Locate the exact
string "Your Nuxt app is now serving basic Schema.org" in the get-started
installation content and replace "Nuxt" with "Angular" (or reword to "Your
Angular app") to correct the copy-paste error.
In `@docs/head/7.api/composables/3.use-seo-meta.md`:
- Around line 27-28: The docs import is using a non-existent package; update the
import sources for useSeoMeta so examples import from the correct package: for
framework-agnostic examples import useSeoMeta from "unhead", and for
framework-specific examples import useSeoMeta from "@unhead/vue",
"@unhead/react", or "@unhead/solid-js" respectively (apply this change to the
instances showing useSeoMeta at the Basic Usage / Complete SEO Setup and the
Vue/React/Solid example sections).
In `@packages/bundler/src/unplugin/UseSeoMetaTransform.ts`:
- Line 48: The options.imports assignment currently uses truthy fallback which
ignores an explicit false; update the assignment in UseSeoMetaTransform (where
options.imports is set) to use nullish coalescing so explicit false is honored
(e.g., replace "options.imports = options.imports || true" with "options.imports
= options.imports ?? true" or equivalent) and ensure the rest of the code treats
false as a valid value.
---
Duplicate comments:
In `@docs/head/1.guides/plugins/infer-seo-meta-tags.md`:
- Line 101: The import for InferSeoMetaPlugin is using the wrong module path;
replace the import statement that references '@unhead/dynamic-import/plugins'
with the correct 'unhead/plugins' path so that the symbol InferSeoMetaPlugin is
imported from unhead/plugins (this is a duplicate of the earlier incorrect
import near line 48, so update both occurrences).
---
Nitpick comments:
In `@docs/head/7.api/plugins.md`:
- Line 6: Update the phrasing "hooks based architecture" to the hyphenated form
"hooks-based architecture" in the Unhead description so the compound modifier is
grammatically correct; locate the sentence starting with "Unhead uses a hooks
based architecture" and replace it with "Unhead uses a hooks-based architecture"
(preserving the rest of the sentence and the reference to unjs/hookable).
In `@packages/bundler/src/unplugin/CreateHeadTransform.ts`:
- Around line 93-95: The loop that calls s.prepend for each entry in
envRegistrations can emit duplicate import lines; before the for-loop dedupe
entries by their import alias and source (use reg.import.as and
reg.import.source as the uniqueness key), collect unique registrations, then
iterate that deduped list to call s.prepend so each import statement is emitted
only once; update the code referencing envRegistrations, reg.import.name,
reg.import.as and reg.import.source accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5be842d5-b965-4988-a321-6adff79b8527
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (60)
docs/0.angular/schema-org/guides/get-started/0.installation.mddocs/0.react/schema-org/guides/get-started/0.installation.mddocs/0.solid-js/schema-org/guides/get-started/0.installation.mddocs/0.svelte/schema-org/guides/get-started/0.installation.mddocs/0.typescript/head/guides/0.get-started/1.installation.mddocs/0.typescript/schema-org/guides/get-started/0.installation.mddocs/0.vue/schema-org/guides/0.get-started/0.installation.mddocs/6.migration-guide/.navigation.ymldocs/6.migration-guide/1.v3.mddocs/6.migration-guide/2.v2.mddocs/content/6.migration-guide/1.v3.mddocs/content/7.releases/1.v3.mddocs/content/7.releases/2.v2.mddocs/head/1.guides/0.get-started/0.overview.mddocs/head/1.guides/1.core-concepts/1.titles.mddocs/head/1.guides/1.core-concepts/2.positions.mddocs/head/1.guides/1.core-concepts/3.class-attr.mddocs/head/1.guides/2.advanced/11.extending-unhead.mddocs/head/1.guides/2.advanced/7.client-only-tags.mddocs/head/1.guides/2.advanced/9.vite-plugin.mddocs/head/1.guides/build-plugins/0.overview.mddocs/head/1.guides/build-plugins/1.tree-shaking.mddocs/head/1.guides/build-plugins/2.seo-meta-transform.mddocs/head/1.guides/build-plugins/3.minify-transform.mddocs/head/1.guides/plugins/6.template-params.mddocs/head/1.guides/plugins/alias-sorting.mddocs/head/1.guides/plugins/canonical.mddocs/head/1.guides/plugins/infer-seo-meta-tags.mddocs/head/1.guides/plugins/minify.mddocs/head/1.guides/plugins/validate.mddocs/head/7.api/0.get-started/overview.mddocs/head/7.api/composables/0.use-head.mddocs/head/7.api/composables/3.use-seo-meta.mddocs/head/7.api/plugins.mdexamples/vite-ssr-vue-prerender/vite.config.jspackages-aliased/addons/src/vite.tspackages/bundler/README.mdpackages/bundler/src/unplugin/CreateHeadTransform.tspackages/bundler/src/unplugin/SSRStaticReplace.tspackages/bundler/src/unplugin/UseSeoMetaTransform.tspackages/bundler/src/unplugin/types.tspackages/bundler/src/unplugin/vite.tspackages/bundler/src/unplugin/webpack.tspackages/bundler/test/createHeadTransform.test.tspackages/bundler/test/useSeoMetaTransform.test.tspackages/react/src/composables.tspackages/react/src/vite.tspackages/schema-org/src/index.tspackages/solid-js/src/vite.tspackages/svelte/src/vite.tspackages/vue/src/composables.tspackages/vue/src/index.tspackages/vue/src/utils.tspackages/vue/src/vite.tstest/exports/bundler.yamltest/exports/react.yamltest/exports/schema-org.yamltest/exports/solid-js.yamltest/exports/svelte.yamltest/exports/vue.yaml
💤 Files with no reviewable changes (6)
- docs/head/1.guides/2.advanced/7.client-only-tags.md
- docs/content/7.releases/2.v2.md
- docs/head/1.guides/2.advanced/11.extending-unhead.md
- docs/content/7.releases/1.v3.md
- docs/content/6.migration-guide/1.v3.md
- docs/head/1.guides/2.advanced/9.vite-plugin.md
| import { createHead } from '@unhead/dynamic-import' | ||
| import { TemplateParamsPlugin } from '@unhead/dynamic-import/plugins' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that `@unhead/dynamic-import` package does not exist and check correct export paths
echo "=== Checking for `@unhead/dynamic-import` package ==="
fd -t d "dynamic-import" packages/
echo "=== Checking unhead package exports ==="
cat packages/unhead/package.json | jq '.exports'
echo "=== Checking if TemplateParamsPlugin is exported from unhead/plugins ==="
rg -n "TemplateParamsPlugin" packages/unhead/src/plugins/index.tsRepository: unjs/unhead
Length of output: 1539
🏁 Script executed:
rg -n "export.*createHead" packages/unhead/src/index.ts | head -20Repository: unjs/unhead
Length of output: 37
🏁 Script executed:
rg -n "createHead" packages/unhead/src/ | head -30Repository: unjs/unhead
Length of output: 2182
🏁 Script executed:
cat -n packages/unhead/src/index.ts | head -50Repository: unjs/unhead
Length of output: 184
🏁 Script executed:
cat -n docs/head/1.guides/plugins/6.template-params.md | head -50Repository: unjs/unhead
Length of output: 1964
🏁 Script executed:
# Check if createHead is exported from the main unhead package export
rg -A5 -B5 "export.*createHead" packages/unhead/src/index.tsRepository: unjs/unhead
Length of output: 37
🏁 Script executed:
# Let's verify what is actually exported from the main unhead package
rg "export" packages/unhead/src/index.ts
# And check what createHead variants exist
rg "export.*function createHead" packages/unhead/src/Repository: unjs/unhead
Length of output: 462
🏁 Script executed:
# Check if createHead is re-exported from main unhead or if it needs server/client imports
rg "createHead" docs/ -A2 -B2 | head -100Repository: unjs/unhead
Length of output: 5864
🏁 Script executed:
# Check what the API documentation actually recommends for createHead
cat -n docs/head/7.api/plugins.md | head -30Repository: unjs/unhead
Length of output: 1117
🏁 Script executed:
# Check what's exported from unhead/server and unhead/client
echo "=== unhead/server exports ==="
rg "export" packages/unhead/src/server/index.ts
echo "=== unhead/client exports ==="
rg "export" packages/unhead/src/client/index.tsRepository: unjs/unhead
Length of output: 939
🏁 Script executed:
# Let's check if there's a re-export or alias for createHead in the main unhead export
cat packages/unhead/src/unhead.ts | head -50Repository: unjs/unhead
Length of output: 1817
🏁 Script executed:
# Check if `@unhead/dynamic-import` is an alias or special export path in package.json
cat packages/unhead/package.json | jq '.exports[] | select(.includes("dynamic"))'
# Also check if there's documentation about which createHead to use
rg "@unhead/dynamic-import" docs/ -lRepository: unjs/unhead
Length of output: 1725
🏁 Script executed:
# Check if there's any documentation or comment about `@unhead/dynamic-import` being a docs-only alias
rg "dynamic-import" packages/ -l
# Also check if there's a README or migration guide that mentions this
rg "dynamic-import" . -t md -B3 -A3 | head -100Repository: unjs/unhead
Length of output: 6135
🏁 Script executed:
# Let me check if `@unhead/dynamic-import` appears to be a documentation convention/placeholder
# by looking at the context where it's used
# Also check what the actual position guide recommends
cat -n docs/head/1.guides/1.core-concepts/2.positions.md | grep -A10 -B5 "dynamic-import"Repository: unjs/unhead
Length of output: 3083
🏁 Script executed:
# Check what the correct imports should be based on the API documentation
# Let's verify if there's a consistent pattern in the documentation
# Check a few different plugin examples to see what imports they use
echo "=== Checking canonical.md imports ==="
grep -A2 "import.*createHead\|import.*Plugin" docs/head/1.guides/plugins/canonical.md | head -20
echo -e "\n=== Checking validate.md imports ==="
grep -A2 "import.*createHead\|import.*Plugin" docs/head/1.guides/plugins/validate.md | head -20
echo -e "\n=== Checking api/plugins.md imports ==="
grep -A2 "import.*createHead\|import.*Plugin" docs/head/7.api/plugins.md | head -20Repository: unjs/unhead
Length of output: 1108
🏁 Script executed:
# Check if createHead might be a re-export from main unhead package
# by looking at what's actually exported in the main index
rg "export.*createHead" packages/unhead/src/index.ts -A5
# Also, let me see what createUnhead does - could that be the main export?
cat -n packages/unhead/src/index.tsRepository: unjs/unhead
Length of output: 184
🏁 Script executed:
# Let's verify if there's any possibility of createHead being re-exported from unhead
# Check if there's a barrel export or if the main unhead uses a different module
# First, let's see if there's an index that re-exports everything
cat packages/unhead/package.json | jq '.exports | to_entries[] | select(.key == ".")'
# Then check what the actual export points to
cat packages/unhead/dist/index.mjs 2>/dev/null | head -50 || echo "No dist folder in sandbox"
# Let's also verify the actual behavior by checking if createHead exists in the source under different names
rg "export.*createHead\|export.*createUnhead" packages/unhead/src/index.tsRepository: unjs/unhead
Length of output: 157
Invalid package name in documentation.
The imports reference @unhead/dynamic-import and @unhead/dynamic-import/plugins, but this package does not exist in the repository. However, the suggested fix requires clarification.
TemplateParamsPlugin should be imported from unhead/plugins ✓, but createHead is not exported from the main unhead package. It's available from:
unhead/server(for server-side usage)unhead/client(for client-side usage)- Framework-specific packages like
unhead/vue/client,unhead/react/client, etc.
Determine the intended use case (server-side or client-side) and update both imports accordingly. For example, if this is for server-side usage:
📝 Suggested fix (server example)
-import { createHead } from '@unhead/dynamic-import'
-import { TemplateParamsPlugin } from '@unhead/dynamic-import/plugins'
+import { createHead } from 'unhead/server'
+import { TemplateParamsPlugin } from 'unhead/plugins'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createHead } from '@unhead/dynamic-import' | |
| import { TemplateParamsPlugin } from '@unhead/dynamic-import/plugins' | |
| import { createHead } from 'unhead/server' | |
| import { TemplateParamsPlugin } from 'unhead/plugins' |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/head/1.guides/plugins/6.template-params.md` around lines 20 - 21, The
docs import the non-existent package '@unhead/dynamic-import'; replace it by
importing TemplateParamsPlugin from 'unhead/plugins' and import createHead from
the appropriate entry for the intended runtime: use 'unhead/server' if this
guide is server-side or 'unhead/client' (or framework-specific client like
'unhead/vue/client' or 'unhead/react/client') if it’s client-side; update the
example imports for createHead and TemplateParamsPlugin (referencing createHead
and TemplateParamsPlugin) to use those real package paths so the snippet matches
the target runtime.
|
|
||
| ```ts [Input] | ||
| import { ValidatePlugin } from 'unhead/plugins' | ||
| import { ValidatePlugin } from '@unhead/dynamic-import/plugins' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find manifest for `@unhead/dynamic-import` and inspect exports for plugins/server entries.
fd -i "package.json" | while read -r manifest; do
if rg -n '"name"\s*:\s*"@unhead/dynamic-import"' "$manifest" >/dev/null; then
echo "== Found package manifest: $manifest =="
rg -n -C2 '"exports"|"\./plugins"|"\./server"' "$manifest"
fi
done
# 2) Confirm ValidatePlugin/AliasSortingPlugin are exported from plugin barrels.
rg -n -C2 'ValidatePlugin|AliasSortingPlugin' --type=ts --type=js
# 3) Confirm docs references to the new path.
rg -n -C1 '@unhead/dynamic-import/plugins' docsRepository: unjs/unhead
Length of output: 17487
🏁 Script executed:
#!/bin/bash
# Find all package.json files and check package names
find packages -name "package.json" -type f | head -20 | while read f; do
echo "=== $f ==="
rg '"name"' "$f" | head -3
done
# Check if there's a dynamic-import package
echo ""
echo "=== Searching for dynamic-import package ==="
rg -i "dynamic-import" packages --type=json | head -20Repository: unjs/unhead
Length of output: 649
🏁 Script executed:
#!/bin/bash
# Check the unhead package.json exports configuration
cat packages/unhead/package.json | grep -A 30 '"exports"'
# Also check if there's any TypeScript export/import setup
echo ""
echo "=== Checking where plugins are exported from ==="
rg "export.*from.*plugins" packages/unhead/src --type=ts -A2 -B2Repository: unjs/unhead
Length of output: 1228
🏁 Script executed:
#!/bin/bash
# Check `@unhead/vue` package exports
echo "=== `@unhead/vue` package.json exports ==="
cat packages/vue/package.json | grep -A 20 '"exports"'
# Verify what the docs should be importing
echo ""
echo "=== Checking other docs for import pattern ==="
rg '@unhead/(vue|react|angular)' docs/head/1.guides/plugins --type=md | head -10Repository: unjs/unhead
Length of output: 1107
🏁 Script executed:
#!/bin/bash
# Search for all references to dynamic-import in the entire codebase
echo "=== All references to dynamic-import ==="
rg "dynamic-import" --type=md --type=ts --type=json | head -30
# Check if there's any external reference or note about it
echo ""
echo "=== Checking docs README or guide index ==="
find docs -name "*.md" -type f | xargs grep -l "dynamic-import" 2>/dev/null | head -5
</parameter>Repository: unjs/unhead
Length of output: 3369
🌐 Web query:
@unhead/dynamic-import npm package
💡 Result:
I couldn’t find any evidence that @unhead/dynamic-import is (or has been) a published npm package. The Unhead project’s official docs and repo don’t reference it as an installable module. [1] [2] [3]
If you’re trying to dynamically load head-related features in the Unhead ecosystem, the supported packages are typically:
- Core:
unhead[3] - Framework adapters like
@unhead/angular(and similar@unhead/*packages) [4] - Add-ons via
@unhead/addons(e.g., Vite-related integrations in the docs) [1]
Sources:
[1] Unhead docs (unhead.unjs.io) (unhead.unjs.io)
[2] Unhead GitHub repo (unjs/unhead) (github.com)
[3] UnJS package page for unhead (unjs.io)
[4] Example official @unhead/* npm package (@unhead/angular) (npmjs.com)
Citations:
- 1: https://unhead.unjs.io/?utm_source=openai
- 2: https://github.com/unjs/unhead?utm_source=openai
- 3: https://unjs.io/packages/unhead?utm_source=openai
- 4: https://www.npmjs.com/package/%40unhead/angular?utm_source=openai
Fix import path: @unhead/dynamic-import/plugins does not exist.
The package @unhead/dynamic-import is not published. Replace with unhead/plugins:
Corrected import
- import { ValidatePlugin } from '@unhead/dynamic-import/plugins'
+ import { ValidatePlugin } from 'unhead/plugins'Copy-paste examples fail with "package not found" error. This affects all plugins docs that reference @unhead/dynamic-import/*.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { ValidatePlugin } from '@unhead/dynamic-import/plugins' | |
| import { ValidatePlugin } from 'unhead/plugins' |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/head/1.guides/plugins/validate.md` at line 26, The import path for
ValidatePlugin is incorrect — update the import statement that currently
references ValidatePlugin from '@unhead/dynamic-import/plugins' to import
ValidatePlugin from 'unhead/plugins'; search for other occurrences of
'@unhead/dynamic-import/*' in docs and replace them with the corresponding
'unhead/*' plugin imports to avoid "package not found" errors.
|
|
||
| ```ts [Custom Link rel] | ||
| import type { GenericLink } from 'unhead/types' | ||
| import type { GenericLink } from '@unhead/dynamic-import/types' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) Locate `@unhead/dynamic-import` package manifest"
DYN_PKG="$(fd -i package.json | xargs rg -n "\"name\"\\s*:\\s*\"@unhead/dynamic-import\"" -l | head -n1 || true)"
echo "Found: ${DYN_PKG:-<none>}"
[ -n "${DYN_PKG:-}" ] && sed -n '1,240p' "$DYN_PKG"
echo
echo "2) Check whether GenericLink/GenericScript are exported in dynamic-import package files"
fd -i . packages | rg -n "dynamic-import" || true
fd -i "*.ts" packages | xargs rg -n "export.*GenericLink|export.*GenericScript|from 'unhead/types'|from \"unhead/types\"" || true
echo
echo "3) Confirm canonical type definitions"
rg -n "export interface GenericLink|export interface GenericScript" packages/unhead/src/types || trueRepository: unjs/unhead
Length of output: 8339
🏁 Script executed:
sed -n '365,385p' docs/head/7.api/composables/0.use-head.mdRepository: unjs/unhead
Length of output: 778
Fix import path for TypeScript examples on lines 370 and 381.
Change imports from @unhead/dynamic-import/types to unhead/types. The non-existent package path will cause type-checking failures in user examples:
import type { GenericLink } from 'unhead/types'
import type { GenericScript } from 'unhead/types'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/head/7.api/composables/0.use-head.md` at line 370, Update the TypeScript
example imports that currently reference the non-existent path
'@unhead/dynamic-import/types' so they import the types from 'unhead/types'
instead; specifically change the imports for GenericLink and GenericScript to
use 'unhead/types' (replace the import source for the GenericLink and
GenericScript type imports).
Addresses CodeRabbit review feedback on #733. - SSRStaticReplace: vite.apply now returns true so the plugin actually runs during SSR builds (returning false disabled it entirely, so the head.ssr static replacement never happened) - CreateHeadTransform: scope createHead callsite matching to symbols imported from unhead/@unhead modules; supports named, aliased, and namespace imports; ignores shadowed locals and unrelated packages - Tests: cover false-positive cases (non-Unhead packages, shadowed locals) plus aliased and namespace imports - Docs: rename unhead({...}) -> Unhead({...}) in build-plugins examples, drop undocumented devtools option from overview, fix head.resolveTags() to resolveTags(head) in plugins API page - Migration guide: restore v3 content (was emptied in 66c387c) and add @unhead/addons -> @unhead/bundler rename + named Unhead export sections
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
packages/bundler/src/unplugin/CreateHeadTransform.ts (1)
61-63:⚠️ Potential issue | 🟠 MajorImported-name matching still rewrites shadowed locals.
Line 85 matches by identifier name presence, not by resolved binding. If
createHeadis imported from@unhead/*and then shadowed in an inner scope, the inner local call is still rewritten, causing false-positive injection and behavior changes. Please resolve callsites to their import binding (or otherwise scope-check) before rewriting.Also applies to: 85-92
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/bundler/src/unplugin/CreateHeadTransform.ts` around lines 61 - 63, The rewrite currently matches calls by identifier name only (using directCreateHeadNames / namespaceNames) and rewrites shadowed local bindings; change the transform to resolve each callsite's binding before rewriting: for Identifier calls (createHead) use the AST traversal path to get the binding (e.g., path.scope.getBinding(id.name)) and verify the binding is the import from `@unhead/`* (check binding.path.isImportSpecifier / binding.path.parent.source.value), and for namespace calls check the object identifier's binding similarly to ensure it’s the imported namespace; skip rewriting when no binding or when the binding is a local shadow so only true imports from `@unhead/`* trigger the injection.
🧹 Nitpick comments (2)
packages/bundler/test/createHeadTransform.test.ts (1)
135-140: Shadowing test misses the imported-symbol scenario.This case has no
@unhead/*import, so it doesn’t validate true shadowing (import { createHead } ...plus a localcreateHead). Please add that variant to prevent regressions in scope resolution.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/bundler/test/createHeadTransform.test.ts` around lines 135 - 140, The test for shadowing in createHead is incomplete because it only checks a local-only definition; add a variant where createHead is also imported (e.g., import { createHead } from '@unhead/some' or similar) and then a local function createHead() shadows that import to ensure the transform (created via createPlugin and accessed as transform) does not rewrite the local symbol; update the test in createHeadTransform.test.ts to call transform with source containing both the import and the local createHead declaration and assert the result remains undefined, using the same createPlugin/test harness (createPlugin, transform) as the existing test.packages/bundler/src/unplugin/SSRStaticReplace.ts (1)
39-47: Setssrexplicitly per build context (not only when true).Line 40 and Line 45 only ever flip
ssrtotrue. If a plugin instance is reused across server/client runs, stale state can leak and rewrite client code as SSR. Prefer assigning the boolean each time.Proposed change
webpack(ctx) { - if (ctx.name === 'server') - ssr = true + ssr = ctx.name === 'server' }, vite: { apply(_config: UserConfig, env: ConfigEnv) { - if (env.isSsrBuild) - ssr = true + ssr = Boolean(env.isSsrBuild) return true }, },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/bundler/src/unplugin/SSRStaticReplace.ts` around lines 39 - 47, The plugin currently only flips the shared ssr flag to true, allowing stale state between runs; update the webpack hook and vite.apply to assign ssr explicitly instead of only setting true: in the webpack(ctx) function set ssr = (ctx.name === 'server'), and in vite.apply(_config: UserConfig, env: ConfigEnv) set ssr = env.isSsrBuild so each invocation deterministically sets SSR mode for this plugin instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/6.migration-guide/1.v3.md`:
- Around line 69-76: The fenced code block inside the ::tip admonition is
missing a blank line after the closing ``` fence which violates MD031; update
the ::tip block around the shown fenced block (the block containing import {
Unhead } from '@unhead/vue/vite') so there is an empty line after the closing
``` and ensure there is a blank line before and after the fenced code block
within the ::tip so the closing fence is followed by a blank line before the
terminating :: marker.
In `@docs/head/7.api/plugins.md`:
- Line 6: Replace the phrase "hooks based" with the hyphenated compound
adjective "hooks-based" in the sentence that reads "Unhead uses a hooks based
architecture powered by unjs/hookable" so the line becomes "Unhead uses a
hooks-based architecture powered by unjs/hookable" to improve grammar and
readability.
---
Duplicate comments:
In `@packages/bundler/src/unplugin/CreateHeadTransform.ts`:
- Around line 61-63: The rewrite currently matches calls by identifier name only
(using directCreateHeadNames / namespaceNames) and rewrites shadowed local
bindings; change the transform to resolve each callsite's binding before
rewriting: for Identifier calls (createHead) use the AST traversal path to get
the binding (e.g., path.scope.getBinding(id.name)) and verify the binding is the
import from `@unhead/`* (check binding.path.isImportSpecifier /
binding.path.parent.source.value), and for namespace calls check the object
identifier's binding similarly to ensure it’s the imported namespace; skip
rewriting when no binding or when the binding is a local shadow so only true
imports from `@unhead/`* trigger the injection.
---
Nitpick comments:
In `@packages/bundler/src/unplugin/SSRStaticReplace.ts`:
- Around line 39-47: The plugin currently only flips the shared ssr flag to
true, allowing stale state between runs; update the webpack hook and vite.apply
to assign ssr explicitly instead of only setting true: in the webpack(ctx)
function set ssr = (ctx.name === 'server'), and in vite.apply(_config:
UserConfig, env: ConfigEnv) set ssr = env.isSsrBuild so each invocation
deterministically sets SSR mode for this plugin instance.
In `@packages/bundler/test/createHeadTransform.test.ts`:
- Around line 135-140: The test for shadowing in createHead is incomplete
because it only checks a local-only definition; add a variant where createHead
is also imported (e.g., import { createHead } from '@unhead/some' or similar)
and then a local function createHead() shadows that import to ensure the
transform (created via createPlugin and accessed as transform) does not rewrite
the local symbol; update the test in createHeadTransform.test.ts to call
transform with source containing both the import and the local createHead
declaration and assert the result remains undefined, using the same
createPlugin/test harness (createPlugin, transform) as the existing test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8a58b0a5-9a48-4654-91e2-55459c05dee6
📒 Files selected for processing (9)
docs/6.migration-guide/1.v3.mddocs/head/1.guides/build-plugins/0.overview.mddocs/head/1.guides/build-plugins/1.tree-shaking.mddocs/head/1.guides/build-plugins/2.seo-meta-transform.mddocs/head/1.guides/build-plugins/3.minify-transform.mddocs/head/7.api/plugins.mdpackages/bundler/src/unplugin/CreateHeadTransform.tspackages/bundler/src/unplugin/SSRStaticReplace.tspackages/bundler/test/createHeadTransform.test.ts
✅ Files skipped from review due to trivial changes (4)
- docs/head/1.guides/build-plugins/3.minify-transform.md
- docs/head/1.guides/build-plugins/1.tree-shaking.md
- docs/head/1.guides/build-plugins/0.overview.md
- docs/head/1.guides/build-plugins/2.seo-meta-transform.md
- v3.md: add blank line before ::tip closing marker (markdownlint MD031) - plugins.md: hyphenate "hooks-based" compound adjective
…ools-next Brings the named Unhead export, ctx-based transforms, dev-mode validate flag, and CodeRabbit fixes from #733 into the devtools branch. Conflict resolution: - types.ts / vite.ts: keep both devtools and validate options side by side - CreateHeadTransform / SSRStaticReplace / tests / build-plugins docs: take the bundler branch versions (scoped createHead matching, vite.apply returning true, Unhead({...}) casing, plugins API resolveTags fix) - v3 migration guide: take bundler branch (full v3 content + addons->bundler rename + named export sections) - 0.overview.md: merge devtools and validate option rows + bullets - pnpm-lock.yaml: regenerated via pnpm install + pnpm dedupe to match the bundler dedupe fix from 73fe714 - SSRStaticReplace.ts: annotate vite.apply return type as boolean to avoid literal-narrowing TS error against vite's UnpluginOptions['vite'] Tests: 74/74 bundler tests pass; remaining tsc errors are pre-existing "Cannot find module" issues from packages requiring build first.
* feat: vite devtools integration
Add devtools and devtools-app packages with Vue composables and plugin updates.
* feat: vite devtools integration
Add devtools and devtools-app packages with Vue composables and plugin updates.
* doc: clean up
* doc: clean up
* chore: progress
* chore: progress
* chore: progress
* chore: progress
* fix(validate): false positives for warmup preloads and charset position
- Allow `preload` + `fetchpriority="low"` for `as="script"` (the warmup
pattern used by `useScript` to start fetching at low priority).
- Skip `preload-async-defer-conflict` when the preload uses
`fetchpriority="low"` for the same reason.
- Run `charset-not-early` only on SSR (DOM order is already set after
hydration), and sort by capo weight while filtering virtual tags
(`templateParams`, `titleTemplate`) so they don't inflate the position
count.
- Pass tag references to several `report()` calls so consumers can
surface the offending tag in error messages.
* refactor(bundler): named Unhead export, ctx-based transforms, dev-mode validate
Foundational changes that landed alongside the in-progress devtools work.
Splits the bundler refactor + DX improvements out from the devtools branch
so they can be reviewed and merged independently.
## Bundler
- BREAKING: `default` export → named `Unhead` export on `@unhead/bundler/{vite,webpack}`
and on every framework wrapper (`@unhead/{react,solid-js,svelte,vue}/vite`).
Existing imports must change from `import unhead from ...` to
`import { Unhead } from ...`.
- New `VitePluginOptions` interface (extends `UnpluginOptions`) with a
`validate` flag that injects `ValidatePlugin` in dev so head-tag warnings
surface in the console without manual setup. Internal `_framework` field
lets framework wrappers identify themselves to the runtime.
- New `CreateHeadTransform` + `createHeadTransformContext` foundation: a
single transform handles `createHead()` wrapping and lets other plugins
register runtime plugins via shared context.
- New `SSRStaticReplace` transform: replaces `head.ssr` with a static
boolean per environment so the dead branch tree-shakes cleanly.
- `UseSeoMetaTransform` now preserves the second argument (e.g.
`useSeoMeta(meta, { head })`) when rewriting to `useHead`.
- Tests: `createHeadTransform.test.ts` and updated `useSeoMetaTransform.test.ts`.
## Framework wrappers
- Vue / React / Solid / Svelte vite plugins updated for the named export
and forward `_framework` so the bundler can resolve framework-scoped
runtime plugins (`@unhead/vue/plugins`, etc.).
- `packages-aliased/addons` deprecation shim re-exports the new named export.
## Vue
- Re-exports `useServerHead`, `useServerHeadSafe`, `useServerSeoMeta` as
deprecated aliases for the non-`Server` variants (v2 compat).
- Re-exports `resolveUnrefHeadInput` from `utils` (v2 compat).
## Schema.org
- Exports `schemaAutoImports` from the package root so consumers can wire
it into `unplugin-auto-import` without reaching into subpaths.
## Docs
- Reorganises `1.guides/2.advanced/{vite-plugin,client-only-tags,extending-unhead}`
into a new `1.guides/build-plugins/` section: overview, tree-shaking,
seo-meta-transform, minify-transform.
- Migration guide moves out of `content/` to top-level `6.migration-guide/`.
- Adds `7.api/plugins.md` and updates `use-head` / `use-seo-meta` API pages.
## Examples
- `vite-ssr-vue-prerender`: switches to the named `Unhead` import.
* chore: dedupe pnpm lockfile to fix vite peer-dep duplication
Two [email protected] instances were resolved due to a peer-dep variation on
the bundler package's optional `esbuild` peer, causing TS to see two
incompatible Plugin types in the new bundler transforms. `pnpm update &&
pnpm dedupe` collapses them onto a single resolution and bumps vitest to
4.1.4.
* fix(bundler): scope createHead matching and restore SSR transform
Addresses CodeRabbit review feedback on #733.
- SSRStaticReplace: vite.apply now returns true so the plugin actually
runs during SSR builds (returning false disabled it entirely, so the
head.ssr static replacement never happened)
- CreateHeadTransform: scope createHead callsite matching to symbols
imported from unhead/@unhead modules; supports named, aliased, and
namespace imports; ignores shadowed locals and unrelated packages
- Tests: cover false-positive cases (non-Unhead packages, shadowed
locals) plus aliased and namespace imports
- Docs: rename unhead({...}) -> Unhead({...}) in build-plugins examples,
drop undocumented devtools option from overview, fix head.resolveTags()
to resolveTags(head) in plugins API page
- Migration guide: restore v3 content (was emptied in 66c387c) and add
@unhead/addons -> @unhead/bundler rename + named Unhead export sections
* docs: address coderabbit nits on v3 migration and plugins API
- v3.md: add blank line before ::tip closing marker (markdownlint MD031)
- plugins.md: hyphenate "hooks-based" compound adjective
* fix(ci): unblock test workflow on devtools branch
Four CI fixes needed after merging refactor/bundler-vite-improvements:
- package.json: exclude @unhead/devtools-app from the main build script
(needs nuxi prepare and was racing pnpm topology against @unhead/vue,
blowing up the Build step). Add a separate build:devtools-app script
for use during release.
- bundler/src/devtools/rpc/functions/get-config.ts: annotate getConfigRpc
as `: any` to break a TS2883 portable-inference chain caused by
duplicate @vitejs/devtools-rpc resolutions across two TS peer-dep
variants in the lockfile.
- unhead/src/scripts/useScript.ts: revert the SSR proxy skip so
script.proxy is still a noop recording proxy in SSR. The skip broke
ssr.test.ts/use.test.ts which intentionally exercise proxy access on
a server head.
- test/exports/bundler.yaml: regenerate snapshot to include the new
devtoolsPlugin export from @unhead/bundler.
* chore: untrack packages/devtools-app/.nuxt build artifacts
158 .nuxt files were committed accidentally. .gitignore already covers
.nuxt at the root, so just removing them from the index is enough.
* fix: address coderabbit review on devtools branch
Critical / major fixes:
- bundler/devtools/plugin.ts: escape `<` in inlined JSON payload so a
serialized `</script>` can't close the application/json block early
- DevtoolsSection.vue: remove duplicate `open` prop (already declared
by `defineModel('open')`, was a Vue compile error)
- composables/link-checker.ts: guard `new Image()` and fetch behind
`typeof window === 'undefined'` so SSR/prerender doesn't crash; also
drop the permanent `checkedUrls` set so fixed assets self-heal and
pendingUrls becomes the de-duplication gate
- composables/update-check.ts: skip the npm registry fetch in SSR; only
flip `checked` after a successful response (was permanently disabling
on transient failures)
- bundler/package.json: move `@vitejs/devtools-kit` from devDeps to deps
since `bundler/src/devtools/bridge.ts` is published and imports it at
runtime via `@vitejs/devtools-kit/client`
- DevtoolsTagTable.vue: track `expandedRows` by `tagMatchKey(tag)`
instead of filteredTags index so filtering doesn't reopen the wrong
detail row
- pages/serp.vue: parse every JSON-LD `<script>` tag (collapse multi-tag
pages into a synthetic `@graph`) instead of stopping at the first
- pages/serp.vue: add `type="button"`, `aria-label`, and `aria-pressed`
to the desktop/mobile preview toggles
Minor fixes:
- docs/build-plugins/4.devtools.md: vite-devtools repo URL → vitejs/devtools
- examples vite-ssr-vue/BlogPage.vue: "first class" → "first-class"
- assets/css/global.css: drop duplicate `--scrollbar-thumb` declaration
- DevtoolsEmptyState.vue: render description slot in a `<div>` instead
of nesting block content inside `<p>`
- DevtoolsHeadEntry.vue: fall back to "unknown" when `entry.mode` is empty
- DevtoolsLayout.vue: tighten non-root tab match so `/schema-preview`
doesn't activate the `/schema` tab
- composables/rpc.ts: drop the `as any` cast on sharedState, add
try/catch around the connection setup with a console warning
- composables/shiki.ts: load CSS grammar (DevtoolsSnippet accepts `css`)
- pages/identity.vue: composite icon-card key including rel/sizes/type
so duplicate hrefs across variants don't collide
Tests: 1214 passed; build, vue-tsc, eslint, all green.
* chore: sync
* chore: sync
🔗 Linked issue
Foundation for #731 (devtools work split out)
❓ Type of change
📚 Description
Pulls all the non-devtools work out of #731 so it can be reviewed and merged independently. Once this lands, the devtools PR rebases on top and contains only the devtools-app + bundler/devtools wiring.
Bundler refactor
Unheadnamed export on@unhead/bundler/{vite,webpack}and on every framework wrapper (@unhead/{react,solid-js,svelte,vue}/vite).VitePluginOptions.validateflag — injectsValidatePluginin dev so head-tag warnings surface in the console without manual setup. Internal_frameworkfield lets framework wrappers identify themselves to the runtime so the right<framework>/pluginssource is resolved.CreateHeadTransform+createHeadTransformContext— single transform handlescreateHead()wrapping and lets other plugins register runtime plugins via shared context (the foundation devtools and the new validate flag both build on).SSRStaticReplacetransform — replaceshead.ssrwith a static boolean per environment so the dead branch tree-shakes cleanly.UseSeoMetaTransformnow preserves the second argument (e.g.useSeoMeta(meta, { head })) when rewriting touseHead.createHeadTransform.test.tsplus expandeduseSeoMetaTransform.test.ts.Vue / Schema.org
@unhead/vuere-exportsuseServerHead,useServerHeadSafe,useServerSeoMetaas deprecated aliases for the non-Servervariants, plusresolveUnrefHeadInputfrom utils (v2 compat).@unhead/schema-orgexportsschemaAutoImportsfrom the package root so consumers can wire it intounplugin-auto-importwithout reaching into subpaths.Docs
1.guides/2.advanced/{vite-plugin,client-only-tags,extending-unhead}into a new1.guides/build-plugins/section: overview, tree-shaking, seo-meta-transform, minify-transform.content/to top-level6.migration-guide/.7.api/plugins.mdand updates theuse-head/use-seo-metaAPI pages.The default export on every vite/webpack plugin has been renamed:
@unhead/bundler/vite,@unhead/bundler/webpack@unhead/react/vite,@unhead/solid-js/vite,@unhead/svelte/vite,@unhead/vue/vite📝 Migration
Update vite/webpack configs from default to named import:
Same shape for
@unhead/react/vite,@unhead/solid-js/vite,@unhead/svelte/vite,@unhead/bundler/{vite,webpack}.✅ Tests
schemaAutoImports/useServerHead*re-exportsmainchain when consuming the new transforms; not introduced hereSummary by CodeRabbit
New Features
Breaking Changes
Documentation