Skip to content

refactor(bundler)!: named Unhead export, ctx-based transforms, dev-mode validate#733

Merged
harlan-zw merged 4 commits into
mainfrom
refactor/bundler-vite-improvements
Apr 9, 2026
Merged

refactor(bundler)!: named Unhead export, ctx-based transforms, dev-mode validate#733
harlan-zw merged 4 commits into
mainfrom
refactor/bundler-vite-improvements

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Foundation for #731 (devtools work split out)

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking 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

  • New Unhead named export on @unhead/bundler/{vite,webpack} and on every framework wrapper (@unhead/{react,solid-js,svelte,vue}/vite).
  • New VitePluginOptions.validate flag — 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 so the right <framework>/plugins source is resolved.
  • New CreateHeadTransform + createHeadTransformContext — single transform handles createHead() wrapping and lets other plugins register runtime plugins via shared context (the foundation devtools and the new validate flag both build on).
  • 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 plus expanded useSeoMetaTransform.test.ts.

Vue / Schema.org

  • @unhead/vue re-exports useServerHead, useServerHeadSafe, useServerSeoMeta as deprecated aliases for the non-Server variants, plus resolveUnrefHeadInput from utils (v2 compat).
  • @unhead/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 the use-head / use-seo-meta API pages.

⚠️ Breaking Changes

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:

- import unhead from '@unhead/vue/vite'
+ import { Unhead } from '@unhead/vue/vite'

  export default defineConfig({
-   plugins: [vue(), unhead()],
+   plugins: [vue(), Unhead()],
  })

Same shape for @unhead/react/vite, @unhead/solid-js/vite, @unhead/svelte/vite, @unhead/bundler/{vite,webpack}.

✅ Tests

  • 1206 tests passing across the workspace
  • Export snapshots updated for the renamed exports + new schemaAutoImports / useServerHead* re-exports
  • Lint clean
  • Typecheck has 10 pre-existing errors from a vite peer-dep duplication on the bundler package — same errors exist on main chain when consuming the new transforms; not introduced here

Summary by CodeRabbit

  • New Features

    • Comprehensive Build Plugins docs (tree-shaking, SEO meta transform, minify) and Plugins API for custom plugins.
    • Dev-time automatic ValidatePlugin injection (browser console warnings).
    • Added deprecated server-side alias composables and a deprecated helper export for Vue.
  • Breaking Changes

    • Vite/build plugin entrypoints now use a named Unhead export (update imports/usages).
  • Documentation

    • Reorganized guides, updated framework setup examples, and expanded v3 migration guidance.

…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.
@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d9d09c1-e79a-487b-8556-e7a9a79ff7e3

📥 Commits

Reviewing files that changed from the base of the PR and between 437f012 and 562cfbb.

📒 Files selected for processing (2)
  • docs/6.migration-guide/1.v3.md
  • docs/head/7.api/plugins.md
✅ Files skipped from review due to trivial changes (1)
  • docs/head/7.api/plugins.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/6.migration-guide/1.v3.md

📝 Walkthrough

Walkthrough

This PR unifies and renames build plugins to a named Unhead export, introduces build-time transforms and a runtime plugin injection mechanism (CreateHeadTransform, SSRStaticReplace), preserves options/arguments in SEO transforms, updates framework Vite integrations and exports, adds tests, and reorganizes documentation and migration guidance.

Changes

