mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-22 16:39:58 +00:00
560 lines
15 KiB
JavaScript
560 lines
15 KiB
JavaScript
import defu from 'defu'
|
|
import { computed, getCurrentInstance as getVM, isReactive, isRef, onBeforeMount, onServerPrefetch, reactive, ref, set, shallowRef, toRaw, toRefs, watch } from '@vue/composition-api'
|
|
import { useNuxtApp } from './app'
|
|
import { useRouter as _useRouter, useState } from './composables'
|
|
|
|
// Vue composition API export
|
|
export {
|
|
computed,
|
|
createApp,
|
|
createRef,
|
|
customRef,
|
|
defineAsyncComponent,
|
|
del,
|
|
effectScope,
|
|
getCurrentInstance,
|
|
getCurrentScope,
|
|
h,
|
|
inject,
|
|
isRaw,
|
|
isReactive,
|
|
isReadonly,
|
|
isRef,
|
|
markRaw,
|
|
nextTick,
|
|
onActivated,
|
|
onBeforeMount,
|
|
onBeforeUnmount,
|
|
onBeforeUpdate,
|
|
onDeactivated,
|
|
onErrorCaptured,
|
|
onMounted,
|
|
onScopeDispose,
|
|
onServerPrefetch,
|
|
onUnmounted,
|
|
onUpdated,
|
|
provide,
|
|
proxyRefs,
|
|
reactive,
|
|
readonly,
|
|
set,
|
|
shallowReactive,
|
|
shallowReadonly,
|
|
shallowRef,
|
|
toRaw,
|
|
toRef,
|
|
toRefs,
|
|
triggerRef,
|
|
unref,
|
|
useAttrs,
|
|
useCssModule,
|
|
useCSSModule,
|
|
useSlots,
|
|
version,
|
|
warn,
|
|
watch,
|
|
watchEffect,
|
|
watchPostEffect,
|
|
watchSyncEffect
|
|
} from '@vue/composition-api'
|
|
|
|
export { ref }
|
|
|
|
// Common deprecation utils
|
|
// TODO: Add migration guide docs to @nuxtjs/composition-api
|
|
const checkDocsMsg = 'Please see https://v3.nuxtjs.org/getting-started/bridge-composition-api for more information.'
|
|
const msgPrefix = '[bridge] [legacy capi]'
|
|
const unsupported = message => () => { throw new Error(`${msgPrefix} ${message} ${checkDocsMsg}`) }
|
|
const _warned = {}
|
|
const warnOnce = (id, message) => {
|
|
if (!_warned[id]) {
|
|
console.warn(msgPrefix, message, checkDocsMsg)
|
|
_warned[id] = true
|
|
}
|
|
}
|
|
|
|
// Warn in case of having any imports from `@nuxtjs/composition-api`
|
|
warnOnce('import', '`@nuxtjs/composition-api` is deprecated.')
|
|
|
|
// Stub functions that provided type support
|
|
export const defineNuxtMiddleware = unsupported('You are using `defineNuxtMiddleware`, which is not supported.')
|
|
export const defineNuxtPlugin = unsupported('You are using `defineNuxtPlugin`, which has a Nuxt 3-compatible replacement.')
|
|
|
|
// Internal exports
|
|
export const setMetaPlugin = unsupported('`setMetaPlugin` is an internal function that is no longer used.')
|
|
export const setSSRContext = unsupported('`setSSRContext` is an internal function that is no longer used.')
|
|
export const globalPlugin = unsupported('`globalPlugin` is an internal function that is no longer used.')
|
|
|
|
// Deprecated functions
|
|
export const withContext = unsupported('`withContext` is a deprecated method that is no longer provided, but which has a Nuxt 3-compatible replacement.')
|
|
export const useStatic = unsupported('`useStatic` is a deprecated method that is no longer provided.')
|
|
export const reqRef = unsupported('`reqRef` is a deprecated method that is no longer provided, but which has a Nuxt 3-compatible replacement.')
|
|
export const reqSsrRef = unsupported('`reqSsrRef` is no longer provided, but has a Nuxt 3-compatible replacement.')
|
|
|
|
// ssrRef helpers
|
|
const sanitise = val => (val && JSON.parse(JSON.stringify(val))) || val
|
|
const getValue = val => val instanceof Function ? val() : val
|
|
|
|
export const ssrRef = (value, key) => {
|
|
const vm = getVM()
|
|
if (!vm) { throw new Error('ssrRef no longer supports global/ambient context and must be called within a setup() function') }
|
|
|
|
warnOnce('ssrRef', '`ssrRef` is deprecated and has a Nuxt 3-compatible replacement.')
|
|
|
|
return useState(key, value instanceof Function ? value : () => value)
|
|
}
|
|
|
|
export const shallowSsrRef = (value, key) => {
|
|
warnOnce('shallowSsrRef', '`shallowSsrRef` is deprecated and has a Nuxt 3-compatible replacement.')
|
|
|
|
const ref = ssrRef(value, key)
|
|
|
|
if (process.client) {
|
|
return shallowRef(ref.value)
|
|
}
|
|
|
|
return ref
|
|
}
|
|
|
|
export const ssrPromise = (value, key) => {
|
|
warnOnce('ssrPromise', 'ssrPromise is deprecated.')
|
|
|
|
const ssrRefs = useSSRRefs()
|
|
const promise = Promise.resolve(isHMR() ? getValue(value) : ssrRefs[key] ?? getValue(value))
|
|
|
|
onServerPrefetch(async () => { ssrRefs[key] = sanitise(await promise) })
|
|
return promise
|
|
}
|
|
|
|
// Composition API functions
|
|
export const onGlobalSetup = (fn) => {
|
|
warnOnce('onGlobalSetup', '`onGlobalSetup` is deprecated and has a Nuxt 3-compatible replacement.')
|
|
useNuxtApp().hook('vue:setup', fn)
|
|
}
|
|
|
|
export const useAsync = (cb, key) => {
|
|
warnOnce('useAsync', 'You are using `useAsync`, which has a Nuxt 3-compatible replacement.')
|
|
|
|
const _ref = isRef(key) ? key : ssrRef(null, key)
|
|
|
|
if (!_ref.value || isHMR()) {
|
|
const p = Promise.resolve(cb()).then(res => (_ref.value = res))
|
|
onServerPrefetch(() => p)
|
|
}
|
|
|
|
return _ref
|
|
}
|
|
|
|
export const useContext = () => {
|
|
warnOnce('useContext', 'You are using `useContext`, which has a Nuxt 3-compatible replacement.')
|
|
|
|
const route = useRoute()
|
|
const nuxt = useNuxtApp()
|
|
|
|
return {
|
|
...nuxt.nuxt2Context,
|
|
route: computed(() => route),
|
|
query: computed(() => route.value.query),
|
|
from: computed(() => nuxt.nuxt2Context.from),
|
|
params: computed(() => route.value.params)
|
|
}
|
|
}
|
|
|
|
function createEmptyMeta () {
|
|
return {
|
|
titleTemplate: null,
|
|
|
|
__dangerouslyDisableSanitizers: [],
|
|
__dangerouslyDisableSanitizersByTagID: {},
|
|
|
|
title: undefined,
|
|
htmlAttrs: {},
|
|
headAttrs: {},
|
|
bodyAttrs: {},
|
|
|
|
base: undefined,
|
|
|
|
meta: [],
|
|
link: [],
|
|
style: [],
|
|
script: [],
|
|
noscript: [],
|
|
|
|
changed: undefined,
|
|
afterNavigation: undefined
|
|
}
|
|
}
|
|
|
|
const getHeadOptions = (options) => {
|
|
const head = function () {
|
|
const optionHead =
|
|
options.head instanceof Function ? options.head.call(this) : options.head
|
|
|
|
if (!this._computedHead) { return optionHead }
|
|
|
|
const computedHead = this._computedHead.map((h) => {
|
|
if (isReactive(h)) { return toRaw(h) }
|
|
if (isRef(h)) { return h.value }
|
|
return h
|
|
})
|
|
return defu({}, ...computedHead.reverse(), optionHead)
|
|
}
|
|
|
|
return { head }
|
|
}
|
|
|
|
export const defineComponent = (options) => {
|
|
if (!('head' in options)) { return options }
|
|
|
|
return {
|
|
...options,
|
|
...getHeadOptions(options)
|
|
}
|
|
}
|
|
|
|
export const useMeta = (init) => {
|
|
warnOnce('useMeta', 'You are using `useMeta`, which has a replacement provided by Nuxt Bridge.')
|
|
const vm = getCurrentInstance()
|
|
const refreshMeta = () => vm.$meta().refresh()
|
|
|
|
if (!vm._computedHead) {
|
|
const metaRefs = reactive(createEmptyMeta())
|
|
vm._computedHead = [metaRefs]
|
|
vm._metaRefs = toRefs(metaRefs)
|
|
|
|
if (process.client) {
|
|
watch(Object.values(vm._metaRefs), refreshMeta, { immediate: true })
|
|
}
|
|
}
|
|
|
|
if (init) {
|
|
const initRef = init instanceof Function ? computed(init) : ref(init)
|
|
vm._computedHead.push(initRef)
|
|
|
|
if (process.client) {
|
|
watch(initRef, refreshMeta, { immediate: true })
|
|
}
|
|
}
|
|
|
|
return vm._metaRefs
|
|
}
|
|
|
|
// Wrapped properties
|
|
export const wrapProperty = (property, makeComputed = true) => () => {
|
|
warnOnce('wrapProperty', 'You are using `wrapProperty`, which is deprecated.')
|
|
const vm = getCurrentInstance()
|
|
return makeComputed ? computed(() => vm[property]) : vm[property]
|
|
}
|
|
|
|
export const useRouter = () => {
|
|
warnOnce('useRouter', 'You are using `useRouter`, which has a Nuxt 3-compatible replacement.')
|
|
return _useRouter()
|
|
}
|
|
|
|
export const useRoute = () => {
|
|
warnOnce('useRoute', 'You are using `useRoute`, which has a Nuxt 3-compatible replacement.')
|
|
const vm = getCurrentInstance()
|
|
return computed(() => vm.$route)
|
|
}
|
|
|
|
export const useStore = () => {
|
|
warnOnce('useRoute', 'You are using `useStore`, which has a Nuxt 3-compatible replacement.')
|
|
return getCurrentInstance().$store
|
|
}
|
|
|
|
// useFetch and helper functions
|
|
|
|
const fetches = new WeakMap()
|
|
const fetchPromises = new Map()
|
|
|
|
const mergeDataOnMount = (data) => {
|
|
const vm = getCurrentInstance()
|
|
if (!vm) { throw new Error('This must be called within a setup function.') }
|
|
|
|
onBeforeMount(() => {
|
|
// Merge data
|
|
for (const key in data) {
|
|
try {
|
|
// Assign missing properties
|
|
if (key in vm) {
|
|
// Skip functions (not stringifiable)
|
|
if (typeof vm[key] === 'function') { continue }
|
|
// Preserve reactive objects
|
|
if (isReactive(vm[key])) {
|
|
// Unset keys that do not exist in incoming data
|
|
for (const k in vm[key]) {
|
|
if (!(k in data[key])) {
|
|
delete vm[key][k]
|
|
}
|
|
}
|
|
Object.assign(vm[key], data[key])
|
|
continue
|
|
}
|
|
}
|
|
set(vm, key, data[key])
|
|
} catch (e) {
|
|
if (process.env.NODE_ENV === 'development')
|
|
// eslint-disable-next-line
|
|
console.warn(`Could not hydrate ${key}.`)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function createGetCounter (counterObject, defaultKey = '') {
|
|
return function getCounter (id = defaultKey) {
|
|
if (counterObject[id] === undefined) {
|
|
counterObject[id] = 0
|
|
}
|
|
return counterObject[id]++
|
|
}
|
|
}
|
|
|
|
const setFetchState = (vm) => {
|
|
vm.$fetchState =
|
|
vm.$fetchState ||
|
|
reactive({
|
|
error: null,
|
|
pending: false,
|
|
timestamp: 0
|
|
})
|
|
}
|
|
|
|
function getKey (vm) {
|
|
const nuxt = useNuxtApp()
|
|
const nuxtState = nuxt.payload
|
|
if (process.server && 'push' in vm.$ssrContext.nuxt.fetch) {
|
|
return undefined
|
|
} else if (process.client && '_payloadFetchIndex' in nuxtState) {
|
|
nuxtState._payloadFetchIndex = nuxtState._payloadFetchIndex || 0
|
|
return nuxtState._payloadFetchIndex++
|
|
}
|
|
const defaultKey = vm.$options._scopeId || vm.$options.name || ''
|
|
const getCounter = createGetCounter(
|
|
process.server
|
|
? vm.$ssrContext.fetchCounters
|
|
: nuxt.vue2App._fetchCounters,
|
|
defaultKey
|
|
)
|
|
|
|
if (typeof vm.$options.fetchKey === 'function') {
|
|
return vm.$options.fetchKey.call(vm, getCounter)
|
|
} else {
|
|
const key = typeof vm.$options.fetchKey === 'string' ? vm.$options.fetchKey : defaultKey
|
|
return key ? key + ':' + getCounter(key) : String(getCounter(key))
|
|
}
|
|
}
|
|
|
|
function normalizeError (err) {
|
|
let message
|
|
if (!(err.message || typeof err === 'string')) {
|
|
try {
|
|
message = JSON.stringify(err, null, 2)
|
|
} catch (e) {
|
|
message = `[${err.constructor.name}]`
|
|
}
|
|
} else {
|
|
message = err.message || err
|
|
}
|
|
return {
|
|
...err,
|
|
message,
|
|
statusCode:
|
|
err.statusCode ||
|
|
err.status ||
|
|
(err.response && err.response.status) ||
|
|
500
|
|
}
|
|
}
|
|
|
|
const loadFullStatic = (vm) => {
|
|
vm._fetchKey = getKey(vm)
|
|
// Check if component has been fetched on server
|
|
const { fetchOnServer } = vm.$options
|
|
const fetchedOnServer =
|
|
typeof fetchOnServer === 'function'
|
|
? fetchOnServer.call(vm) !== false
|
|
: fetchOnServer !== false
|
|
|
|
if (!fetchedOnServer || vm.$nuxt?.isPreview || !vm.$nuxt?._pagePayload) {
|
|
return
|
|
}
|
|
vm._hydrated = true
|
|
const data = vm.$nuxt._pagePayload.fetch[vm._fetchKey]
|
|
|
|
// If fetch error
|
|
if (data && data._error) {
|
|
vm.$fetchState.error = data._error
|
|
return
|
|
}
|
|
|
|
mergeDataOnMount(data)
|
|
}
|
|
|
|
async function serverPrefetch (vm) {
|
|
if (!vm._fetchOnServer) { return }
|
|
|
|
// Call and await on $fetch
|
|
setFetchState(vm)
|
|
|
|
try {
|
|
await callFetches.call(vm)
|
|
} catch (err) {
|
|
if (process.dev) {
|
|
console.error('Error in fetch():', err)
|
|
}
|
|
vm.$fetchState.error = normalizeError(err)
|
|
}
|
|
vm.$fetchState.pending = false
|
|
|
|
// Define an ssrKey for hydration
|
|
vm._fetchKey =
|
|
// Nuxt 2.15+ uses a different format - an object rather than an array
|
|
'push' in vm.$ssrContext.nuxt.fetch
|
|
? vm.$ssrContext.nuxt.fetch.length
|
|
: vm._fetchKey || vm.$ssrContext.fetchCounters['']++
|
|
|
|
// Add data-fetch-key on parent element of Component
|
|
if (!vm.$vnode.data) { vm.$vnode.data = {} }
|
|
const attrs = (vm.$vnode.data.attrs = vm.$vnode.data.attrs || {})
|
|
attrs['data-fetch-key'] = vm._fetchKey
|
|
|
|
const data = { ...vm._data }
|
|
Object.entries(vm.__composition_api_state__.rawBindings).forEach(
|
|
([key, val]) => {
|
|
if (val instanceof Function || val instanceof Promise) { return }
|
|
|
|
data[key] = isRef(val) ? val.value : val
|
|
}
|
|
)
|
|
|
|
// Add to ssrContext for window.__NUXT__.fetch
|
|
const content = vm.$fetchState.error
|
|
? { _error: vm.$fetchState.error }
|
|
: JSON.parse(JSON.stringify(data))
|
|
if ('push' in vm.$ssrContext.nuxt.fetch) {
|
|
vm.$ssrContext.nuxt.fetch.push(content)
|
|
} else {
|
|
vm.$ssrContext.nuxt.fetch[vm._fetchKey] = content
|
|
}
|
|
}
|
|
|
|
async function callFetches () {
|
|
const fetchesToCall = fetches.get(this)
|
|
if (!fetchesToCall) { return }
|
|
this.$nuxt.nbFetching++
|
|
|
|
this.$fetchState.pending = true
|
|
this.$fetchState.error = null
|
|
this._hydrated = false
|
|
|
|
let error = null
|
|
const startTime = Date.now()
|
|
|
|
try {
|
|
await Promise.all(
|
|
fetchesToCall.map((fetch) => {
|
|
if (fetchPromises.has(fetch)) { return fetchPromises.get(fetch) }
|
|
const promise = Promise.resolve(fetch(this)).finally(() =>
|
|
fetchPromises.delete(fetch)
|
|
)
|
|
fetchPromises.set(fetch, promise)
|
|
return promise
|
|
})
|
|
)
|
|
} catch (err) {
|
|
if (process.dev) {
|
|
console.error('Error in fetch():', err)
|
|
}
|
|
error = normalizeError(err)
|
|
}
|
|
|
|
const delayLeft = (this._fetchDelay || 0) - (Date.now() - startTime)
|
|
if (delayLeft > 0) {
|
|
await new Promise(resolve => setTimeout(resolve, delayLeft))
|
|
}
|
|
|
|
this.$fetchState.error = error
|
|
this.$fetchState.pending = false
|
|
this.$fetchState.timestamp = Date.now()
|
|
|
|
this.$nextTick(() => (this.$nuxt).nbFetching--)
|
|
}
|
|
|
|
const isSsrHydration = vm => vm.$vnode?.elm?.dataset?.fetchKey
|
|
|
|
export const useFetch = (callback) => {
|
|
warnOnce('useFetch', 'You are using `useFetch`, which has a Nuxt 3-compatible replacement.')
|
|
const vm = getCurrentInstance()
|
|
const nuxt = useNuxtApp()
|
|
|
|
const nuxtState = nuxt.payload
|
|
|
|
const callbacks = fetches.get(vm) || []
|
|
fetches.set(vm, [...callbacks, callback])
|
|
|
|
if (typeof vm.$options.fetchOnServer === 'function') {
|
|
vm._fetchOnServer = vm.$options.fetchOnServer.call(vm) !== false
|
|
} else {
|
|
vm._fetchOnServer = vm.$options.fetchOnServer !== false
|
|
}
|
|
|
|
if (process.server) {
|
|
vm._fetchKey = getKey(vm)
|
|
}
|
|
|
|
setFetchState(vm)
|
|
|
|
onServerPrefetch(() => serverPrefetch(vm))
|
|
|
|
function result () {
|
|
return {
|
|
fetch: vm.$fetch,
|
|
fetchState: vm.$fetchState
|
|
}
|
|
}
|
|
|
|
vm._fetchDelay =
|
|
typeof vm.$options.fetchDelay === 'number' ? vm.$options.fetchDelay : 0
|
|
|
|
vm.$fetch = callFetches.bind(vm)
|
|
|
|
onBeforeMount(() => !vm._hydrated && callFetches.call(vm))
|
|
|
|
if (process.server || !isSsrHydration(vm)) {
|
|
if (process.client && !process.dev && process.static) { loadFullStatic(vm) }
|
|
return result()
|
|
}
|
|
|
|
// Hydrate component
|
|
vm._hydrated = true
|
|
vm._fetchKey = vm.$vnode.elm?.dataset.fetchKey || getKey(vm)
|
|
const data = nuxtState.fetch[vm._fetchKey]
|
|
|
|
// If fetch error
|
|
if (data && data._error) {
|
|
vm.$fetchState.error = data._error
|
|
return result()
|
|
}
|
|
|
|
mergeDataOnMount(data)
|
|
|
|
return result()
|
|
}
|
|
|
|
// -- Private shared utils (across composables) --
|
|
|
|
function getCurrentInstance () {
|
|
const vm = getVM()
|
|
if (!vm) { throw new Error('This must be called within a setup function.') }
|
|
return vm.proxy
|
|
}
|
|
|
|
const useSSRRefs = () => {
|
|
const { payload } = useNuxtApp()
|
|
payload.ssrRefs = payload.ssrRefs || {}
|
|
return payload.ssrRefs
|
|
}
|
|
|
|
const isHMR = () => process.env.NODE_ENV === 'development' && process.client && window.$nuxt?.context.isHMR
|