Skip to content

Commit 82e31bc

Browse files
committed
fix(vue): keep reactive input at useHead boundary
1 parent 69c79be commit 82e31bc

13 files changed

Lines changed: 82 additions & 104 deletions

File tree

packages/vue/src/client.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import type { CreateClientHeadOptions } from 'unhead/types'
2-
import type { UseHeadInput, VueHeadClient } from './types'
1+
import type { CreateClientHeadOptions, ResolvableHead } from 'unhead/types'
2+
import type { VueHeadClient } from './types'
33
import { createHead as _createHead, createDomRenderer } from 'unhead/client'
44
import { vueInstall } from './install'
5-
import { VueResolver } from './resolver'
65

76
export { VueHeadMixin } from './VueHeadMixin'
87
export { renderDOMHead } from 'unhead/client'
98

109
/* @__NO_SIDE_EFFECTS__ */
11-
export function createHead(options: CreateClientHeadOptions = {}): VueHeadClient<UseHeadInput, boolean> {
10+
export function createHead(options: CreateClientHeadOptions = {}): VueHeadClient<ResolvableHead, boolean> {
1211
const domRenderer = createDomRenderer()
13-
let head: VueHeadClient<UseHeadInput, boolean>
12+
let head: VueHeadClient<ResolvableHead, boolean>
1413
let renderId = 0
1514
const debouncedRenderer = () => {
1615
const id = ++renderId
@@ -19,11 +18,7 @@ export function createHead(options: CreateClientHeadOptions = {}): VueHeadClient
1918
domRenderer(head)
2019
}, 0)
2120
}
22-
head = _createHead({
23-
render: debouncedRenderer,
24-
...options,
25-
propResolvers: [VueResolver, ...(options.propResolvers || [])],
26-
}) as VueHeadClient<UseHeadInput, boolean>
21+
head = _createHead({ render: debouncedRenderer, ...options }) as VueHeadClient<ResolvableHead, boolean>
2722
head.install = vueInstall(head)
2823
return head
2924
}