Cohort / File(s) Summary
Bundler core & plugin APIs
packages/bundler/src/unplugin/vite.ts, packages/bundler/src/unplugin/webpack.ts, packages/bundler/src/unplugin/types.ts, packages/bundler/README.md
Rename default exports to named Unhead(...), add VitePluginOptions (validate/_framework), inject SSRStaticReplace and runtime plugin registration, and update README examples.
New transforms & runtime injection
packages/bundler/src/unplugin/CreateHeadTransform.ts, packages/bundler/src/unplugin/SSRStaticReplace.ts, packages/bundler/src/unplugin/UseSeoMetaTransform.ts
Add CreateHeadTransform (dev-time runtime plugin injection), SSRStaticReplace (replace head.ssr at build time), and fix UseSeoMetaTransform to preserve optional second argument.
Tests
packages/bundler/test/createHeadTransform.test.ts, packages/bundler/test/useSeoMetaTransform.test.ts
Add unit tests validating CreateHeadTransform behaviors (env-specific injection, root substitution, import forms) and useSeoMeta second-arg preservation.
Framework Vite integrations & exports
packages/vue/src/vite.ts, packages/react/src/vite.ts, packages/solid-js/src/vite.ts, packages/svelte/src/vite.ts, packages/react/src/composables.ts, packages/solid-js/src/vite.ts
Change default export unhead → named Unhead, switch options to VitePluginOptions, propagate _framework to bundler, and minor composable variable rename in React.
Vue API additions & deprecations
packages/vue/src/composables.ts, packages/vue/src/index.ts, packages/vue/src/utils.ts
Add deprecated aliases useServerHead, useServerHeadSafe, useServerSeoMeta; export resolveUnrefHeadInput (deprecated wrapper); adjust exports.
Schema-org exports
packages/schema-org/src/index.ts
Add re-export schemaAutoImports from ./imports.
Docs: Build Plugins & Guides
docs/head/1.guides/build-plugins/..., docs/head/1.guides/2.advanced/..., many docs under docs/*
Add comprehensive Build Plugins docs (overview, tree-shaking, seo-meta, minify), remove old Vite-plugin/legacy pages, update many guides/examples to use Unhead and new import paths (@unhead/dynamic-import/...), and update migration guide.
Examples & workspace
examples/vite-ssr-vue-prerender/vite.config.js, pnpm-workspace.yaml, packages-aliased/addons/src/vite.ts, packages/bundler/README.md
Update example Vite config to use Unhead(), adjust pnpm catalog vitest version, change re-export behavior in aliased package, and update README examples.
Exports manifests / tests
test/exports/*.yaml, test/exports/vue.yaml, test/exports/schema-org.yaml
Update test export maps to reflect named Unhead exports, added Vue server composables/util exports, added schemaAutoImports.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • v3 Release #711: Core v3 work — this PR implements the unified build-plugin architecture, transforms, API rename, and migration guidance referenced by that issue.

Possibly related PRs

Poem

🐰 In fields of code I hop and hum,

I named the plugin, "Unhead" — not some,
I tuck in transforms, prune what’s unneeded,
Docs rebloom, exports seeded,
Cheers from a rabbit: builds neatly done! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: a bundler refactor introducing a named Unhead export, context-based transforms, and dev-mode validation.
Description check ✅ Passed The description comprehensively covers all major changes, includes linked issue, type of change checkboxes, detailed breakdown of bundler/Vue/docs changes, clear breaking change warning, migration examples, and test results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/bundler-vite-improvements

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 and usage tips.

@github-actions

github-actions Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.6 kB 4.3 kB
Server (Minimal) 10.3 kB 4.2 kB
Vue Client (Minimal) 11.6 kB 4.8 kB
Vue Server (Minimal) 11.3 kB 4.6 kB

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.

@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.

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 | 🟠 Major

Invalid 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 base unhead package.

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 | 🟡 Minor

Documentation 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 | 🟡 Minor

Bug: imports: false option is not honored.

The expression options.imports || true will always evaluate to true when imports is explicitly set to false. Use nullish coalescing to respect explicit false values.

🐛 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 | 🔴 Critical

Duplicate: Incorrect import path for InferSeoMetaPlugin.

Same issue as line 48 - the import path should be unhead/plugins instead 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c83934 and 66c387c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (60)
  • docs/0.angular/schema-org/guides/get-started/0.installation.md
  • docs/0.react/schema-org/guides/get-started/0.installation.md
  • docs/0.solid-js/schema-org/guides/get-started/0.installation.md
  • docs/0.svelte/schema-org/guides/get-started/0.installation.md
  • docs/0.typescript/head/guides/0.get-started/1.installation.md
  • docs/0.typescript/schema-org/guides/get-started/0.installation.md
  • docs/0.vue/schema-org/guides/0.get-started/0.installation.md
  • docs/6.migration-guide/.navigation.yml
  • docs/6.migration-guide/1.v3.md
  • docs/6.migration-guide/2.v2.md
  • docs/content/6.migration-guide/1.v3.md
  • docs/content/7.releases/1.v3.md
  • docs/content/7.releases/2.v2.md
  • docs/head/1.guides/0.get-started/0.overview.md
  • docs/head/1.guides/1.core-concepts/1.titles.md
  • docs/head/1.guides/1.core-concepts/2.positions.md
  • docs/head/1.guides/1.core-concepts/3.class-attr.md
  • docs/head/1.guides/2.advanced/11.extending-unhead.md
  • docs/head/1.guides/2.advanced/7.client-only-tags.md
  • docs/head/1.guides/2.advanced/9.vite-plugin.md
  • docs/head/1.guides/build-plugins/0.overview.md
  • docs/head/1.guides/build-plugins/1.tree-shaking.md
  • docs/head/1.guides/build-plugins/2.seo-meta-transform.md
  • docs/head/1.guides/build-plugins/3.minify-transform.md
  • docs/head/1.guides/plugins/6.template-params.md
  • docs/head/1.guides/plugins/alias-sorting.md
  • docs/head/1.guides/plugins/canonical.md
  • docs/head/1.guides/plugins/infer-seo-meta-tags.md
  • docs/head/1.guides/plugins/minify.md
  • docs/head/1.guides/plugins/validate.md
  • docs/head/7.api/0.get-started/overview.md
  • docs/head/7.api/composables/0.use-head.md
  • docs/head/7.api/composables/3.use-seo-meta.md
  • docs/head/7.api/plugins.md
  • examples/vite-ssr-vue-prerender/vite.config.js
  • packages-aliased/addons/src/vite.ts
  • packages/bundler/README.md
  • packages/bundler/src/unplugin/CreateHeadTransform.ts
  • packages/bundler/src/unplugin/SSRStaticReplace.ts
  • packages/bundler/src/unplugin/UseSeoMetaTransform.ts
  • packages/bundler/src/unplugin/types.ts
  • packages/bundler/src/unplugin/vite.ts
  • packages/bundler/src/unplugin/webpack.ts
  • packages/bundler/test/createHeadTransform.test.ts
  • packages/bundler/test/useSeoMetaTransform.test.ts
  • packages/react/src/composables.ts
  • packages/react/src/vite.ts
  • packages/schema-org/src/index.ts
  • packages/solid-js/src/vite.ts
  • packages/svelte/src/vite.ts
  • packages/vue/src/composables.ts
  • packages/vue/src/index.ts
  • packages/vue/src/utils.ts
  • packages/vue/src/vite.ts
  • test/exports/bundler.yaml
  • test/exports/react.yaml
  • test/exports/schema-org.yaml
  • test/exports/solid-js.yaml
  • test/exports/svelte.yaml
  • test/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

Comment thread docs/head/1.guides/build-plugins/0.overview.md
Comment thread docs/head/1.guides/build-plugins/1.tree-shaking.md
Comment thread docs/head/1.guides/build-plugins/2.seo-meta-transform.md
Comment thread docs/head/1.guides/build-plugins/3.minify-transform.md
Comment on lines +20 to +21
import { createHead } from '@unhead/dynamic-import'
import { TemplateParamsPlugin } from '@unhead/dynamic-import/plugins'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: unjs/unhead

Length of output: 1539


🏁 Script executed:

rg -n "export.*createHead" packages/unhead/src/index.ts | head -20

Repository: unjs/unhead

Length of output: 37


🏁 Script executed:

rg -n "createHead" packages/unhead/src/ | head -30

Repository: unjs/unhead

Length of output: 2182


🏁 Script executed:

cat -n packages/unhead/src/index.ts | head -50

Repository: unjs/unhead

Length of output: 184


🏁 Script executed:

cat -n docs/head/1.guides/plugins/6.template-params.md | head -50

Repository: 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.ts

Repository: 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 -100

Repository: 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 -30

Repository: 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.ts

Repository: 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 -50

Repository: 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/ -l

Repository: 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 -100

Repository: 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 -20

Repository: 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.ts

Repository: 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.ts

Repository: 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.

Suggested change
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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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' docs

Repository: 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 -20

Repository: 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 -B2

Repository: 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 -10

Repository: 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:


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.

Suggested change
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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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 || true

Repository: unjs/unhead

Length of output: 8339


🏁 Script executed:

sed -n '365,385p' docs/head/7.api/composables/0.use-head.md

Repository: 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).

Comment thread docs/head/7.api/plugins.md
Comment thread packages/bundler/src/unplugin/CreateHeadTransform.ts Outdated
Comment thread packages/bundler/src/unplugin/SSRStaticReplace.ts
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

@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.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/bundler/src/unplugin/CreateHeadTransform.ts (1)

61-63: ⚠️ Potential issue | 🟠 Major

Imported-name matching still rewrites shadowed locals.

Line 85 matches by identifier name presence, not by resolved binding. If createHead is 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 local createHead). 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: Set ssr explicitly per build context (not only when true).

Line 40 and Line 45 only ever flip ssr to true. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 73fe714 and 437f012.

📒 Files selected for processing (9)
  • docs/6.migration-guide/1.v3.md
  • docs/head/1.guides/build-plugins/0.overview.md
  • docs/head/1.guides/build-plugins/1.tree-shaking.md
  • docs/head/1.guides/build-plugins/2.seo-meta-transform.md
  • docs/head/1.guides/build-plugins/3.minify-transform.md
  • docs/head/7.api/plugins.md
  • packages/bundler/src/unplugin/CreateHeadTransform.ts
  • packages/bundler/src/unplugin/SSRStaticReplace.ts
  • packages/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

Comment thread docs/6.migration-guide/1.v3.md
Comment thread docs/head/7.api/plugins.md Outdated
- v3.md: add blank line before ::tip closing marker (markdownlint MD031)
- plugins.md: hyphenate "hooks-based" compound adjective
@harlan-zw harlan-zw merged commit 3a60751 into main Apr 9, 2026
6 checks passed
harlan-zw added a commit that referenced this pull request Apr 9, 2026
…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.
harlan-zw added a commit that referenced this pull request Apr 9, 2026
* 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
@harlan-zw harlan-zw mentioned this pull request Apr 9, 2026
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