diff --git a/package.json b/package.json index ab777c16ff..8274379cf1 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,9 @@ "@nuxt/vite-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*", "@types/node": "22.10.5", - "@unhead/schema": "2.0.0-alpha.2", - "@unhead/vue": "2.0.0-alpha.2", - "@unhead/shared": "2.0.0-alpha.2", + "@unhead/schema": "2.0.0-alpha.3", + "@unhead/vue": "2.0.0-alpha.3", + "@unhead/shared": "2.0.0-alpha.3", "@vue/compiler-core": "3.5.13", "@vue/compiler-dom": "3.5.13", "@vue/shared": "3.5.13", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 8a2a9a9cce..749ba0a2a5 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -71,7 +71,7 @@ "@nuxt/schema": "workspace:*", "@nuxt/telemetry": "^2.6.4", "@nuxt/vite-builder": "workspace:*", - "@unhead/vue": "^2.0.0-alpha.2", + "@unhead/vue": "^2.0.0-alpha.3", "@vue/shared": "^3.5.13", "acorn": "8.14.0", "c12": "^2.0.1", diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts index c837ba8dfb..4b8704fa25 100644 --- a/packages/nuxt/src/app/composables/index.ts +++ b/packages/nuxt/src/app/composables/index.ts @@ -1,17 +1,3 @@ -import type { UseHeadInput } from '@unhead/vue' -import type { HeadAugmentations } from 'nuxt/schema' - -/** @deprecated Use `UseHeadInput` from `@unhead/vue` instead. This may be removed in a future minor version. */ -export type MetaObject = UseHeadInput -export { - /** @deprecated Import `useHead` from `#imports` instead. This may be removed in a future minor version. */ - useHead, - /** @deprecated Import `useSeoMeta` from `#imports` instead. This may be removed in a future minor version. */ - useSeoMeta, - /** @deprecated Import `useServerSeoMeta` from `#imports` instead. This may be removed in a future minor version. */ - useServerSeoMeta, -} from '@unhead/vue' - export { defineNuxtComponent } from './component' export { useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData } from './asyncData' export type { AsyncDataOptions, AsyncData, AsyncDataRequestStatus } from './asyncData' diff --git a/packages/nuxt/src/head/module.ts b/packages/nuxt/src/head/module.ts index c3434332dd..f9865a5fd9 100644 --- a/packages/nuxt/src/head/module.ts +++ b/packages/nuxt/src/head/module.ts @@ -1,6 +1,7 @@ import { resolve } from 'pathe' import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, tryResolveModule } from '@nuxt/kit' import type { NuxtOptions } from '@nuxt/schema' +import { unheadVueComposablesImports } from '@unhead/vue' import { distDir } from '../dirs' const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head', 'Html', 'Body'] @@ -38,31 +39,35 @@ export default defineNuxtModule({ ] } + const exportPath = resolve(runtimeDir, 'exports', isNuxtV4 ? 'v4' : 'v3') + nuxt.options.alias['#unhead/exports'] = exportPath addImportsSources({ - from: resolve(runtimeDir, 'composables', isNuxtV4 ? 'v4' : 'v3'), - // hard-coded for now we so don't support auto-imports on the deprecated composables - imports: [ - 'injectHead', - 'useHead', - 'useSeoMeta', - 'useHeadSafe', - 'useServerHead', - 'useServerSeoMeta', - 'useServerHeadSafe', - ], + from: exportPath, + imports: unheadVueComposablesImports['@unhead/vue'], }) - const unheadVue = await tryResolveModule('unhead/plugins', nuxt.options.modulesDir) || 'unhead/plugins' + // for Nuxt v3 users we will alias `@unhead/vue` to our custom export path so that + // import { useHead } from '@unhead/vue' + // will work in a context without the Vue app such as Nuxt plugins and such + // for Nuxt v4 user should import from #imports + if (!isNuxtV4) { + for (const subpath of ['legacy', 'types']) { + const subpathModule = `@unhead/vue/${subpath}` + nuxt.options.alias[subpathModule] = await tryResolveModule(subpathModule, nuxt.options.modulesDir) || subpathModule + } + nuxt.options.alias['@unhead/vue'] = exportPath + } addTemplate({ filename: 'unhead-options.mjs', - getContents () { + async getContents () { if (isNuxtV4) { return `export default {}` } + const unheadPlugins = await tryResolveModule('unhead/plugins', nuxt.options.modulesDir) || 'unhead/plugins' // v1 unhead legacy options const disableCapoSorting = !nuxt.options.experimental.headNext - return `import { DeprecationsPlugin, PromisesPlugin } from ${JSON.stringify(unheadVue)}; + return `import { DeprecationsPlugin, PromisesPlugin } from ${JSON.stringify(unheadPlugins)}; export default { disableCapoSorting: ${disableCapoSorting} plugins: [DeprecationsPlugin, PromisesPlugin], diff --git a/packages/nuxt/src/head/runtime/composables/v3.ts b/packages/nuxt/src/head/runtime/composables/v3.ts deleted file mode 100644 index 754251746f..0000000000 --- a/packages/nuxt/src/head/runtime/composables/v3.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ActiveHeadEntry, MergeHead } from '@unhead/schema' -import type { UseHeadInput, UseHeadOptions, UseHeadSafeInput, UseSeoMetaInput } from '@unhead/vue' -import { useHead as head, useHeadSafe as headSafe, useSeoMeta as seoMeta, useServerHead as serverHead, useServerHeadSafe as serverHeadSafe, useServerSeoMeta as serverSeoMeta } from '@unhead/vue' -import { tryUseNuxtApp } from '#app' - -interface NuxtUseHeadOptions extends UseHeadOptions { - nuxt?: any -} - -export function injectHead (nuxtApp?: any) { - return (nuxtApp || tryUseNuxtApp())?.runWithContext(() => injectHead()) -} - -export function useHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { - return (options.nuxt || tryUseNuxtApp()).runWithContext(() => head(input, options)) -} - -export function useHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { - return (options.nuxt || tryUseNuxtApp()).runWithContext(() => headSafe(input, options)) -} - -export function useSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry | void { - return (options.nuxt || tryUseNuxtApp()).runWithContext(() => seoMeta(input, options)) -} - -export function useServerHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { - return (options.nuxt || tryUseNuxtApp()).runWithContext(() => serverHead(input, options)) -} - -export function useServerHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry | void { - return (options.nuxt || tryUseNuxtApp()).runWithContext(() => serverHeadSafe(input, options)) -} - -export function useServerSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { - return (options.nuxt || tryUseNuxtApp()).runWithContext(() => serverSeoMeta(input, options)) -} diff --git a/packages/nuxt/src/head/runtime/composables/v4.ts b/packages/nuxt/src/head/runtime/composables/v4.ts deleted file mode 100644 index 18de7ac524..0000000000 --- a/packages/nuxt/src/head/runtime/composables/v4.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ActiveHeadEntry, MergeHead } from '@unhead/schema' -import type { UseHeadInput, UseHeadOptions, UseHeadSafeInput, UseSeoMetaInput } from '@unhead/vue' -import { useHead as head, useHeadSafe as headSafe, useSeoMeta as seoMeta, useServerHead as serverHead, useServerHeadSafe as serverHeadSafe, useServerSeoMeta as serverSeoMeta } from '@unhead/vue' -import { useNuxtApp } from '#app' - -interface NuxtUseHeadOptions extends UseHeadOptions { - nuxt?: any -} - -export function injectHead (nuxtApp?: any) { - return (nuxtApp || useNuxtApp()).runWithContext(() => injectHead()) -} - -export function useHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> { - return (options.nuxt || useNuxtApp()).runWithContext(() => head(input, options)) -} - -export function useHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> { - return (options.nuxt || useNuxtApp()).runWithContext(() => headSafe(input, options)) -} - -export function useSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry { - return (options.nuxt || useNuxtApp()).runWithContext(() => seoMeta(input, options)) -} - -export function useServerHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> { - return (options.nuxt || useNuxtApp()).runWithContext(() => serverHead(input, options)) -} - -export function useServerHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry { - return (options.nuxt || useNuxtApp()).runWithContext(() => serverHeadSafe(input, options)) -} - -export function useServerSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> { - return (options.nuxt || useNuxtApp()).runWithContext(() => serverSeoMeta(input, options)) -} diff --git a/packages/nuxt/src/head/runtime/exports/v3.ts b/packages/nuxt/src/head/runtime/exports/v3.ts new file mode 100644 index 0000000000..eaef5fa59f --- /dev/null +++ b/packages/nuxt/src/head/runtime/exports/v3.ts @@ -0,0 +1,94 @@ +import type { ActiveHeadEntry, MergeHead } from '@unhead/schema' +import type { UseHeadInput, UseHeadOptions, UseHeadSafeInput, UseSeoMetaInput, + VueHeadClient } from '@unhead/vue' +import { + useHead as head, + useHeadSafe as headSafe, + useSeoMeta as seoMeta, + useServerHead as serverHead, + useServerHeadSafe as serverHeadSafe, + useServerSeoMeta as serverSeoMeta, +} from '@unhead/vue' +import type { UseScriptInput, UseScriptOptions, UseScriptReturn } from '@unhead/vue/legacy' +import { + injectHead as inject, + useScript as script, +} from '@unhead/vue/legacy' +import { tryUseNuxtApp } from '#app' +import type { NuxtApp } from '#app' + +export * from '@unhead/vue/legacy' + +interface NuxtUseHeadOptions extends UseHeadOptions { + nuxt?: NuxtApp +} + +/** + * Injects the head client from the Nuxt context or Vue inject. + * + * In Nuxt v3 this function will not throw an error if the context is missing. + */ +export function injectHead (nuxtApp?: NuxtApp): VueHeadClient | undefined { + const nuxt = nuxtApp || tryUseNuxtApp() + if (nuxt?.ssrContext?.head) { + return nuxt?.ssrContext?.head + } + if (nuxt) { + return nuxt.runWithContext(inject) as VueHeadClient + } + // try use Vue inject + return inject() +} + +export function useHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + if (unhead) { + return head(input, { head: unhead, ...options }) as ActiveHeadEntry> + } +} + +export function useHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + if (unhead) { + return headSafe(input, { head: unhead, ...options }) as ActiveHeadEntry + } +} + +export function useSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry | void { + const unhead = injectHead(options.nuxt) + if (unhead) { + return seoMeta(input, { head: unhead, ...options }) as ActiveHeadEntry + } +} + +export function useServerHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + if (unhead) { + return serverHead(input, { head: unhead, ...options }) as ActiveHeadEntry> + } +} + +export function useServerHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry | void { + const unhead = injectHead(options.nuxt) + if (unhead) { + return serverHeadSafe(input, { head: unhead, ...options }) as ActiveHeadEntry + } +} + +export function useServerSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + if (unhead) { + return serverSeoMeta(input, { head: unhead, ...options }) as ActiveHeadEntry + } +} + +/** + * Aliased for users doing `import { useScript } from '@unhead/vue'` + * @deprecated This will be removed in Nuxt v4. Use `useScript` exported from `@unhead/scripts/vue` + */ +export function useScript = Record> (input: UseScriptInput, options?: UseScriptOptions & { nuxt?: NuxtApp }): UseScriptReturn | void { + const unhead = injectHead(options?.nuxt) + // TODO not sure + // @ts-expect-error untyped + return script(input, { head: unhead, ...options }) +} diff --git a/packages/nuxt/src/head/runtime/exports/v4.ts b/packages/nuxt/src/head/runtime/exports/v4.ts new file mode 100644 index 0000000000..f8cfb5b4db --- /dev/null +++ b/packages/nuxt/src/head/runtime/exports/v4.ts @@ -0,0 +1,58 @@ +import type { ActiveHeadEntry, MergeHead } from '@unhead/schema' +import type { UseHeadInput, UseHeadOptions, UseHeadSafeInput, UseSeoMetaInput, VueHeadClient } from '@unhead/vue' +import { useHead as head, useHeadSafe as headSafe, injectHead as inject, useSeoMeta as seoMeta, useServerHead as serverHead, useServerHeadSafe as serverHeadSafe, useServerSeoMeta as serverSeoMeta } from '@unhead/vue' +import type { NuxtApp } from 'nuxt/app' +import { useNuxtApp } from 'nuxt/app' + +export * from '@unhead/vue' + +interface NuxtUseHeadOptions extends UseHeadOptions { + nuxt?: NuxtApp +} + +/** + * Injects the head client from the Nuxt context or Vue inject. + * + * In Nuxt v3 this function will not throw an error if the context is missing. + */ +export function injectHead (nuxtApp?: NuxtApp): VueHeadClient { + const nuxt = nuxtApp || useNuxtApp() + if (nuxt?.ssrContext?.head) { + return nuxt?.ssrContext?.head + } + if (nuxt) { + return nuxt.runWithContext(inject) as VueHeadClient + } + // try use Vue inject + return inject() +} + +export function useHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + return head(input, { head: unhead, ...options }) as ActiveHeadEntry> +} + +export function useHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + return headSafe(input, { head: unhead, ...options }) as ActiveHeadEntry +} + +export function useSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry | void { + const unhead = injectHead(options.nuxt) + return seoMeta(input, { head: unhead, ...options }) as ActiveHeadEntry +} + +export function useServerHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + return serverHead(input, { head: unhead, ...options }) as ActiveHeadEntry> +} + +export function useServerHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry | void { + const unhead = injectHead(options.nuxt) + return serverHeadSafe(input, { head: unhead, ...options }) as ActiveHeadEntry +} + +export function useServerSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry> | void { + const unhead = injectHead(options.nuxt) + return serverSeoMeta(input, { head: unhead, ...options }) as ActiveHeadEntry +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57a1360322..69f8910a1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,9 @@ overrides: '@nuxt/vite-builder': workspace:* '@nuxt/webpack-builder': workspace:* '@types/node': 22.10.5 - '@unhead/schema': 2.0.0-alpha.2 - '@unhead/vue': 2.0.0-alpha.2 - '@unhead/shared': 2.0.0-alpha.2 + '@unhead/schema': 2.0.0-alpha.3 + '@unhead/vue': 2.0.0-alpha.3 + '@unhead/shared': 2.0.0-alpha.3 '@vue/compiler-core': 3.5.13 '@vue/compiler-dom': 3.5.13 '@vue/shared': 3.5.13 @@ -70,11 +70,11 @@ importers: specifier: 7.5.8 version: 7.5.8 '@unhead/schema': - specifier: 2.0.0-alpha.2 - version: 2.0.0-alpha.2 + specifier: 2.0.0-alpha.3 + version: 2.0.0-alpha.3 '@unhead/vue': - specifier: 2.0.0-alpha.2 - version: 2.0.0-alpha.2(vue@3.5.13(typescript@5.7.3)) + specifier: 2.0.0-alpha.3 + version: 2.0.0-alpha.3(vue@3.5.13(typescript@5.7.3)) '@vitest/coverage-v8': specifier: 2.1.8 version: 2.1.8(vitest@2.1.8(@types/node@22.10.5)(happy-dom@16.5.3)(jiti@2.4.2)(sass@1.78.0)(terser@5.32.0)(tsx@4.19.2)(yaml@2.6.1)) @@ -305,8 +305,8 @@ importers: specifier: 22.10.5 version: 22.10.5 '@unhead/vue': - specifier: 2.0.0-alpha.2 - version: 2.0.0-alpha.2(vue@3.5.13(typescript@5.7.3)) + specifier: 2.0.0-alpha.3 + version: 2.0.0-alpha.3(vue@3.5.13(typescript@5.7.3)) '@vue/shared': specifier: 3.5.13 version: 3.5.13 @@ -650,8 +650,8 @@ importers: specifier: 2.0.10 version: 2.0.10 '@unhead/schema': - specifier: 2.0.0-alpha.2 - version: 2.0.0-alpha.2 + specifier: 2.0.0-alpha.3 + version: 2.0.0-alpha.3 '@vitejs/plugin-vue': specifier: 5.2.1 version: 5.2.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(sass@1.78.0)(terser@5.32.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.13(typescript@5.7.3)) @@ -2792,14 +2792,14 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@unhead/schema@2.0.0-alpha.2': - resolution: {integrity: sha512-Zuo0kqx2jnmxXDefsGEblTwSSXVenYDkMJ5H17lFTyxTIPnscISpvBSEFlPyCit7oTZXQ69wo4URhKiIXOZRyg==} + '@unhead/schema@2.0.0-alpha.3': + resolution: {integrity: sha512-Q9xkdjtBNH0gBp0IzX2FIjRFencMlhQwZgm6L9HtPDtM1UEsOsHZq4f/EsaUyJj82py3aJrbVe6rQfTs1gPTjw==} - '@unhead/shared@2.0.0-alpha.2': - resolution: {integrity: sha512-7hJpzfmnb9md6R3q5oxpX7bPdmTRbXsyaqHJzwaSrm9Q4+1J8wm0o36lwncQ0ssZfYxXynzrLkgY7sqCTSoGjA==} + '@unhead/shared@2.0.0-alpha.3': + resolution: {integrity: sha512-fiqKptv3VrjVASoMGMsLyKAFiVPyArPPr6VXDkhAPVlu1twvs58AhuWF6GJaJr2XJCnamPBj88rUVFO0x4E/9g==} - '@unhead/vue@2.0.0-alpha.2': - resolution: {integrity: sha512-ieMix+zVKp+8grIkokWHgeakS+GSK1NtDs40iC072XVYbtw+5qCKA0Vmkelp31NTH/XRbVHtIqlvbzNCWKT24w==} + '@unhead/vue@2.0.0-alpha.3': + resolution: {integrity: sha512-EpjNAykZ9XffZEPXxWp7iMAKrIordKjDfliaY8Wlz2fbsqbHnucTfTEvmJE+niFpA3pvjxJOd/atAME6MuYzbw==} peerDependencies: vue: 3.5.13 @@ -8982,7 +8982,7 @@ snapshots: '@types/google.maps': 3.58.1 '@types/vimeo__player': 2.18.3 '@types/youtube': 0.1.0 - '@unhead/vue': 2.0.0-alpha.2(vue@3.5.13(typescript@5.7.3)) + '@unhead/vue': 2.0.0-alpha.3(vue@3.5.13(typescript@5.7.3)) '@vueuse/core': 11.1.0(vue@3.5.13(typescript@5.7.3)) consola: 3.4.0 defu: 6.1.4 @@ -9915,20 +9915,20 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@unhead/schema@2.0.0-alpha.2': + '@unhead/schema@2.0.0-alpha.3': dependencies: hookable: 5.5.3 zhead: 2.2.4 - '@unhead/shared@2.0.0-alpha.2': + '@unhead/shared@2.0.0-alpha.3': dependencies: - '@unhead/schema': 2.0.0-alpha.2 + '@unhead/schema': 2.0.0-alpha.3 packrup: 0.1.2 - '@unhead/vue@2.0.0-alpha.2(vue@3.5.13(typescript@5.7.3))': + '@unhead/vue@2.0.0-alpha.3(vue@3.5.13(typescript@5.7.3))': dependencies: - '@unhead/schema': 2.0.0-alpha.2 - '@unhead/shared': 2.0.0-alpha.2 + '@unhead/schema': 2.0.0-alpha.3 + '@unhead/shared': 2.0.0-alpha.3 hookable: 5.5.3 unhead: 2.0.0-alpha.2 vue: 3.5.13(typescript@5.7.3) @@ -15420,8 +15420,8 @@ snapshots: unhead@2.0.0-alpha.2: dependencies: - '@unhead/schema': 2.0.0-alpha.2 - '@unhead/shared': 2.0.0-alpha.2 + '@unhead/schema': 2.0.0-alpha.3 + '@unhead/shared': 2.0.0-alpha.3 hookable: 5.5.3 unicode-emoji-modifier-base@1.0.0: {}