mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 09:02:03 +00:00
feat(nuxt): support async transform of object properties (#20182)
This commit is contained in:
parent
98d1c0e827
commit
d6c3c2439a
@ -356,7 +356,7 @@ const { data } = await useFetch('/api/price')
|
|||||||
|
|
||||||
## Using Async Setup
|
## Using Async Setup
|
||||||
|
|
||||||
If you are using `async setup()`, the current component instance will be lost after the first `await`. (This is a Vue 3 limitation.) If you want to use multiple async operations, such as multiple calls to `useFetch`, you will need to use `<script setup>` or await them together at the end of setup.
|
If you are using `async setup()`, the current component instance will be lost after the first `await`. (This is a Vue 3 limitation.) If you want to use multiple async operations, such as multiple calls to `useFetch`, you will need to use `<script setup>`, await them together at the end of setup, or alternatively use `defineNuxtComponent` (which applies a custom transform to the setup function).
|
||||||
|
|
||||||
::alert{icon=👉}
|
::alert{icon=👉}
|
||||||
Using `<script setup>` is recommended, as it removes the limitation of using top-level await. [Read more](https://vuejs.org/api/sfc-script-setup.html#top-level-await)
|
Using `<script setup>` is recommended, as it removes the limitation of using top-level await. [Read more](https://vuejs.org/api/sfc-script-setup.html#top-level-await)
|
||||||
|
@ -54,7 +54,7 @@ When you are using the built-in Composition API composables provided by Vue and
|
|||||||
|
|
||||||
During a component lifecycle, Vue tracks the temporary instance of the current component (and similarly, Nuxt tracks a temporary instance of `nuxtApp`) via a global variable, and then unsets it in same tick. This is essential when server rendering, both to avoid cross-request state pollution (leaking a shared reference between two users) and to avoid leakage between different components.
|
During a component lifecycle, Vue tracks the temporary instance of the current component (and similarly, Nuxt tracks a temporary instance of `nuxtApp`) via a global variable, and then unsets it in same tick. This is essential when server rendering, both to avoid cross-request state pollution (leaking a shared reference between two users) and to avoid leakage between different components.
|
||||||
|
|
||||||
That means that (with very few exceptions) you cannot use them outside a Nuxt plugin, Nuxt route middleware or Vue setup function. On top of that, you must use them synchronously - that is, you cannot use `await` before calling a composable, except within `<script setup>` blocks, in `defineNuxtPlugin` or in `defineNuxtRouteMiddleware`, where we perform a transform to keep the synchronous context even after the `await`.
|
That means that (with very few exceptions) you cannot use them outside a Nuxt plugin, Nuxt route middleware or Vue setup function. On top of that, you must use them synchronously - that is, you cannot use `await` before calling a composable, except within `<script setup>` blocks, within the setup function of a component declared with `defineNuxtComponent`, in `defineNuxtPlugin` or in `defineNuxtRouteMiddleware`, where we perform a transform to keep the synchronous context even after the `await`.
|
||||||
|
|
||||||
If you get an error message like `Nuxt instance is unavailable` then it probably means you are calling a Nuxt composable in the wrong place in the Vue or Nuxt lifecycle.
|
If you get an error message like `Nuxt instance is unavailable` then it probably means you are calling a Nuxt composable in the wrong place in the Vue or Nuxt lifecycle.
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { getCurrentInstance, reactive, toRefs } from 'vue'
|
|||||||
import type { DefineComponent, defineComponent } from 'vue'
|
import type { DefineComponent, defineComponent } from 'vue'
|
||||||
import { useHead } from '@unhead/vue'
|
import { useHead } from '@unhead/vue'
|
||||||
import type { NuxtApp } from '../nuxt'
|
import type { NuxtApp } from '../nuxt'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { callWithNuxt, useNuxtApp } from '../nuxt'
|
||||||
import { useAsyncData } from './asyncData'
|
import { useAsyncData } from './asyncData'
|
||||||
import { useRoute } from './router'
|
import { useRoute } from './router'
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ async function runLegacyAsyncData (res: Record<string, any> | Promise<Record<str
|
|||||||
const vm = getCurrentInstance()!
|
const vm = getCurrentInstance()!
|
||||||
const { fetchKey } = vm.proxy!.$options
|
const { fetchKey } = vm.proxy!.$options
|
||||||
const key = typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey || route.fullPath
|
const key = typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey || route.fullPath
|
||||||
const { data } = await useAsyncData(`options:asyncdata:${key}`, () => fn(nuxt))
|
const { data } = await useAsyncData(`options:asyncdata:${key}`, () => callWithNuxt(nuxt, fn, [nuxt]))
|
||||||
if (data.value && typeof data.value === 'object') {
|
if (data.value && typeof data.value === 'object') {
|
||||||
Object.assign(await res, toRefs(reactive(data.value)))
|
Object.assign(await res, toRefs(reactive(data.value)))
|
||||||
} else if (process.dev) {
|
} else if (process.dev) {
|
||||||
@ -38,7 +38,8 @@ export const defineNuxtComponent: typeof defineComponent =
|
|||||||
[NuxtComponentIndicator]: true,
|
[NuxtComponentIndicator]: true,
|
||||||
...options,
|
...options,
|
||||||
setup (props, ctx) {
|
setup (props, ctx) {
|
||||||
const res = setup?.(props, ctx) || {}
|
const nuxtApp = useNuxtApp()
|
||||||
|
const res = setup ? Promise.resolve(callWithNuxt(nuxtApp, setup, [props, ctx])).then(r => r || {}) : {}
|
||||||
|
|
||||||
const promises: Promise<any>[] = []
|
const promises: Promise<any>[] = []
|
||||||
if (options.asyncData) {
|
if (options.asyncData) {
|
||||||
|
@ -98,8 +98,12 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
|
|
||||||
nuxt.hook('modules:done', () => {
|
nuxt.hook('modules:done', () => {
|
||||||
// Add unctx transform
|
// Add unctx transform
|
||||||
addVitePlugin(UnctxTransformPlugin(nuxt).vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))
|
const options = {
|
||||||
addWebpackPlugin(UnctxTransformPlugin(nuxt).webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))
|
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
|
||||||
|
transformerOptions: nuxt.options.optimization.asyncTransforms
|
||||||
|
}
|
||||||
|
addVitePlugin(UnctxTransformPlugin.vite(options))
|
||||||
|
addWebpackPlugin(UnctxTransformPlugin.webpack(options))
|
||||||
|
|
||||||
// Add composable tree-shaking optimisations
|
// Add composable tree-shaking optimisations
|
||||||
const serverTreeShakeOptions: TreeShakeComposablesPluginOptions = {
|
const serverTreeShakeOptions: TreeShakeComposablesPluginOptions = {
|
||||||
|
@ -1,30 +1,42 @@
|
|||||||
import { normalize } from 'pathe'
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { parseQuery, parseURL } from 'ufo'
|
||||||
|
import type { TransformerOptions } from 'unctx/transform'
|
||||||
import { createTransformer } from 'unctx/transform'
|
import { createTransformer } from 'unctx/transform'
|
||||||
import { createUnplugin } from 'unplugin'
|
import { createUnplugin } from 'unplugin'
|
||||||
import type { Nuxt, NuxtApp } from 'nuxt/schema'
|
|
||||||
|
|
||||||
const TRANSFORM_MARKER = '/* _processed_nuxt_unctx_transform */\n'
|
const TRANSFORM_MARKER = '/* _processed_nuxt_unctx_transform */\n'
|
||||||
|
|
||||||
export const UnctxTransformPlugin = (nuxt: Nuxt) => {
|
interface UnctxTransformPluginOptions {
|
||||||
const transformer = createTransformer({
|
sourcemap?: boolean
|
||||||
asyncFunctions: ['defineNuxtPlugin', 'defineNuxtRouteMiddleware']
|
transformerOptions: TransformerOptions
|
||||||
})
|
}
|
||||||
|
|
||||||
let app: NuxtApp | undefined
|
export const UnctxTransformPlugin = createUnplugin((options: UnctxTransformPluginOptions) => {
|
||||||
nuxt.hook('app:resolve', (_app) => { app = _app })
|
const transformer = createTransformer(options.transformerOptions)
|
||||||
|
return {
|
||||||
return createUnplugin((options: { sourcemap?: boolean } = {}) => ({
|
|
||||||
name: 'unctx:transform',
|
name: 'unctx:transform',
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
transformInclude (id) {
|
transformInclude (id) {
|
||||||
if (id.includes('macro=true')) { return true }
|
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||||
|
const query = parseQuery(search)
|
||||||
|
|
||||||
id = normalize(id).replace(/\?.*$/, '')
|
// Vue files
|
||||||
return app?.plugins.some(i => i.src === id) || app?.middleware.some(m => m.path === id)
|
if (
|
||||||
|
pathname.endsWith('.vue') ||
|
||||||
|
'macro' in query ||
|
||||||
|
('vue' in query && (query.type === 'template' || query.type === 'script' || 'setup' in query))
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// JavaScript files
|
||||||
|
if (pathname.match(/\.((c|m)?j|t)sx?$/g)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
transform (code, id) {
|
transform (code, id) {
|
||||||
// TODO: needed for webpack - update transform in unctx/unplugin?
|
// TODO: needed for webpack - update transform in unctx/unplugin?
|
||||||
if (code.startsWith(TRANSFORM_MARKER)) { return }
|
if (code.startsWith(TRANSFORM_MARKER) || !transformer.shouldTransform(code)) { return }
|
||||||
const result = transformer.transform(code)
|
const result = transformer.transform(code)
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
@ -35,5 +47,5 @@ export const UnctxTransformPlugin = (nuxt: Nuxt) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
}
|
})
|
||||||
|
@ -156,8 +156,10 @@ export default defineNuxtModule({
|
|||||||
layer => resolve(layer.config.srcDir, layer.config.dir?.pages || 'pages')
|
layer => resolve(layer.config.srcDir, layer.config.dir?.pages || 'pages')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
nuxt.hook('modules:done', () => {
|
||||||
addVitePlugin(PageMetaPlugin.vite(pageMetaOptions))
|
addVitePlugin(PageMetaPlugin.vite(pageMetaOptions))
|
||||||
addWebpackPlugin(PageMetaPlugin.webpack(pageMetaOptions))
|
addWebpackPlugin(PageMetaPlugin.webpack(pageMetaOptions))
|
||||||
|
})
|
||||||
|
|
||||||
// Add prefetching support for middleware & layouts
|
// Add prefetching support for middleware & layouts
|
||||||
addPlugin(resolve(runtimeDir, 'plugins/prefetch.client'))
|
addPlugin(resolve(runtimeDir, 'plugins/prefetch.client'))
|
||||||
|
@ -23,6 +23,7 @@ export default defineBuildConfig({
|
|||||||
'vue-bundle-renderer',
|
'vue-bundle-renderer',
|
||||||
'@unhead/schema',
|
'@unhead/schema',
|
||||||
'vue',
|
'vue',
|
||||||
|
'unctx',
|
||||||
'hookable',
|
'hookable',
|
||||||
'nitropack',
|
'nitropack',
|
||||||
'webpack',
|
'webpack',
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||||
"nitropack": "^2.3.2",
|
"nitropack": "^2.3.2",
|
||||||
"unbuild": "latest",
|
"unbuild": "latest",
|
||||||
|
"unctx": "^2.2.0",
|
||||||
"vite": "~4.2.1"
|
"vite": "~4.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -184,5 +184,19 @@ export default defineUntypedSchema({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options passed directly to the transformer from `unctx` that preserves async context
|
||||||
|
* after `await`.
|
||||||
|
*
|
||||||
|
* @type {import('unctx').TransformerOptions}
|
||||||
|
*/
|
||||||
|
asyncTransforms: {
|
||||||
|
asyncFunctions: ['defineNuxtPlugin', 'defineNuxtRouteMiddleware'],
|
||||||
|
objectDefinitions: {
|
||||||
|
defineNuxtComponent: ['asyncData', 'setup'],
|
||||||
|
definePageMeta: ['middleware', 'validate']
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -739,6 +739,9 @@ importers:
|
|||||||
unbuild:
|
unbuild:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
|
unctx:
|
||||||
|
specifier: ^2.2.0
|
||||||
|
version: 2.2.0
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1(@types/node@18.15.11)
|
version: 4.2.1(@types/node@18.15.11)
|
||||||
@ -5099,7 +5102,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.0
|
'@types/estree': 1.0.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/esutils@2.0.3:
|
/esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
@ -8712,7 +8714,6 @@ packages:
|
|||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.0
|
magic-string: 0.30.0
|
||||||
unplugin: 1.3.1
|
unplugin: 1.3.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/undici@5.20.0:
|
/undici@5.20.0:
|
||||||
resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}
|
resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}
|
||||||
|
5
test/fixtures/basic/pages/chunk-error.vue
vendored
5
test/fixtures/basic/pages/chunk-error.vue
vendored
@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: defineNuxtRouteMiddleware(async (to, from) => {
|
async middleware (to, from) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1))
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
if (process.client && from !== to && !nuxtApp.isHydrating) {
|
if (process.client && from !== to && !nuxtApp.isHydrating) {
|
||||||
// trigger a loading error when navigated to via client-side navigation
|
// trigger a loading error when navigated to via client-side navigation
|
||||||
await import(/* webpackIgnore: true */ /* @vite-ignore */ `some-non-exis${''}ting-module`)
|
await import(/* webpackIgnore: true */ /* @vite-ignore */ `some-non-exis${''}ting-module`)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
const someValue = useState('val', () => 1)
|
const someValue = useState('val', () => 1)
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,7 +6,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
|
async setup () {
|
||||||
|
await nextTick()
|
||||||
|
useRuntimeConfig()
|
||||||
|
},
|
||||||
async asyncData () {
|
async asyncData () {
|
||||||
|
await nextTick()
|
||||||
return {
|
return {
|
||||||
hello: await $fetch('/api/hello')
|
hello: await $fetch('/api/hello')
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: defineNuxtRouteMiddleware(async () => {
|
middleware: async () => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1))
|
await new Promise(resolve => setTimeout(resolve, 1))
|
||||||
return navigateTo({ path: '/' }, { redirectCode: 307 })
|
return navigateTo({ path: '/' }, { redirectCode: 307 })
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user