Skip to content

Commit 9a77c3d

Browse files
committed
fix(vue): export Head component
1 parent f2a140b commit 9a77c3d

File tree

12 files changed

+150
-64
lines changed

12 files changed

+150
-64
lines changed

packages/vue/src/components.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { defineComponent } from 'vue'
2+
import type { VNode } from 'vue'
3+
import { Vue2 } from './env'
4+
import type { ReactiveHead, UseHeadInput } from './types'
5+
import { useHead } from './composables'
6+
7+
export type Props = Readonly<Record<string, any>>
8+
9+
const addVNodeToHeadObj = (node: VNode, obj: ReactiveHead) => {
10+
// @ts-expect-error vue2 vnode API
11+
const nodeType = Vue2 ? node.tag : node.type
12+
const type
13+
= nodeType === 'html'
14+
? 'htmlAttrs'
15+
: nodeType === 'body'
16+
? 'bodyAttrs'
17+
: (nodeType as keyof ReactiveHead)
18+
19+
if (typeof type !== 'string' || !(type in obj))
20+
return
21+
22+
// @ts-expect-error untyped
23+
const nodeData = Vue2 ? node.data : node
24+
const props: Record<string, any> = (Vue2 ? nodeData.attrs : node.props) || {}
25+
// handle class and style attrs
26+
if (Vue2) {
27+
if (nodeData.staticClass)
28+
props.class = nodeData.staticClass
29+
if (nodeData.staticStyle)
30+
props.style = Object.entries(nodeData.staticStyle).map(([key, value]) => `${key}:${value}`).join(';')
31+
}
32+
if (node.children) {
33+
const childrenAttr = Vue2 ? 'text' : 'children'
34+
props.children = Array.isArray(node.children)
35+
// @ts-expect-error untyped
36+
? node.children[0]![childrenAttr]
37+
// @ts-expect-error vue2 vnode API
38+
: node[childrenAttr]
39+
}
40+
if (Array.isArray(obj[type])) {
41+
// @ts-expect-error untyped
42+
obj[type].push(props)
43+
}
44+
else if (type === 'title') { obj.title = props.children }
45+
else {
46+
// @ts-expect-error untyped
47+
obj[type] = props
48+
}
49+
}
50+
51+
const vnodesToHeadObj = (nodes: VNode[]) => {
52+
const obj: UseHeadInput = {
53+
title: undefined,
54+
htmlAttrs: undefined,
55+
bodyAttrs: undefined,
56+
base: undefined,
57+
meta: [],
58+
link: [],
59+
style: [],
60+
script: [],
61+
noscript: [],
62+
}
63+
for (const node of nodes) {
64+
if (typeof node.type === 'symbol' && Array.isArray(node.children)) {
65+
for (const childNode of node.children)
66+
addVNodeToHeadObj(childNode as VNode, obj)
67+
}
68+
else {
69+
addVNodeToHeadObj(node, obj)
70+
}
71+
}
72+
return obj
73+
}
74+
75+
export const Head = /* @__PURE__ */ defineComponent({
76+
// eslint-disable-next-line vue/no-reserved-component-names
77+
name: 'Head',
78+
79+
setup(_, { slots }) {
80+
return () => {
81+
// @ts-expect-error untyped
82+
useHead(() => vnodesToHeadObj(slots.default()))
83+
return null
84+
}
85+
},
86+
})

packages/vue/src/composables.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { asArray } from 'unhead'
22
import type { Arrayable, HeadEntryOptions } from 'unhead'
3-
import type { Link, Meta, Noscript, ReactiveHead, Script, Style } from './types'
4-
import { IS_CLIENT } from './env'
3+
import type { Link, Meta, Noscript, ReactiveHead, Script, Style, UseHeadInput } from './types'
4+
import { IsClient } from './env'
55
import { useHead as _serverUseHead, useServerHead as _serverUseServerHead } from './runtime/server'
66
import { useHead as _clientUseHead } from './runtime/client'
77

88
export function useServerHead(input: ReactiveHead, options: HeadEntryOptions = {}) {
9-
if (!IS_CLIENT)
9+
if (!IsClient)
1010
_serverUseServerHead(input, options)
1111
}
1212