packages/vue/src/legacy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CreateClientHeadOptions, CreateServerHeadOptions, HeadPluginInput, SSRHeadPayload } from 'unhead/types'
1+
import type { CreateClientHeadOptions, CreateServerHeadOptions, HeadPluginInput, ResolvableHead, SSRHeadPayload } from 'unhead/types'
22
import type { UseHeadInput, VueHeadClient } from './types'
33
import { DeprecationsPlugin } from 'unhead/legacy'
44
import { AliasSortingPlugin, PromisesPlugin, TemplateParamsPlugin } from 'unhead/plugins'
@@ -29,7 +29,7 @@ export const legacyPlugins: HeadPluginInput[] = [DeprecationsPlugin, PromisesPlu
2929
* `legacyPlugins` yourself if you still need v1/v2 tag prop compatibility.
3030
*/
3131
/* @__NO_SIDE_EFFECTS__ */
32-
export function createHead(options: CreateClientHeadOptions = {}): VueHeadClient<UseHeadInput, boolean> {
32+
export function createHead(options: CreateClientHeadOptions = {}): VueHeadClient<ResolvableHead, boolean> {
3333
return _createClientHead({
3434
...options,
3535
plugins: [...legacyPlugins, ...(options.plugins || [])],

packages/vue/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export { VueHeadMixin } from './VueHeadMixin'
88
export { type PreparedTemplate, prepareTemplate, propsToString, renderSSRHead, type SSRHeadPayload, transformHtmlTemplate } from 'unhead/server'
99

1010
/* @__NO_SIDE_EFFECTS__ */
11-
export function createHead(options: Omit<CreateServerHeadOptions, 'propsResolver'> = {}): VueHeadClient<UseHeadInput, SSRHeadPayload> {
11+
export function createHead(options: Omit<CreateServerHeadOptions, 'propResolvers'> = {}): VueHeadClient<UseHeadInput, SSRHeadPayload> {
1212
const head = _createServerHead({
1313
...options,
1414
propResolvers: [VueResolver],

packages/vue/src/stream/client.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import type { CreateStreamableClientHeadOptions, UnheadStreamQueue } from 'unhead/stream/client'
2-
import type { UseHeadInput, VueHeadClient } from '../types'
2+
import type { ResolvableHead } from 'unhead/types'
3+
import type { VueHeadClient } from '../types'
34
import { createStreamableHead as _createStreamableHead } from 'unhead/stream/client'
45
import { vueInstall } from '../install'
5-
import { VueResolver } from '../resolver'
66
import { VueHeadMixin } from '../VueHeadMixin'
77

88
/**
99
* Creates a client head by wrapping the core instance from the iife script.
1010
*/
1111
/* @__NO_SIDE_EFFECTS__ */
12-
export function createStreamableHead(options: CreateStreamableClientHeadOptions = {}): VueHeadClient<UseHeadInput, boolean> | undefined {
13-
const head = _createStreamableHead({
14-
...options,
15-
propResolvers: [VueResolver, ...(options.propResolvers || [])],
16-
}) as VueHeadClient<UseHeadInput, boolean> | undefined
12+
export function createStreamableHead(options: CreateStreamableClientHeadOptions = {}): VueHeadClient<ResolvableHead, boolean> | undefined {
13+
const head = _createStreamableHead(options) as VueHeadClient<ResolvableHead, boolean> | undefined
1714
if (head) {
1815
head.install = vueInstall(head)
1916
}

packages/vue/src/stream/server.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { WebStreamableHeadContext } from 'unhead/stream/server'
2-
import type { CreateStreamableServerHeadOptions, ResolvableHead, SSRHeadPayload } from 'unhead/types'
3-
import type { VueHeadClient } from '../types'
2+
import type { CreateStreamableServerHeadOptions, SSRHeadPayload } from 'unhead/types'
3+
import type { UseHeadInput, VueHeadClient } from '../types'
44
import {
55
createStreamableHead as _createStreamableHead,
66
renderSSRHeadSuspenseChunk,
@@ -13,11 +13,11 @@ import { VueResolver } from '../resolver'
1313
* Vue-specific context returned by createStreamableHead.
1414
* Extends WebStreamableHeadContext with Vue-specific head type.
1515
*/
16-
export interface VueStreamableHeadContext extends Omit<WebStreamableHeadContext<ResolvableHead>, 'head'> {
16+
export interface VueStreamableHeadContext extends Omit<WebStreamableHeadContext<UseHeadInput>, 'head'> {
1717
/**
1818
* The Vue head instance to use with app.use(head)
1919
*/
20-
head: VueHeadClient<any, SSRHeadPayload>
20+
head: VueHeadClient<UseHeadInput, SSRHeadPayload>
2121
}
2222

2323
/**
@@ -52,13 +52,13 @@ export interface VueStreamableHeadContext extends Omit<WebStreamableHeadContext<
5252
* ```
5353
*/
5454
export function createStreamableHead(
55-
options: Omit<CreateStreamableServerHeadOptions, 'propsResolver'> = {},
55+
options: Omit<CreateStreamableServerHeadOptions, 'propResolvers'> = {},
5656
): VueStreamableHeadContext {
5757
const { head } = _createStreamableHead({
5858
...options,
5959
propResolvers: [VueResolver],
6060
})
61-
const vueHead = head as VueHeadClient<any, SSRHeadPayload>
61+
const vueHead = head as VueHeadClient<UseHeadInput, SSRHeadPayload>
6262
vueHead.install = vueInstall(vueHead)
6363

6464
const flushPatch = () => {

packages/vue/src/types/schema.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
MaybeEventFnHandlers,
1313
MetaFlat,
1414
Noscript,
15+
ResolvableHead,
1516
ResolvableTemplateParams,
1617
SchemaAugmentations,
1718
Script,
@@ -146,4 +147,8 @@ export type UseHeadOptions = Omit<HeadEntryOptions, 'head'> & { head?: VueHeadCl
146147

147148
export type UseHeadInput<_Deprecated = never> = ResolvableValue<ReactiveHead>
148149
export type UseSeoMetaInput = ResolvableProperties<MetaFlat> & { title?: ReactiveHead['title'], titleTemplate?: ReactiveHead['titleTemplate'] }
149-
export type VueHeadClient<I = UseHeadInput, R = unknown> = Unhead<I, R> & Plugin
150+
/**
151+
* Client heads accept core-resolvable input by default. Pass Vue reactive input
152+
* through useHead(), which owns dependency tracking and lifecycle cleanup.
153+
*/
154+
export type VueHeadClient<I = ResolvableHead, R = unknown> = Unhead<I, R> & Plugin

packages/vue/test/unit/dom/pushResolver.test.ts

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

packages/vue/test/unit/e2e/basic.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ describe('vue e2e', () => {
141141
})
142142

143143
it('hydration breaking', async () => {
144-
const schema = <ReactiveHead> {
144+
const schema = {
145145
title: 'My amazing site',
146146
htmlAttrs: {
147147
class: 'layout-default',
@@ -179,7 +179,7 @@ describe('vue e2e', () => {
179179
charset: 'utf-8',
180180
},
181181
],
182-
}
182+
} satisfies ReactiveHead
183183
// scenario: we are injecting root head schema which will not have a hydration step,
184184
// but we are also injecting a child head schema which will have a hydration step
185185
const ssrHead = await ssrVueAppWithUnhead(() => {

packages/vue/test/unit/e2e/charset.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,27 @@ import { csrVueAppWithUnhead, ssrVueAppWithUnhead } from '../../util'
88

99
describe('vue e2e charset', () => {
1010
it('ssr / csr hydration', async () => {
11-
const AppSchema: ReactiveHead = {
11+
const AppSchema = {
1212
title: 'My app',
1313
meta: [
1414
{
1515
charset: 'utf-8',
1616
},
1717
],
18-
}
18+
} satisfies ReactiveHead
1919

20-
const IndexSchema: ReactiveHead = {
20+
const IndexSchema = {
2121
title: 'Home page',
2222
meta: [
2323
{
2424
charset: () => 'utf-8',
2525
},
2626
],
27-
}
27+
} satisfies ReactiveHead
2828

29-
const AboutSchema: ReactiveHead = {
29+
const AboutSchema = {
3030
title: 'About page',
31-
}
31+
} satisfies ReactiveHead
3232

3333
// ssr render on the index page
3434
const ssrHead = await ssrVueAppWithUnhead(() => {

packages/vue/test/unit/e2e/keys.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ const SPACE_RE = / /
1212

1313
describe('vue e2e keys', () => {
1414
it('ssr / csr hydration', async () => {
15-
const IndexSchema: ReactiveHead = {
15+
const IndexSchema = {
1616
link: [{ rel: 'icon', href: '/page-index.ico', key: 'main-icon' }],
17-
}
17+
} satisfies ReactiveHead
1818

19-
const AboutSchema: ReactiveHead = {
19+
const AboutSchema = {
2020
link: [{ rel: 'icon', href: '/page-about.ico', key: 'main-icon' }],
21-
}
21+
} satisfies ReactiveHead
2222

2323
// ssr render on the index page
2424
const ssrHead = createServerHead({

0 commit comments

Comments
 (0)