mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
feat(bridge): add support for legacy composition api helpers (#584)
This commit is contained in:
parent
7fdcee3252
commit
ad9d2d1906
@ -16,6 +16,11 @@ export function setupAppBridge (_options: any) {
|
|||||||
nuxt.options.alias.vue = nuxt.options.alias.vue ||
|
nuxt.options.alias.vue = nuxt.options.alias.vue ||
|
||||||
resolveModule('vue/dist/vue.runtime.esm.js', { paths: nuxt.options.modulesDir })
|
resolveModule('vue/dist/vue.runtime.esm.js', { paths: nuxt.options.modulesDir })
|
||||||
|
|
||||||
|
// Deprecate various Nuxt options
|
||||||
|
if (nuxt.options.globalName !== 'nuxt') {
|
||||||
|
throw new Error('Custom global name is not supported by @nuxt/bridge.')
|
||||||
|
}
|
||||||
|
|
||||||
// Fix wp4 esm
|
// Fix wp4 esm
|
||||||
nuxt.hook('webpack:config', (configs) => {
|
nuxt.hook('webpack:config', (configs) => {
|
||||||
for (const config of configs.filter(c => c.module)) {
|
for (const config of configs.filter(c => c.module)) {
|
||||||
|
102
packages/bridge/src/capi-legacy-key-plugin.ts
Normal file
102
packages/bridge/src/capi-legacy-key-plugin.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
import { createUnplugin } from 'unplugin'
|
||||||
|
import { parse } from 'acorn'
|
||||||
|
import MagicString from 'magic-string'
|
||||||
|
import { walk } from 'estree-walker'
|
||||||
|
import { parseQuery, parseURL } from 'ufo'
|
||||||
|
|
||||||
|
function createKey (
|
||||||
|
source: string,
|
||||||
|
method: crypto.BinaryToTextEncoding = 'base64'
|
||||||
|
) {
|
||||||
|
const hash = crypto.createHash('md5')
|
||||||
|
hash.update(source)
|
||||||
|
return hash.digest(method).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyedFunctions =
|
||||||
|
/(useStatic|shallowSsrRef|ssrPromise|ssrRef|reqSsrRef|useAsync)/
|
||||||
|
|
||||||
|
export const KeyPlugin = createUnplugin(() => {
|
||||||
|
return {
|
||||||
|
name: 'nuxt-legacy-capi-key-transform',
|
||||||
|
enforce: 'pre',
|
||||||
|
transformInclude (id) {
|
||||||
|
const { pathname, search } = parseURL(id)
|
||||||
|
const query = parseQuery(search)
|
||||||
|
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// vue files
|
||||||
|
if (pathname.endsWith('.vue') && (query.type === 'script' || !search)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// js files
|
||||||
|
if (pathname.match(/\.((c|m)?j|t)sx?/g)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transform (code, id) {
|
||||||
|
if (!keyedFunctions.test(code)) { return null }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { 0: script = code, index: codeIndex = 0 } =
|
||||||
|
code.match(/(?<=<script[^>]*>)[\S\s.]*?(?=<\/script>)/) || []
|
||||||
|
const ast = parse(script, { ecmaVersion: 2020, sourceType: 'module' })
|
||||||
|
const s = new MagicString(code)
|
||||||
|
|
||||||
|
walk(ast, {
|
||||||
|
enter (node) {
|
||||||
|
const { end } = node as unknown as {
|
||||||
|
end: number
|
||||||
|
}
|
||||||
|
const { callee, arguments: args = [] } = node as {
|
||||||
|
callee?: {
|
||||||
|
type?: string
|
||||||
|
name?: string
|
||||||
|
property?: { type: string; name: string }
|
||||||
|
}
|
||||||
|
arguments?: any[]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
callee?.type === 'Identifier' ||
|
||||||
|
callee?.property?.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
let method: crypto.BinaryToTextEncoding = 'base64'
|
||||||
|
|
||||||
|
switch (callee.name || callee.property?.name) {
|
||||||
|
case 'useStatic':
|
||||||
|
if (args.length > 2) { return }
|
||||||
|
if (args.length === 2) {
|
||||||
|
s.prependLeft(codeIndex + end - 1, ', undefined')
|
||||||
|
}
|
||||||
|
method = 'hex'
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'shallowSsrRef':
|
||||||
|
case 'ssrPromise':
|
||||||
|
case 'ssrRef':
|
||||||
|
case 'reqSsrRef':
|
||||||
|
case 'useAsync':
|
||||||
|
if (args.length > 1) { return }
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.appendLeft(
|
||||||
|
codeIndex + end - 1,
|
||||||
|
", '" + createKey(`${id}-${end}`, method) + "'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return s.toString()
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,6 +1,8 @@
|
|||||||
import { useNuxt, addPluginTemplate, addPlugin } from '@nuxt/kit'
|
import { useNuxt, addPlugin, addPluginTemplate, addVitePlugin, addWebpackPlugin } from '@nuxt/kit'
|
||||||
import { resolve } from 'pathe'
|
import { resolve } from 'pathe'
|
||||||
|
|
||||||
import { distDir } from './dirs'
|
import { distDir } from './dirs'
|
||||||
|
import { KeyPlugin } from './capi-legacy-key-plugin'
|
||||||
|
|
||||||
export function setupCAPIBridge (_options: any) {
|
export function setupCAPIBridge (_options: any) {
|
||||||
const nuxt = useNuxt()
|
const nuxt = useNuxt()
|
||||||
@ -16,7 +18,10 @@ export function setupCAPIBridge (_options: any) {
|
|||||||
addPluginTemplate({ filename: 'capi.plugin.mjs', src: capiPluginPath })
|
addPluginTemplate({ filename: 'capi.plugin.mjs', src: capiPluginPath })
|
||||||
|
|
||||||
// Add support for useNuxtApp
|
// Add support for useNuxtApp
|
||||||
addPlugin(resolve(distDir, 'runtime/app.plugin.mjs'))
|
const appPlugin = addPluginTemplate(resolve(distDir, 'runtime/app.plugin.mjs'))
|
||||||
|
nuxt.hook('modules:done', () => {
|
||||||
|
nuxt.options.plugins.unshift(appPlugin)
|
||||||
|
})
|
||||||
|
|
||||||
// Register Composition API before loading the rest of app
|
// Register Composition API before loading the rest of app
|
||||||
nuxt.hook('webpack:config', (configs) => {
|
nuxt.hook('webpack:config', (configs) => {
|
||||||
@ -24,5 +29,14 @@ export function setupCAPIBridge (_options: any) {
|
|||||||
configs.forEach(config => config.entry.app.unshift(capiPluginPath))
|
configs.forEach(config => config.entry.app.unshift(capiPluginPath))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle legacy `@nuxtjs/composition-api`
|
||||||
|
nuxt.options.alias['@nuxtjs/composition-api'] = resolve(distDir, 'runtime/capi.legacy.mjs')
|
||||||
|
nuxt.options.build.transpile.push('@nuxtjs/composition-api', '@vue/composition-api')
|
||||||
|
addPlugin(resolve(distDir, 'runtime/capi.legacy.plugin.mjs'))
|
||||||
|
|
||||||
|
// Enable automatic ssrRef key generation
|
||||||
|
addVitePlugin(KeyPlugin.vite())
|
||||||
|
addWebpackPlugin(KeyPlugin.webpack())
|
||||||
|
|
||||||
// TODO: Add @nuxtjs/composition-api shims
|
// TODO: Add @nuxtjs/composition-api shims
|
||||||
}
|
}
|
||||||
|
606
packages/bridge/src/runtime/capi.legacy.mjs
Normal file
606
packages/bridge/src/runtime/capi.legacy.mjs
Normal file
@ -0,0 +1,606 @@
|
|||||||
|
import defu from 'defu'
|
||||||
|
import { computed, customRef, getCurrentInstance as getVM, isReactive, isRef, onBeforeMount, onServerPrefetch, reactive, ref, set, shallowRef, toRaw, toRefs, watch } from '@vue/composition-api'
|
||||||
|
import { useNuxtApp } from './app'
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
_warned[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn in case of having any imports from `@nuxtjs/composition-api`
|
||||||
|
warnOnce('import', `\`@nuxtjs/composition-api\` is deprecated. ${checkDocsMsg}`)
|
||||||
|
|
||||||
|
// Stub functions that provided type support
|
||||||
|
export const defineNuxtMiddleware = unsupported('You are using `defineNuxtMiddleware`, which can be replaced with `defineNuxtMiddleware` from `@nuxt/bridge`.')
|
||||||
|
export const defineNuxtPlugin = unsupported('You are using `defineNuxtPlugin`, which can be replaced with `defineNuxtPlugin` from `@nuxt/bridge`.')
|
||||||
|
|
||||||
|
// 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.')
|
||||||
|
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.')
|
||||||
|
export const reqSsrRef = unsupported('`reqSsrRef` is no longer provided (`ssrRef` can be used instead).')
|
||||||
|
|
||||||
|
// ssrRef helpers
|
||||||
|
const isProxyable = val => !!val && typeof val === 'object'
|
||||||
|
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') }
|
||||||
|
|
||||||
|
const ssrRefs = useSSRRefs()
|
||||||
|
|
||||||
|
let resolvedValue = isHMR() ? getValue(value) : ssrRefs[key] ?? getValue(value)
|
||||||
|
|
||||||
|
const _ref = ref(resolvedValue)
|
||||||
|
if (process.client) { return _ref }
|
||||||
|
|
||||||
|
const setData = (key, val) => {
|
||||||
|
ssrRefs[key] = sanitise(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Function) { setData(key, resolvedValue) }
|
||||||
|
|
||||||
|
const getProxy = (track, trigger, observable) =>
|
||||||
|
new Proxy(observable, {
|
||||||
|
get (target, prop) {
|
||||||
|
track()
|
||||||
|
if (isProxyable(target[prop])) { return getProxy(track, trigger, target[prop]) }
|
||||||
|
return Reflect.get(target, prop)
|
||||||
|
},
|
||||||
|
set (obj, prop, newVal) {
|
||||||
|
const result = Reflect.set(obj, prop, newVal)
|
||||||
|
setData(key, resolvedValue)
|
||||||
|
trigger()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const proxy = customRef((track, trigger) => ({
|
||||||
|
get: () => {
|
||||||
|
track()
|
||||||
|
if (isProxyable(resolvedValue)) { return getProxy(track, trigger, resolvedValue) }
|
||||||
|
return resolvedValue
|
||||||
|
},
|
||||||
|
set: (v) => {
|
||||||
|
setData(key, v)
|
||||||
|
resolvedValue = v
|
||||||
|
trigger()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shallowSsrRef = (value, key) => {
|
||||||
|
const ssrRefs = useSSRRefs()
|
||||||
|
|
||||||
|
let resolvedValue = isHMR() ? getValue(value) : ssrRefs[key] ?? getValue(value)
|
||||||
|
|
||||||
|
const _ref = shallowRef(resolvedValue)
|
||||||
|
if (process.client) { return _ref }
|
||||||
|
|
||||||
|
const setData = (key, val) => {
|
||||||
|
ssrRefs[key] = sanitise(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Function) {
|
||||||
|
setData(key, resolvedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return computed({
|
||||||
|
get () {
|
||||||
|
return resolvedValue
|
||||||
|
},
|
||||||
|
set (newValue) {
|
||||||
|
setData(key, newValue)
|
||||||
|
resolvedValue = newValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ssrPromise = (value, key) => {
|
||||||
|
warnOnce('ssrPromise', 'ssrPromise is deprecated. Please use an alternative implementation.')
|
||||||
|
|
||||||
|
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 can be replaced with `defineNuxtPlugin` and `nuxt.provide`.')
|
||||||
|
const app = useNuxtApp()
|
||||||
|
app._setupFns.push(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAsync = (cb, key) => {
|
||||||
|
warnOnce('useAsync', 'You are using `useAsync`, which can be replaced with `useAsyncData` from `@nuxt/bridge`.')
|
||||||
|
|
||||||
|
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 can be replaced with `useNuxtApp` from `@nuxt/bridge`.')
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const nuxt = useNuxtApp()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...nuxt.legacyNuxt.context,
|
||||||
|
route: computed(() => route),
|
||||||
|
query: computed(() => route.value.query),
|
||||||
|
from: computed(() => nuxt.legacyNuxt.context.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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) => {
|
||||||
|
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) => () => {
|
||||||
|
const vm = getCurrentInstance()
|
||||||
|
return makeComputed ? computed(() => vm[property]) : vm[property]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRouter = () => {
|
||||||
|
return getCurrentInstance().$router
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRoute = () => {
|
||||||
|
const vm = getCurrentInstance()
|
||||||
|
return computed(() => vm.$route)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = () => 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.legacyApp._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) => {
|
||||||
|
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
|
15
packages/bridge/src/runtime/capi.legacy.plugin.mjs
Normal file
15
packages/bridge/src/runtime/capi.legacy.plugin.mjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
nuxt._setupFns = []
|
||||||
|
|
||||||
|
const _originalSetup = nuxt.legacyNuxt.setup
|
||||||
|
|
||||||
|
nuxt.legacyNuxt.setup = function (...args) {
|
||||||
|
const result = _originalSetup instanceof Function ? _originalSetup(...args) : {}
|
||||||
|
for (const fn of nuxt._setupFns) {
|
||||||
|
Object.assign(result, fn.call(this, ...args))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
})
|
20
test/fixtures/bridge/components/FetchTest.vue
vendored
Normal file
20
test/fixtures/bridge/components/FetchTest.vue
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<tr><td><b>useFetch</b></td><td> {{ fetched }}</td></tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, useFetch } from '@nuxtjs/composition-api'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup () {
|
||||||
|
const fetched = ref('🚧')
|
||||||
|
useFetch(() => {
|
||||||
|
fetched.value = '✅'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
2
test/fixtures/bridge/nuxt.config.ts
vendored
2
test/fixtures/bridge/nuxt.config.ts
vendored
@ -4,6 +4,7 @@ import { defineNuxtConfig } from '@nuxt/kit'
|
|||||||
global.__NUXT_PREPATHS__ = (global.__NUXT_PREPATHS__ || []).concat(__dirname)
|
global.__NUXT_PREPATHS__ = (global.__NUXT_PREPATHS__ || []).concat(__dirname)
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
components: true,
|
||||||
buildModules: [
|
buildModules: [
|
||||||
'@nuxt/bridge'
|
'@nuxt/bridge'
|
||||||
],
|
],
|
||||||
@ -16,6 +17,7 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
buildDir: process.env.NITRO_BUILD_DIR,
|
buildDir: process.env.NITRO_BUILD_DIR,
|
||||||
|
plugins: ['~/plugins/setup.js'],
|
||||||
nitro: {
|
nitro: {
|
||||||
output: { dir: process.env.NITRO_OUTPUT_DIR }
|
output: { dir: process.env.NITRO_OUTPUT_DIR }
|
||||||
}
|
}
|
||||||
|
88
test/fixtures/bridge/pages/legacy-capi.vue
vendored
Normal file
88
test/fixtures/bridge/pages/legacy-capi.vue
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<!-- Basic setup function -->
|
||||||
|
<tr><td><b>setup</b></td><td> {{ setup }}</td></tr>
|
||||||
|
<!-- Ref -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<b>ref</b>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ ref }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button @click="ref = '❇️'">
|
||||||
|
update
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Lifecycle methods -->
|
||||||
|
<tr><td><b>onMounted</b></td><td> {{ mounted }}</td></tr>
|
||||||
|
<!-- Wrappers -->
|
||||||
|
<tr><td><b>useStore</b></td><td> {{ store.state.test }}</td></tr>
|
||||||
|
<tr><td><b>useRoute</b></td><td> {{ route.path === '/legacy' ? '✅' : '❌' }}</td></tr>
|
||||||
|
<tr><td><b>useContext</b></td><td> {{ Object.keys(context).length ? '✅' : '❌' }}</td></tr>
|
||||||
|
<!-- Helpers -->
|
||||||
|
<tr><td><b>useAsync</b></td><td> {{ async }}</td></tr>
|
||||||
|
<tr><td><b>ssrRef</b></td><td> {{ ssrRef }}</td></tr>
|
||||||
|
<tr><td><b>shallowSsrRef</b></td><td> {{ shallow }}</td></tr>
|
||||||
|
<tr><td><b>ssrPromise</b></td><td> {{ promise }}</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>useMeta</b></td><td> {{ title }}</td>
|
||||||
|
<td>
|
||||||
|
<button @click="title = '❇️'">
|
||||||
|
update
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td><b>onGlobalSetup</b></td><td> {{ globalsetup }}</td></tr>
|
||||||
|
<FetchTest />
|
||||||
|
<tr><td><b>reqSsrRef</b></td><td> {{ '⛔️' }}</td></tr>
|
||||||
|
<tr><td><b>reqRef</b></td><td> {{ '⛔️' }}</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted, ref, useRoute, useContext, useStore, useAsync, ssrRef, shallowSsrRef, ssrPromise, useMeta } from '@nuxtjs/composition-api'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup () {
|
||||||
|
const mounted = ref()
|
||||||
|
const shallow = shallowSsrRef('❌')
|
||||||
|
const { isHMR, $globalsetup } = useContext()
|
||||||
|
const { title } = useMeta()
|
||||||
|
if (process.server || isHMR) {
|
||||||
|
shallow.value = '✅'
|
||||||
|
title.value = '❌'
|
||||||
|
}
|
||||||
|
const promise = ref(null)
|
||||||
|
ssrPromise(() => new Promise(resolve => setTimeout(() => resolve(process.server || isHMR ? '✅' : '❌'), 100))).then((r) => {
|
||||||
|
promise.value = r
|
||||||
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
mounted.value = '✅'
|
||||||
|
title.value = '✅'
|
||||||
|
})
|
||||||
|
const store = useStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
return {
|
||||||
|
setup: '✅',
|
||||||
|
ref: ref('✅'),
|
||||||
|
mounted,
|
||||||
|
store,
|
||||||
|
route,
|
||||||
|
context: useContext(),
|
||||||
|
async: useAsync(() => new Promise(resolve => setTimeout(() => resolve(process.server || isHMR ? '✅' : '❌'), 100))),
|
||||||
|
ssrRef: ssrRef(() => process.server || isHMR ? '✅' : '❌'),
|
||||||
|
shallow,
|
||||||
|
promise,
|
||||||
|
title,
|
||||||
|
globalsetup: $globalsetup
|
||||||
|
}
|
||||||
|
},
|
||||||
|
head: {}
|
||||||
|
})
|
||||||
|
</script>
|
11
test/fixtures/bridge/plugins/setup.js
vendored
Normal file
11
test/fixtures/bridge/plugins/setup.js
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { onGlobalSetup, ref } from '@nuxtjs/composition-api'
|
||||||
|
|
||||||
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
const globalsetup = ref('🚧')
|
||||||
|
onGlobalSetup(() => {
|
||||||
|
globalsetup.value = '✅'
|
||||||
|
})
|
||||||
|
nuxt.provide('globalsetup', globalsetup)
|
||||||
|
})
|
9
test/fixtures/bridge/store/index.js
vendored
Normal file
9
test/fixtures/bridge/store/index.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const state = () => ({
|
||||||
|
test: '❌'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
nuxtServerInit ({ state }) {
|
||||||
|
state.test = '✅'
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user