13-
export function useHead(input: ReactiveHead, options: HeadEntryOptions = {}) {
14-
if ((options.mode === 'server' && IS_CLIENT) || (options.mode === 'client' && !IS_CLIENT))
13+
export function useHead(input: UseHeadInput, options: HeadEntryOptions = {}) {
14+
if ((options.mode === 'server' && IsClient) || (options.mode === 'client' && !IsClient))
1515
return
1616

17-
return IS_CLIENT ? _clientUseHead(input, options) : _serverUseHead(input, options)
17+
return IsClient ? _clientUseHead(input, options) : _serverUseHead(input, options)
1818
}
1919

2020
export const useTitle = (title: ReactiveHead['title']) => useHead({ title })

packages/vue/src/createHead.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
import type { App, InjectionKey, Plugin } from 'vue'
2-
import { getCurrentInstance, inject, version } from 'vue'
3-
import {HydratesStatePlugin, createHead as _createHead, getActiveHead, CreateHeadOptions} from 'unhead'
4-
import type { HeadClient } from 'unhead'
2+
import { getCurrentInstance, inject } from 'vue'
3+
import { HydratesStatePlugin, createHead as _createHead, getActiveHead } from 'unhead'
4+
import type { CreateHeadOptions, HeadClient } from 'unhead'
55
import { VueReactiveInputPlugin } from './vueReactiveInputPlugin'
6-
import type { ReactiveHead } from './types'
6+
import type { ReactiveHead, UseHeadInput } from './types'
7+
import { Vue3 } from './env'
78

8-
export const Vue3 = version.startsWith('3')
9-
10-
export type VueHeadClient = HeadClient<ReactiveHead> & Plugin
9+
export type VueHeadClient = HeadClient<UseHeadInput> & Plugin
1110

1211
export const headSymbol = Symbol('head') as InjectionKey<VueHeadClient>
1312

1413
export function injectHead() {
1514
return ((getCurrentInstance() && inject(headSymbol)) || getActiveHead<ReactiveHead>()) as VueHeadClient
1615
}
1716

18-
export function createHead<T>(options: CreateHeadOptions<T>): VueHeadClient {
19-
const head = _createHead<ReactiveHead>({
17+
export function createHead<T>(options: CreateHeadOptions<T> = {}): VueHeadClient {
18+
const head = _createHead<UseHeadInput>({
2019
...options,
2120
plugins: [
2221
HydratesStatePlugin,
2322
VueReactiveInputPlugin,
24-
...(options.plugins || [])
23+
...(options?.plugins || []),
2524
],
2625
})
2726

packages/vue/src/env.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export const IS_CLIENT = typeof window !== 'undefined'
1+
import { version } from 'vue'
2+
3+
export const Vue3 = version.startsWith('3')
4+
export const Vue2 = version.startsWith('2.')
5+
6+
export const IsClient = typeof window !== 'undefined'

packages/vue/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './types'
22
export * from './utils'
33
export * from './vueReactiveInputPlugin'
44
export * from './composables'
5+
export * from './components'
56
export * from './createHead'
67

78
export { renderDOMHead, debouncedRenderDOMHead } from 'unhead/client'

packages/vue/src/runtime/alias/composables.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { asArray } from 'unhead'
22
import type { Arrayable, HeadEntryOptions } from 'unhead'
3-
import type { Link, Meta, Noscript, ReactiveHead, Script, Style } from '../../types'
3+
import type { Link, Meta, Noscript, ReactiveHead, Script, Style, UseHeadInput } from '../../types'
44
import { useHead as _useHead, useServerHead as _useServerHead } from '#head-runtime'
55

6-
export const useServerHead = (input: ReactiveHead, options: HeadEntryOptions = {}) => _useServerHead(input, options)
6+
export const useServerHead = (input: UseHeadInput, options: HeadEntryOptions = {}) => _useServerHead(input, options)
77

8-
export const useHead = (input: ReactiveHead, options: HeadEntryOptions = {}) => _useHead(input, options)
8+
export const useHead = (input: UseHeadInput, options: HeadEntryOptions = {}) => _useHead(input, options)
99

1010
export const useTitle = (title: ReactiveHead['title']) => useHead({ title })
1111

packages/vue/src/runtime/client/useHead.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import type { Ref } from 'vue'
22
import { getCurrentInstance, nextTick, onBeforeUnmount, ref, watch, watchEffect } from 'vue'
33
import { debouncedRenderDOMHead } from 'unhead/client'
44
import type { ActiveHeadEntry, HeadEntryOptions } from 'unhead'
5+
import type { UseHeadInput } from '../../index'
56
import { injectHead, resolveUnrefHeadInput } from '../../index'
67
import type {
78
ReactiveHead,
89
} from '../../types'
910

10-
export function useHead(input: ReactiveHead, options: HeadEntryOptions = {}) {
11+
export function useHead(input: UseHeadInput, options: HeadEntryOptions = {}) {
1112
const head = injectHead()
1213

1314
const vm = getCurrentInstance()
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type { HeadEntryOptions } from 'unhead'
2-
import type {
3-
ReactiveHead,
4-
} from '../../types'
2+
import type { UseHeadInput } from '../../types'
53

64
// eslint-ignore-next-line @typescript-eslint/no-unused-vars
7-
export function useServerHead(input: ReactiveHead, options: HeadEntryOptions = {}) {
5+
export function useServerHead(input: UseHeadInput, options: HeadEntryOptions = {}) {
86
// intentionally left blank
97
}
108

packages/vue/src/runtime/server/useHead.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type { HeadEntryOptions } from 'unhead'
2+
import type { UseHeadInput } from '../..'
23
import { injectHead } from '../..'
3-
import type {
4-
ReactiveHead,
5-
} from '../../types'
64

7-
export function useHead(input: ReactiveHead, options: HeadEntryOptions = {}) {
5+
export function useHead(input: UseHeadInput, options: HeadEntryOptions = {}) {
86
const head = injectHead()
97
head.push(input, options)
108
}

packages/vue/src/runtime/server/useServerHead.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type { HeadEntryOptions } from 'unhead'
2-
import type {
3-
ReactiveHead,
4-
} from '../../types'
2+
import type { UseHeadInput } from '../../types'
53
import { useHead } from './index'
64

7-
export function useServerHead(input: ReactiveHead, options: HeadEntryOptions = {}) {
5+
export function useServerHead(input: UseHeadInput, options: HeadEntryOptions = {}) {
86
// ensure server mode
97
useHead(input, { ...options, mode: 'server' })
108
}

0 commit comments

Comments
 (0)