From 83e0df6e60ce6ce715014bd16d4a0083484e8d4d Mon Sep 17 00:00:00 2001 From: Ryota Watanabe <43837308+wattanx@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:14:15 +0900 Subject: [PATCH] refactor: relocate group of functions for clarity (#965) * refactor: relocate group of functions to nuxt.ts for clarity * refactor: relocate group of functions to composabels dir for clarity * refactor: relocate group of functions to composabels dir for clarity * refactor: relocate group of functions to composabels dir for clarity * refactor: relocate group of functions to nuxt.ts for clarity * refactor: relocate group of functions to composabels dir for clarity * refactor: relocate group of functions to composabels dir for clarity * refactor: relocate group of functions to composabels dir for clarity --- packages/bridge/src/imports/presets.ts | 30 ++-- packages/bridge/src/runtime/app.plugin.mjs | 2 +- packages/bridge/src/runtime/app.ts | 88 ----------- packages/bridge/src/runtime/capi.legacy.ts | 5 +- .../runtime/{ => composables}/asyncData.ts | 2 +- .../src/runtime/composables/component.ts | 34 +++++ .../src/runtime/{ => composables}/cookie.ts | 2 +- .../src/runtime/{ => composables}/error.ts | 2 +- .../src/runtime/{ => composables}/fetch.ts | 3 +- .../bridge/src/runtime/composables/index.ts | 8 + .../{composables.ts => composables/router.ts} | 127 +--------------- .../src/runtime/{ => composables}/ssr.ts | 2 +- .../bridge/src/runtime/composables/state.ts | 25 +++ .../bridge/src/runtime/head/plugins/unhead.ts | 2 +- packages/bridge/src/runtime/index.ts | 4 +- packages/bridge/src/runtime/mocks.ts | 5 + packages/bridge/src/runtime/nuxt.ts | 143 ++++++++++++++++++ 17 files changed, 254 insertions(+), 230 deletions(-) rename packages/bridge/src/runtime/{ => composables}/asyncData.ts (99%) create mode 100644 packages/bridge/src/runtime/composables/component.ts rename packages/bridge/src/runtime/{ => composables}/cookie.ts (98%) rename packages/bridge/src/runtime/{ => composables}/error.ts (97%) rename packages/bridge/src/runtime/{ => composables}/fetch.ts (96%) create mode 100644 packages/bridge/src/runtime/composables/index.ts rename packages/bridge/src/runtime/{composables.ts => composables/router.ts} (54%) rename packages/bridge/src/runtime/{ => composables}/ssr.ts (95%) create mode 100644 packages/bridge/src/runtime/composables/state.ts create mode 100644 packages/bridge/src/runtime/mocks.ts create mode 100644 packages/bridge/src/runtime/nuxt.ts diff --git a/packages/bridge/src/imports/presets.ts b/packages/bridge/src/imports/presets.ts index 1d4eb484..e56e1f97 100644 --- a/packages/bridge/src/imports/presets.ts +++ b/packages/bridge/src/imports/presets.ts @@ -20,32 +20,44 @@ export const commonPresets: InlinePreset[] = [ const granularAppPresets: InlinePreset[] = [ { - imports: ['defineNuxtComponent', 'setNuxtAppInstance', 'useNuxtApp', 'defineNuxtPlugin'], - from: '#app/app' + imports: ['setNuxtAppInstance', 'useNuxtApp', 'defineNuxtPlugin', 'useRuntimeConfig', 'useNuxt2Meta'], + from: '#app/nuxt' }, { - imports: ['useRuntimeConfig', 'useNuxt2Meta', 'useRoute', 'useRouter', 'useState', 'abortNavigation', 'addRouteMiddleware', 'defineNuxtRouteMiddleware', 'navigateTo'], - from: '#app/composables' + imports: ['defineNuxtComponent'], + from: '#app/composables/component' + }, + { + imports: ['useRoute', 'useRouter', 'abortNavigation', 'addRouteMiddleware', 'defineNuxtRouteMiddleware', 'navigateTo'], + from: '#app/composables/router' + }, + { + imports: ['useState'], + from: '#app/composables/state' }, { imports: ['useLazyAsyncData', 'refreshNuxtData'], - from: '#app/asyncData' + from: '#app/composables/asyncData' }, { imports: ['clearError', 'createError', 'isNuxtError', 'showError', 'useError', 'throwError'], - from: '#app/error' + from: '#app/composables/error' }, { imports: ['useLazyFetch'], - from: '#app/fetch' + from: '#app/composables/fetch' }, { imports: ['useCookie'], - from: '#app/cookie' + from: '#app/composables/cookie' }, { imports: ['useRequestHeaders', 'useRequestEvent'], - from: '#app/ssr' + from: '#app/composables/ssr' + }, + { + imports: ['useAsyncData', 'useFetch', 'useHydration'], + from: '#app/mocks' } ] diff --git a/packages/bridge/src/runtime/app.plugin.mjs b/packages/bridge/src/runtime/app.plugin.mjs index 4f39296e..62d118d0 100644 --- a/packages/bridge/src/runtime/app.plugin.mjs +++ b/packages/bridge/src/runtime/app.plugin.mjs @@ -1,6 +1,6 @@ import Vue, { version } from 'vue' import { createHooks } from 'hookable' -import { setNuxtAppInstance } from '#app/app' +import { setNuxtAppInstance } from '#app/nuxt' import { globalMiddleware } from '#build/global-middleware' // Reshape payload to match key `useLazyAsyncData` expects diff --git a/packages/bridge/src/runtime/app.ts b/packages/bridge/src/runtime/app.ts index 9a8e385a..aeda1929 100644 --- a/packages/bridge/src/runtime/app.ts +++ b/packages/bridge/src/runtime/app.ts @@ -1,90 +1,2 @@ -import type { DefineComponent } from 'vue' -import { useHead } from '@unhead/vue' -import type { NuxtAppCompat } from '@nuxt/bridge-schema' -import { defineComponent, getCurrentInstance } from './composables' - export const isVue2 = true export const isVue3 = false - -export const defineNuxtComponent: typeof defineComponent = -function defineNuxtComponent (...args: any[]): any { - const [options, key] = args - const { setup, head, ...opts } = options - - // Avoid wrapping if no options api is used - if (!setup && !options.asyncData && !options.head) { - return { - ...options - } - } - - return { - _fetchKeyBase: key, - ...opts, - setup (props, ctx) { - const nuxtApp = useNuxtApp() - const res = setup ? callWithNuxt(nuxtApp, setup, [props, ctx]) : {} - - if (options.head) { - const nuxtApp = useNuxtApp() - useHead(typeof options.head === 'function' ? () => options.head(nuxtApp) : options.head) - } - - return res - } - } as DefineComponent -} - -export interface Context { - $_nuxtApp: NuxtAppCompat -} - -let currentNuxtAppInstance: NuxtAppCompat | null - -export const setNuxtAppInstance = (nuxt: NuxtAppCompat | null) => { - currentNuxtAppInstance = nuxt -} - -/** - * Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`. - * @param nuxt A Nuxt instance - * @param setup The function to call - */ -export function callWithNuxt any> (nuxt: NuxtAppCompat, setup: T, args?: Parameters) { - setNuxtAppInstance(nuxt) - const p: ReturnType = args ? setup(...args as Parameters) : setup() - if (process.server) { - // Unset nuxt instance to prevent context-sharing in server-side - setNuxtAppInstance(null) - } - return p -} - -interface Plugin { - (nuxt: NuxtAppCompat): Promise | Promise<{ provide?: Record }> | void | { provide?: Record } -} - -export function defineNuxtPlugin (plugin: Plugin): (ctx: Context, inject: (id: string, value: any) => void) => void { - return async (ctx, inject) => { - const result = await callWithNuxt(ctx.$_nuxtApp, plugin, [ctx.$_nuxtApp]) - if (result && result.provide) { - for (const key in result.provide) { - inject(key, result.provide[key]) - } - } - return result - } -} - -export const useNuxtApp = (): NuxtAppCompat => { - const vm = getCurrentInstance() - - if (!vm) { - if (!currentNuxtAppInstance) { - throw new Error('nuxt app instance unavailable') - } - return currentNuxtAppInstance - } - - return vm.proxy.$_nuxtApp -} diff --git a/packages/bridge/src/runtime/capi.legacy.ts b/packages/bridge/src/runtime/capi.legacy.ts index 68b26a30..1bc0a9e9 100644 --- a/packages/bridge/src/runtime/capi.legacy.ts +++ b/packages/bridge/src/runtime/capi.legacy.ts @@ -2,8 +2,9 @@ import { defu } from 'defu' import { ComputedRef, computed, getCurrentInstance as getVM, isReactive, isRef, onBeforeMount, onServerPrefetch, reactive, ref, set, shallowRef, toRaw, toRefs, watch } from 'vue' import type { Route } from 'vue-router' import type { Nuxt2Context } from '@nuxt/bridge-schema' -import { useNuxtApp } from './app' -import { useRouter as _useRouter, useRoute as _useRoute, useState } from './composables' +import { useNuxtApp } from './nuxt' +import { useRouter as _useRouter, useRoute as _useRoute } from './composables/router' +import { useState } from './composables/state' // @ts-expect-error virtual file import { isFullStatic } from '#build/composition-globals.mjs' diff --git a/packages/bridge/src/runtime/asyncData.ts b/packages/bridge/src/runtime/composables/asyncData.ts similarity index 99% rename from packages/bridge/src/runtime/asyncData.ts rename to packages/bridge/src/runtime/composables/asyncData.ts index d0ceaabb..8368a086 100644 --- a/packages/bridge/src/runtime/asyncData.ts +++ b/packages/bridge/src/runtime/composables/asyncData.ts @@ -1,7 +1,7 @@ import { onBeforeMount, onServerPrefetch, onUnmounted, ref, getCurrentInstance, watch } from 'vue' import type { Ref, WatchSource } from 'vue' import type { NuxtAppCompat } from '@nuxt/bridge-schema' -import { useNuxtApp } from './app' +import { useNuxtApp } from '../nuxt' export type _Transform = (input: Input) => Output diff --git a/packages/bridge/src/runtime/composables/component.ts b/packages/bridge/src/runtime/composables/component.ts new file mode 100644 index 00000000..17c6236c --- /dev/null +++ b/packages/bridge/src/runtime/composables/component.ts @@ -0,0 +1,34 @@ + +import { defineComponent } from 'vue' +import type { DefineComponent } from 'vue' +import { useHead } from '@unhead/vue' +import { useNuxtApp, callWithNuxt } from '../nuxt' + +export const defineNuxtComponent: typeof defineComponent = +function defineNuxtComponent (...args: any[]): any { + const [options, key] = args + const { setup, head, ...opts } = options + + // Avoid wrapping if no options api is used + if (!setup && !options.asyncData && !options.head) { + return { + ...options + } + } + + return { + _fetchKeyBase: key, + ...opts, + setup (props, ctx) { + const nuxtApp = useNuxtApp() + const res = setup ? callWithNuxt(nuxtApp, setup, [props, ctx]) : {} + + if (options.head) { + const nuxtApp = useNuxtApp() + useHead(typeof options.head === 'function' ? () => options.head(nuxtApp) : options.head) + } + + return res + } + } as DefineComponent +} diff --git a/packages/bridge/src/runtime/cookie.ts b/packages/bridge/src/runtime/composables/cookie.ts similarity index 98% rename from packages/bridge/src/runtime/cookie.ts rename to packages/bridge/src/runtime/composables/cookie.ts index 6e4d2596..7a6eee29 100644 --- a/packages/bridge/src/runtime/cookie.ts +++ b/packages/bridge/src/runtime/composables/cookie.ts @@ -3,7 +3,7 @@ import { parse, serialize, CookieParseOptions, CookieSerializeOptions } from 'co import { appendHeader } from 'h3' import type { H3Event } from 'h3' import destr from 'destr' -import { useNuxtApp } from './app' +import { useNuxtApp } from '../nuxt' import { useRequestEvent } from './ssr' type _CookieOptions = Omit diff --git a/packages/bridge/src/runtime/error.ts b/packages/bridge/src/runtime/composables/error.ts similarity index 97% rename from packages/bridge/src/runtime/error.ts rename to packages/bridge/src/runtime/composables/error.ts index 3cd6bde4..c7f8f8fd 100644 --- a/packages/bridge/src/runtime/error.ts +++ b/packages/bridge/src/runtime/composables/error.ts @@ -1,6 +1,6 @@ import { createError as _createError, H3Error } from 'h3' import { toRef } from 'vue' -import { useNuxtApp } from './app' +import { useNuxtApp } from '../nuxt' export const useError = () => toRef(useNuxtApp().payload, 'error') diff --git a/packages/bridge/src/runtime/fetch.ts b/packages/bridge/src/runtime/composables/fetch.ts similarity index 96% rename from packages/bridge/src/runtime/fetch.ts rename to packages/bridge/src/runtime/composables/fetch.ts index 3b6749aa..edfb1fcb 100644 --- a/packages/bridge/src/runtime/fetch.ts +++ b/packages/bridge/src/runtime/composables/fetch.ts @@ -1,7 +1,8 @@ import type { FetchOptions, FetchRequest } from 'ofetch' import type { TypedInternalResponse } from 'nitropack' import { hash } from 'ohash' -import { computed, isRef, Ref } from 'vue' +import { computed, isRef } from 'vue' +import type { Ref } from 'vue' import type { AsyncDataOptions, _Transform, KeyOfRes } from './asyncData' import { useAsyncData } from './asyncData' diff --git a/packages/bridge/src/runtime/composables/index.ts b/packages/bridge/src/runtime/composables/index.ts new file mode 100644 index 00000000..0a22a6ad --- /dev/null +++ b/packages/bridge/src/runtime/composables/index.ts @@ -0,0 +1,8 @@ +export { useLazyAsyncData, refreshNuxtData } from './asyncData' +export * from './component' +export { useCookie } from './cookie' +export { clearError, createError, isNuxtError, throwError, showError, useError } from './error' +export { useLazyFetch } from './fetch' +export * from './router' +export { useRequestHeaders, useRequestEvent } from './ssr' +export * from './state' diff --git a/packages/bridge/src/runtime/composables.ts b/packages/bridge/src/runtime/composables/router.ts similarity index 54% rename from packages/bridge/src/runtime/composables.ts rename to packages/bridge/src/runtime/composables/router.ts index e5199732..22fe890f 100644 --- a/packages/bridge/src/runtime/composables.ts +++ b/packages/bridge/src/runtime/composables/router.ts @@ -1,42 +1,13 @@ -import { getCurrentInstance, onBeforeUnmount, isRef, watch, reactive, toRef, isReactive, Ref, set } from 'vue' -import type { CombinedVueInstance } from 'vue/types/vue' -import type { MetaInfo } from 'vue-meta' +import { getCurrentInstance, reactive } from 'vue' import type VueRouter from 'vue-router' import type { Location, RawLocation, Route, NavigationFailure } from 'vue-router' -import type { RuntimeConfig } from '@nuxt/schema' import { sendRedirect } from 'h3' -import { defu } from 'defu' import { useRouter as useVueRouter, useRoute as useVueRoute } from 'vue-router/composables' import { hasProtocol, joinURL, parseURL } from 'ufo' -import { useNuxtApp, callWithNuxt } from './app' +import { useNuxtApp, callWithNuxt, useRuntimeConfig } from '../nuxt' import { createError, showError } from './error' import type { NuxtError } from './error' -export { useLazyAsyncData, refreshNuxtData } from './asyncData' -export { useLazyFetch } from './fetch' -export { useCookie } from './cookie' -export { clearError, createError, isNuxtError, throwError, showError, useError } from './error' -export { useRequestHeaders, useRequestEvent } from './ssr' - -export * from 'vue' - -const mock = () => () => { throw new Error('not implemented') } - -export const useAsyncData = mock() -export const useFetch = mock() -export const useHydration = mock() - -// Runtime config helper -export const useRuntimeConfig = () => { - const nuxtApp = useNuxtApp() - if (nuxtApp._config) { - return nuxtApp._config as RuntimeConfig - } - - nuxtApp._config = reactive(nuxtApp.$config) - return nuxtApp._config as RuntimeConfig -} - // Auto-import equivalents for `vue-router` export const useRouter = () => { if (getCurrentInstance()) { @@ -66,98 +37,6 @@ export const useRoute = () => { return nuxtApp._route as Route } -// payload.state is used for vuex by nuxt 2 -export const useState = (key: string, init?: (() => T)): Ref => { - const nuxtApp = useNuxtApp() - if (!nuxtApp.payload.useState) { - nuxtApp.payload.useState = {} - } - if (!isReactive(nuxtApp.payload.useState)) { - nuxtApp.payload.useState = reactive(nuxtApp.payload.useState) - } - - // see @vuejs/composition-api reactivity tracking on a reactive object with set - if (!(key in nuxtApp.payload.useState)) { - set(nuxtApp.payload.useState, key, undefined) - } - - const state = toRef(nuxtApp.payload.useState, key) - if (state.value === undefined && init) { - state.value = init() - } - return state -} - -type Reffed> = { - [P in keyof T]: T[P] extends Array ? Ref>> | Array> : T[P] extends Record ? Reffed | Ref> : T[P] | Ref -} - -function unwrap (value: any): Record { - if (!value || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') { return value } - if (Array.isArray(value)) { return value.map(i => unwrap(i)) } - if (isRef(value)) { return unwrap(value.value) } - if (typeof value === 'object') { - return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, unwrap(value)])) - } - return value -} - -type AugmentedComponent = CombinedVueInstance> & { - _vueMeta?: boolean - $metaInfo?: MetaInfo -} - -/** internal */ -function metaInfoFromOptions (metaOptions: Reffed | (() => Reffed)) { - return metaOptions instanceof Function ? metaOptions : () => metaOptions -} - -export const useNuxt2Meta = (metaOptions: Reffed | (() => Reffed)) => { - let vm: AugmentedComponent | null = null - try { - vm = getCurrentInstance()!.proxy as AugmentedComponent - const meta = vm.$meta() - const $root = vm.$root - - if (!vm._vueMeta) { - vm._vueMeta = true - - let parent = vm.$parent as AugmentedComponent - while (parent && parent !== $root) { - if (parent._vueMeta === undefined) { - parent._vueMeta = false - } - parent = parent.$parent - } - } - // @ts-ignore - vm.$options.head = vm.$options.head || {} - - const unwatch = watch(metaInfoFromOptions(metaOptions), (metaInfo: MetaInfo) => { - vm.$metaInfo = { - ...vm.$metaInfo || {}, - ...unwrap(metaInfo) - } - if (process.client) { - meta.refresh() - } - }, { immediate: true, deep: true }) - - onBeforeUnmount(unwatch) - } catch { - const app = (useNuxtApp().nuxt2Context as any).app - if (typeof app.head === 'function') { - const originalHead = app.head - app.head = function () { - const head = originalHead.call(this) || {} - return defu(unwrap(metaInfoFromOptions(metaOptions)()), head) - } - } else { - app.head = defu(unwrap(metaInfoFromOptions(metaOptions)()), app.head) - } - } -} - export interface AddRouteMiddlewareOptions { global?: boolean } @@ -218,6 +97,8 @@ export const navigateTo = (to: RawLocation | undefined | null, options?: Navigat const nuxtApp = useNuxtApp() if (nuxtApp.ssrContext && nuxtApp.ssrContext.event) { const redirectLocation = isExternal ? toPath : joinURL(useRuntimeConfig().app.baseURL, router.resolve(to).resolved.fullPath || '/') + + // @ts-expect-error return nuxtApp.callHook('app:redirected').then(() => sendRedirect(nuxtApp.ssrContext!.event, redirectLocation, options?.redirectCode || 302)) } } diff --git a/packages/bridge/src/runtime/ssr.ts b/packages/bridge/src/runtime/composables/ssr.ts similarity index 95% rename from packages/bridge/src/runtime/ssr.ts rename to packages/bridge/src/runtime/composables/ssr.ts index d5428f80..ec34cd42 100644 --- a/packages/bridge/src/runtime/ssr.ts +++ b/packages/bridge/src/runtime/composables/ssr.ts @@ -1,6 +1,6 @@ import type { H3Event } from 'h3' import type { NuxtAppCompat } from '@nuxt/bridge-schema' -import { useNuxtApp } from './app' +import { useNuxtApp } from '../nuxt' export function useRequestHeaders (include: K[]): Record export function useRequestHeaders (): Readonly> diff --git a/packages/bridge/src/runtime/composables/state.ts b/packages/bridge/src/runtime/composables/state.ts new file mode 100644 index 00000000..78b2d917 --- /dev/null +++ b/packages/bridge/src/runtime/composables/state.ts @@ -0,0 +1,25 @@ + +import { reactive, toRef, isReactive, Ref, set } from 'vue' +import { useNuxtApp } from '../nuxt' + +// payload.state is used for vuex by nuxt 2 +export const useState = (key: string, init?: (() => T)): Ref => { + const nuxtApp = useNuxtApp() + if (!nuxtApp.payload.useState) { + nuxtApp.payload.useState = {} + } + if (!isReactive(nuxtApp.payload.useState)) { + nuxtApp.payload.useState = reactive(nuxtApp.payload.useState) + } + + // see @vuejs/composition-api reactivity tracking on a reactive object with set + if (!(key in nuxtApp.payload.useState)) { + set(nuxtApp.payload.useState, key, undefined) + } + + const state = toRef(nuxtApp.payload.useState, key) + if (state.value === undefined && init) { + state.value = init() + } + return state +} diff --git a/packages/bridge/src/runtime/head/plugins/unhead.ts b/packages/bridge/src/runtime/head/plugins/unhead.ts index c68cd103..1c93258c 100644 --- a/packages/bridge/src/runtime/head/plugins/unhead.ts +++ b/packages/bridge/src/runtime/head/plugins/unhead.ts @@ -6,7 +6,7 @@ import { } from '@unhead/vue' import { markRaw } from 'vue' import { renderSSRHead } from '@unhead/ssr' -import { defineNuxtPlugin, useNuxtApp } from '../../app' +import { defineNuxtPlugin, useNuxtApp } from '../../nuxt' // @ts-ignore import metaConfig from '#build/meta.config.mjs' diff --git a/packages/bridge/src/runtime/index.ts b/packages/bridge/src/runtime/index.ts index 28f5914f..deff001f 100644 --- a/packages/bridge/src/runtime/index.ts +++ b/packages/bridge/src/runtime/index.ts @@ -1,2 +1,4 @@ +export * from './composables/index' export * from './app' -export * from './composables' +export * from './nuxt' +export * from './mocks' diff --git a/packages/bridge/src/runtime/mocks.ts b/packages/bridge/src/runtime/mocks.ts new file mode 100644 index 00000000..1464f840 --- /dev/null +++ b/packages/bridge/src/runtime/mocks.ts @@ -0,0 +1,5 @@ +const mock = () => () => { throw new Error('not implemented') } + +export const useAsyncData = mock() +export const useFetch = mock() +export const useHydration = mock() diff --git a/packages/bridge/src/runtime/nuxt.ts b/packages/bridge/src/runtime/nuxt.ts new file mode 100644 index 00000000..79601ea3 --- /dev/null +++ b/packages/bridge/src/runtime/nuxt.ts @@ -0,0 +1,143 @@ +import type { RuntimeConfig } from '@nuxt/schema' +import type { NuxtAppCompat } from '@nuxt/bridge-schema' +import { getCurrentInstance, reactive, onBeforeUnmount, watch, isRef } from 'vue' +import type { Ref } from 'vue' +import type { CombinedVueInstance } from 'vue/types/vue' +import type { MetaInfo } from 'vue-meta' +import { defu } from 'defu' + +export interface Context { + $_nuxtApp: NuxtAppCompat +} + +type AugmentedComponent = CombinedVueInstance> & { + _vueMeta?: boolean + $metaInfo?: MetaInfo +} + +type Reffed> = { + [P in keyof T]: T[P] extends Array ? Ref>> | Array> : T[P] extends Record ? Reffed | Ref> : T[P] | Ref +} + +function unwrap (value: any): Record { + if (!value || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') { return value } + if (Array.isArray(value)) { return value.map(i => unwrap(i)) } + if (isRef(value)) { return unwrap(value.value) } + if (typeof value === 'object') { + return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, unwrap(value)])) + } + return value +} + +/** internal */ +function metaInfoFromOptions (metaOptions: Reffed | (() => Reffed)) { + return metaOptions instanceof Function ? metaOptions : () => metaOptions +} + +export const useNuxt2Meta = (metaOptions: Reffed | (() => Reffed)) => { + let vm: AugmentedComponent | null = null + try { + vm = getCurrentInstance()!.proxy as AugmentedComponent + const meta = vm.$meta() + const $root = vm.$root + + if (!vm._vueMeta) { + vm._vueMeta = true + + let parent = vm.$parent as AugmentedComponent + while (parent && parent !== $root) { + if (parent._vueMeta === undefined) { + parent._vueMeta = false + } + parent = parent.$parent + } + } + // @ts-ignore + vm.$options.head = vm.$options.head || {} + + const unwatch = watch(metaInfoFromOptions(metaOptions), (metaInfo: MetaInfo) => { + vm.$metaInfo = { + ...vm.$metaInfo || {}, + ...unwrap(metaInfo) + } + if (process.client) { + meta.refresh() + } + }, { immediate: true, deep: true }) + + onBeforeUnmount(unwatch) + } catch { + const app = (useNuxtApp().nuxt2Context as any).app + if (typeof app.head === 'function') { + const originalHead = app.head + app.head = function () { + const head = originalHead.call(this) || {} + return defu(unwrap(metaInfoFromOptions(metaOptions)()), head) + } + } else { + app.head = defu(unwrap(metaInfoFromOptions(metaOptions)()), app.head) + } + } +} + +let currentNuxtAppInstance: NuxtAppCompat | null + +export const setNuxtAppInstance = (nuxt: NuxtAppCompat | null) => { + currentNuxtAppInstance = nuxt +} + +/** + * Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`. + * @param nuxt A Nuxt instance + * @param setup The function to call + */ +export function callWithNuxt any> (nuxt: NuxtAppCompat, setup: T, args?: Parameters) { + setNuxtAppInstance(nuxt) + const p: ReturnType = args ? setup(...args as Parameters) : setup() + if (process.server) { + // Unset nuxt instance to prevent context-sharing in server-side + setNuxtAppInstance(null) + } + return p +} + +interface Plugin { + (nuxt: NuxtAppCompat): Promise | Promise<{ provide?: Record }> | void | { provide?: Record } +} + +export function defineNuxtPlugin (plugin: Plugin): (ctx: Context, inject: (id: string, value: any) => void) => void { + return async (ctx, inject) => { + const result = await callWithNuxt(ctx.$_nuxtApp, plugin, [ctx.$_nuxtApp]) + if (result && result.provide) { + for (const key in result.provide) { + inject(key, result.provide[key]) + } + } + return result + } +} + +export const useNuxtApp = (): NuxtAppCompat => { + const vm = getCurrentInstance() + + if (!vm) { + if (!currentNuxtAppInstance) { + throw new Error('nuxt app instance unavailable') + } + return currentNuxtAppInstance + } + + // @ts-ignore + return vm.proxy.$_nuxtApp +} + +// Runtime config helper +export const useRuntimeConfig = () => { + const nuxtApp = useNuxtApp() + if (nuxtApp._config) { + return nuxtApp._config as RuntimeConfig + } + + nuxtApp._config = reactive(nuxtApp.$config) + return nuxtApp._config as RuntimeConfig +}