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

Skip to content

Commit e29d435

Browse files
authored
feat(devtools): vite devtools integration (#731)
* 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
1 parent 183424b commit e29d435

82 files changed

Lines changed: 17879 additions & 6653 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ playwright-report/
2323

2424
# Skilld references (recreated by `skilld install`)
2525
.skilld
26+
27+
.nuxt
28+
.output

docs/head/1.guides/build-plugins/0.overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ The plugin combines several build-time optimizations:
6868
- **[useSeoMeta transform](/docs/head/guides/build-plugins/seo-meta-transform)**: Converts `useSeoMeta()` calls into raw `useHead()` calls at build time (~3kb savings)
6969
- **[Minify transform](/docs/head/guides/build-plugins/minify-transform)**: Pre-minify static inline script/style content at build time
7070
- **Dev validation**: Auto-injects `ValidatePlugin` in dev so head tag warnings surface in the browser console (enabled by default)
71+
- **[Devtools](/docs/head/guides/build-plugins/devtools)**: Inspect head tags, entries, and SEO metadata in Vite DevTools (dev only, enabled by default)
7172

7273
## Options
7374

@@ -90,6 +91,7 @@ Unhead({
9091
| `transformSeoMeta` | `object \| false` | enabled | [Transform `useSeoMeta()`](/docs/head/guides/build-plugins/seo-meta-transform) to `useHead()` |
9192
| `minify` | `object \| false` | disabled | [Pre-minify static inline script/style](/docs/head/guides/build-plugins/minify-transform) |
9293
| `validate` | `boolean` | enabled | Auto-inject `ValidatePlugin` in dev for head tag warnings |
94+
| `devtools` | `object \| false` | enabled | [Vite DevTools integration](/docs/head/guides/build-plugins/devtools) (dev only) |
9395
| `filter` | `object` || Shared include/exclude file filter |
9496
| `sourcemap` | `boolean` || Enable sourcemap generation for transforms |
9597

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
title: Devtools
3+
description: Inspect head tags, SEO metadata, entries, and source locations in the Vite DevTools panel. Enabled by default in development.
4+
navigation.title: Devtools
5+
---
6+
7+
**Quick Answer:** The Unhead devtools integration adds a panel to [Vite DevTools](https://github.com/vitejs/devtools) showing all active head tags, entries, SEO overview, plugins, and source file locations. Enabled by default in dev mode; install the `@vitejs/devtools-kit` and `@unhead/devtools-app` peers to activate the UI.
8+
9+
## What Does It Do?
10+
11+
The devtools plugin provides a real-time view of your head state during development:
12+
13+
- **Entries** — every `useHead()` / `useSeoMeta()` call with its source file and line number
14+
- **Tags** — all resolved tags (meta, link, script, style, etc.) with their props, position, priority, and dedupe keys
15+
- **SEO overview** — title, description, canonical, robots, Open Graph at a glance
16+
- **Scripts** — tracked `useScript()` instances with their load status
17+
- **Plugins** — which Unhead plugins are active
18+
- **Template params** — current template parameter values and separator
19+
- **Validation** — warnings from the [Validate plugin](/docs/head/guides/plugins/validate) (if enabled)
20+
21+
### Source Tracing
22+
23+
The plugin injects `_source` metadata (file path and line number) into every `useHead()`, `useSeoMeta()`, `useHeadSafe()`, and `useScript()` call at transform time. This lets you click through from a tag in the devtools panel to the exact line that created it.
24+
25+
## Setup
26+
27+
Devtools are enabled by default when using the [unified Vite plugin](/docs/head/guides/build-plugins/overview). They only run in `vite dev` (not in builds).
28+
29+
::FrameworkCode
30+
31+
#vue
32+
```ts [vite.config.ts]
33+
import { Unhead } from '@unhead/vue/vite'
34+
import vue from '@vitejs/plugin-vue'
35+
import { defineConfig } from 'vite'
36+
37+
export default defineConfig({
38+
plugins: [vue(), Unhead()],
39+
})
40+
```
41+
42+
#react
43+
```ts [vite.config.ts]
44+
import { Unhead } from '@unhead/react/vite'
45+
import react from '@vitejs/plugin-react'
46+
import { defineConfig } from 'vite'
47+
48+
export default defineConfig({
49+
plugins: [react(), Unhead()],
50+
})
51+
```
52+
53+
#nuxt
54+
```ts
55+
// Enabled automatically
56+
```
57+
58+
::
59+
60+
### Disable
61+
62+
```ts
63+
Unhead({
64+
devtools: false,
65+
})
66+
```
67+
68+
## Requirements
69+
70+
The devtools panel requires the `@vitejs/devtools-kit` peer dependency and the `@unhead/devtools-app` package (which provides the UI). Both are optional; the plugin degrades gracefully if they're missing.
71+
72+
## How It Works
73+
74+
The integration has three parts:
75+
76+
1. **Vite transform** — injects `_source` metadata into composable calls and wraps `createHead()` to expose the instance on `window.__unhead_devtools__`
77+
2. **Bridge script** — a client-side module (`/@unhead/bridge.mjs`) that reads the head instance, serializes its state, and syncs it to the Vite DevTools shared state via `@vitejs/devtools-kit/client`
78+
3. **DevTools panel** — a static app (`@unhead/devtools-app`) served at `/__unhead/` and registered as a Vite DevTools dock panel
79+
80+
State is synced on every `dom:rendered` hook, so the panel updates in real time as head tags change.
81+
82+
## See Also
83+
84+
- [Build Plugins Overview](/docs/head/guides/build-plugins/overview) - All build-time optimizations
85+
- [Validate Plugin](/docs/head/guides/plugins/validate) - Catch SEO and head tag issues (shown in devtools)

examples/vite-ssr-vue-streaming/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@playwright/test": "^1.59.1",
2525
"@types/express": "^5.0.6",
2626
"@types/node": "^25.5.2",
27+
"@vitejs/devtools": "^0.1.13",
2728
"@vitejs/plugin-vue": "latest",
2829
"cross-env": "^10.1.0",
2930
"typescript": "5.8.3",

examples/vite-ssr-vue-streaming/vite.config.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { defineConfig } from 'vite'
22
import vue from '@vitejs/plugin-vue'
3-
import unhead from '@unhead/vue/vite'
3+
import { DevTools } from '@vitejs/devtools'
4+
import { Unhead } from '@unhead/vue/vite'
45

56
export default defineConfig({
67
plugins: [
8+
DevTools(),
79
vue(),
8-
unhead({ streaming: true }),
10+
Unhead({ streaming: true }),
911
],
12+
devtools: {
13+
enabled: true,
14+
clientAuth: false,
15+
},
1016
build: {
1117
minify: false,
1218
},

examples/vite-ssr-vue/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,25 @@
1212
"check": "vue-tsc"
1313
},
1414
"dependencies": {
15+
"@unhead/schema-org": "workspace:*",
1516
"@unhead/vue": "workspace:*",
1617
"compression": "^1.8.1",
1718
"express": "^5.2.1",
1819
"rollup-plugin-visualizer": "^7.0.1",
1920
"sirv": "^3.0.2",
2021
"vite-bundle-analyzer": "^1.3.7",
21-
"vue": "^3.5.32"
22+
"vue": "^3.5.32",
23+
"vue-router": "^5.0.4"
2224
},
2325
"devDependencies": {
2426
"@types/express": "^5.0.6",
2527
"@types/node": "^25.5.2",
28+
"@unhead/bundler": "workspace:*",
29+
"@vitejs/devtools": "^0.1.13",
2630
"@vitejs/plugin-vue": "^6.0.5",
2731
"cross-env": "^10.1.0",
2832
"typescript": "5.8.3",
33+
"unplugin-auto-import": "^21.0.0",
2934
"vite": "^8.0.8",
3035
"vue-tsc": "^3.2.6"
3136
}

examples/vite-ssr-vue/src/App.vue

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,57 @@
11
<script setup lang="ts">
2-
// This starter template is using Vue 3 <script setup> SFCs
3-
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
4-
import HelloWorld from './components/HelloWorld.vue'
5-
import { useHead } from '@unhead/vue'
2+
import { RouterLink, RouterView } from 'vue-router'
63
74
useHead({
8-
title: 'hello from app.vue'
5+
titleTemplate: '%s | Unhead Demo',
6+
templateParams: {
7+
separator: '|',
8+
},
9+
htmlAttrs: { lang: 'en' },
10+
meta: [
11+
{ charset: 'utf-8' },
12+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
13+
{ name: 'color-scheme', content: 'light dark' },
14+
{ name: 'theme-color', content: '#4f46e5', media: '(prefers-color-scheme: light)' },
15+
{ name: 'theme-color', content: '#818cf8', media: '(prefers-color-scheme: dark)' },
16+
],
17+
link: [
18+
{ rel: 'icon', type: 'image/svg+xml', href: '/vite.svg' },
19+
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },
20+
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },
21+
],
922
})
1023
</script>
1124

1225
<template>
13-
<div>
14-
<a href="https://vite.dev" target="_blank">
15-
<img src="/vite.svg" class="logo" alt="Vite logo" />
16-
</a>
17-
<a href="https://vuejs.org/" target="_blank">
18-
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
19-
</a>
20-
</div>
21-
<HelloWorld msg="Vite + Vue" />
26+
<div id="layout">
27+
<nav class="site-nav">
28+
<RouterLink to="/">Home</RouterLink>
29+
<RouterLink to="/about">About</RouterLink>
30+
<RouterLink to="/blog">Blog</RouterLink>
31+
<RouterLink to="/scripts">Scripts</RouterLink>
32+
</nav>
33+
<main>
34+
<RouterView />
35+
</main>
36+
</div>
2237
</template>
2338

2439
<style scoped>
25-
.logo {
26-
height: 6em;
27-
padding: 1.5em;
28-
will-change: filter;
40+
.site-nav {
41+
display: flex;
42+
gap: 1rem;
43+
padding: 1rem;
44+
border-bottom: 1px solid #e5e7eb;
2945
}
30-
.logo:hover {
31-
filter: drop-shadow(0 0 2em #646cffaa);
46+
.site-nav a {
47+
color: #4f46e5;
48+
text-decoration: none;
49+
font-weight: 500;
3250
}
33-
.logo.vue:hover {
34-
filter: drop-shadow(0 0 2em #42b883aa);
51+
.site-nav a:hover {
52+
text-decoration: underline;
53+
}
54+
main {
55+
padding: 2rem;
3556
}
3657
</style>

examples/vite-ssr-vue/src/components/HelloWorld.vue

Lines changed: 0 additions & 43 deletions
This file was deleted.

examples/vite-ssr-vue/src/entry-client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import './style.css'
22
import { createApp } from './main'
33
import { createHead } from '@unhead/vue/client'
44

5-
const { app } = createApp()
5+
const { app, router } = createApp()
66
const head = createHead()
77
app.use(head)
88

9-
app.mount('#app')
9+
router.isReady().then(() => {
10+
app.mount('#app')
11+
})
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { renderToString } from 'vue/server-renderer'
22
import { createApp } from './main'
33
import { createHead } from '@unhead/vue/server'
4-
import { useSeoMeta } from '@unhead/vue'
54

6-
export async function render(_url: string) {
7-
const { app } = createApp()
5+
export async function render(url: string) {
6+
const { app, router } = createApp()
87
const head = createHead()
98
app.use(head)
10-
// no client-side hydration needed
11-
useSeoMeta({
12-
title: 'My Awesome Site',
13-
description: 'My awesome site description',
14-
}, { head })
15-
const ctx = {}
16-
const html = await renderToString(app, ctx)
9+
10+
router.push(url)
11+
await router.isReady()
12+
13+
const html = await renderToString(app)
1714

1815
return { html, head }
1916
}

0 commit comments

Comments
 (0)