mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
feat(nuxt): experimental native async context support (#20918)
Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
parent
9c5b9b7d53
commit
554f868bce
@ -17,7 +17,9 @@ import type { RouteMiddleware } from '../../app'
|
|||||||
import type { NuxtError } from '../app/composables/error'
|
import type { NuxtError } from '../app/composables/error'
|
||||||
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
||||||
|
|
||||||
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
|
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app', {
|
||||||
|
asyncContext: !!process.env.NUXT_ASYNC_CONTEXT && process.server
|
||||||
|
})
|
||||||
|
|
||||||
type HookResult = Promise<void> | void
|
type HookResult = Promise<void> | void
|
||||||
|
|
||||||
|
@ -45,7 +45,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
|||||||
buildDir: nuxt.options.buildDir,
|
buildDir: nuxt.options.buildDir,
|
||||||
experimental: {
|
experimental: {
|
||||||
// @ts-expect-error `typescriptBundlerResolution` coming in next nitro version
|
// @ts-expect-error `typescriptBundlerResolution` coming in next nitro version
|
||||||
typescriptBundlerResolution: nuxt.options.experimental.typescriptBundlerResolution || nuxt.options.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' || _nitroConfig.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler'
|
typescriptBundlerResolution: nuxt.options.experimental.typescriptBundlerResolution || nuxt.options.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' || _nitroConfig.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler',
|
||||||
|
asyncContext: nuxt.options.experimental.asyncContext
|
||||||
},
|
},
|
||||||
imports: {
|
imports: {
|
||||||
autoImport: nuxt.options.imports.autoImport as boolean,
|
autoImport: nuxt.options.imports.autoImport as boolean,
|
||||||
@ -191,6 +192,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
|||||||
'process.env.NUXT_INLINE_STYLES': !!nuxt.options.experimental.inlineSSRStyles,
|
'process.env.NUXT_INLINE_STYLES': !!nuxt.options.experimental.inlineSSRStyles,
|
||||||
'process.env.NUXT_JSON_PAYLOADS': !!nuxt.options.experimental.renderJsonPayloads,
|
'process.env.NUXT_JSON_PAYLOADS': !!nuxt.options.experimental.renderJsonPayloads,
|
||||||
'process.env.NUXT_COMPONENT_ISLANDS': !!nuxt.options.experimental.componentIslands,
|
'process.env.NUXT_COMPONENT_ISLANDS': !!nuxt.options.experimental.componentIslands,
|
||||||
|
'process.env.NUXT_ASYNC_CONTEXT': !!nuxt.options.experimental.asyncContext,
|
||||||
'process.dev': nuxt.options.dev,
|
'process.dev': nuxt.options.dev,
|
||||||
__VUE_PROD_DEVTOOLS__: false
|
__VUE_PROD_DEVTOOLS__: false
|
||||||
},
|
},
|
||||||
|
@ -25,6 +25,7 @@ import { addModuleTranspiles } from './modules'
|
|||||||
import { initNitro } from './nitro'
|
import { initNitro } from './nitro'
|
||||||
import schemaModule from './schema'
|
import schemaModule from './schema'
|
||||||
import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
|
import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
|
||||||
|
import { AsyncContextInjectionPlugin } from './plugins/async-context'
|
||||||
import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports'
|
import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports'
|
||||||
|
|
||||||
export function createNuxt (options: NuxtOptions): Nuxt {
|
export function createNuxt (options: NuxtOptions): Nuxt {
|
||||||
@ -143,6 +144,11 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
addWebpackPlugin(() => DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))
|
addWebpackPlugin(() => DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform initial composable call within `<script setup>` to preserve context
|
||||||
|
if (nuxt.options.experimental.asyncContext) {
|
||||||
|
addBuildPlugin(AsyncContextInjectionPlugin(nuxt))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: [Experimental] Avoid emitting assets when flag is enabled
|
// TODO: [Experimental] Avoid emitting assets when flag is enabled
|
||||||
if (nuxt.options.experimental.noScripts && !nuxt.options.dev) {
|
if (nuxt.options.experimental.noScripts && !nuxt.options.dev) {
|
||||||
nuxt.hook('build:manifest', async (manifest) => {
|
nuxt.hook('build:manifest', async (manifest) => {
|
||||||
|
54
packages/nuxt/src/core/plugins/async-context.ts
Normal file
54
packages/nuxt/src/core/plugins/async-context.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { createUnplugin } from 'unplugin'
|
||||||
|
import MagicString from 'magic-string'
|
||||||
|
import type { Nuxt } from '@nuxt/schema'
|
||||||
|
import type { Node } from 'estree-walker'
|
||||||
|
import { walk } from 'estree-walker'
|
||||||
|
import type { BlockStatement } from 'estree'
|
||||||
|
import { isVue } from '../utils'
|
||||||
|
|
||||||
|
export const AsyncContextInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
|
||||||
|
return {
|
||||||
|
name: 'nuxt:async-context-injection',
|
||||||
|
transformInclude (id) {
|
||||||
|
return isVue(id, { type: ['template', 'script'] })
|
||||||
|
},
|
||||||
|
transform (code) {
|
||||||
|
const s = new MagicString(code)
|
||||||
|
|
||||||
|
let importName: string
|
||||||
|
|
||||||
|
walk(this.parse(code) as Node, {
|
||||||
|
enter (node) {
|
||||||
|
// only interested in calls of defineComponent function
|
||||||
|
if (node.type === 'ImportDeclaration' && node.source.value === 'vue') {
|
||||||
|
importName = importName ?? node.specifiers.find(s => s.type === 'ImportSpecifier' && s.imported.name === 'defineComponent')?.local.name
|
||||||
|
}
|
||||||
|
// we only want to transform `async setup()` functions
|
||||||
|
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === importName) {
|
||||||
|
walk(node, {
|
||||||
|
enter (setup) {
|
||||||
|
if (setup.type === 'Property' && setup.key.type === 'Identifier' && setup.key.name === 'setup') {
|
||||||
|
if (setup.value.type === 'FunctionExpression' && setup.value.async) {
|
||||||
|
const body: BlockStatement = setup.value.body
|
||||||
|
const { start, end } = body as BlockStatement & { start: number, end: number }
|
||||||
|
s.appendLeft(start, '{ return useNuxtApp().runWithContext(async () => ')
|
||||||
|
s.appendRight(end, ') }')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (s.hasChanged()) {
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: nuxt.options.sourcemap.client || nuxt.options.sourcemap.server
|
||||||
|
? s.generateMap({ hires: true })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||||
import {
|
import {
|
||||||
createRenderer,
|
createRenderer,
|
||||||
getPrefetchLinks,
|
getPrefetchLinks,
|
||||||
@ -34,6 +35,11 @@ globalThis.__buildAssetsURL = buildAssetsURL
|
|||||||
// @ts-expect-error private property consumed by vite-generated url helpers
|
// @ts-expect-error private property consumed by vite-generated url helpers
|
||||||
globalThis.__publicAssetsURL = publicAssetsURL
|
globalThis.__publicAssetsURL = publicAssetsURL
|
||||||
|
|
||||||
|
// Polyfill for unctx (https://github.com/unjs/unctx#native-async-context)
|
||||||
|
if (process.env.NUXT_ASYNC_CONTEXT && !('AsyncLocalStorage' in globalThis)) {
|
||||||
|
(globalThis as any).AsyncLocalStorage = AsyncLocalStorage
|
||||||
|
}
|
||||||
|
|
||||||
export interface NuxtRenderHTMLContext {
|
export interface NuxtRenderHTMLContext {
|
||||||
island?: boolean
|
island?: boolean
|
||||||
htmlAttrs: string[]
|
htmlAttrs: string[]
|
||||||
|
@ -63,7 +63,7 @@ export default defineUntypedSchema({
|
|||||||
* is kept in sync with the current page in view in `<NuxtPage>`. This is not true for
|
* is kept in sync with the current page in view in `<NuxtPage>`. This is not true for
|
||||||
* `vue-router`'s exported `useRoute` or for the default `$route` object available in your
|
* `vue-router`'s exported `useRoute` or for the default `$route` object available in your
|
||||||
* Vue templates.
|
* Vue templates.
|
||||||
*
|
*
|
||||||
* By enabling this option a mixin will be injected to keep the `$route` template object
|
* By enabling this option a mixin will be injected to keep the `$route` template object
|
||||||
* in sync with Nuxt's managed `useRoute()`.
|
* in sync with Nuxt's managed `useRoute()`.
|
||||||
*/
|
*/
|
||||||
@ -220,6 +220,13 @@ export default defineUntypedSchema({
|
|||||||
*/
|
*/
|
||||||
watcher: 'chokidar-granular',
|
watcher: 'chokidar-granular',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable native async context to be accessable for nested composables
|
||||||
|
*
|
||||||
|
* @see https://github.com/nuxt/nuxt/pull/20918
|
||||||
|
*/
|
||||||
|
asyncContext: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the capo.js head plugin in order to render tags in of the head in a more performant way.
|
* Add the capo.js head plugin in order to render tags in of the head in a more performant way.
|
||||||
*
|
*
|
||||||
|
@ -82,7 +82,10 @@ export async function bundle (nuxt: Nuxt) {
|
|||||||
exclude: ['nuxt/app']
|
exclude: ['nuxt/app']
|
||||||
},
|
},
|
||||||
css: resolveCSSOptions(nuxt),
|
css: resolveCSSOptions(nuxt),
|
||||||
define: { __NUXT_VERSION__: JSON.stringify(nuxt._version) },
|
define: {
|
||||||
|
__NUXT_VERSION__: JSON.stringify(nuxt._version),
|
||||||
|
'process.env.NUXT_ASYNC_CONTEXT': nuxt.options.experimental.asyncContext
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
copyPublicDir: false,
|
copyPublicDir: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
@ -218,6 +218,7 @@ function getEnv (ctx: WebpackConfigContext) {
|
|||||||
'process.env.NODE_ENV': JSON.stringify(ctx.config.mode),
|
'process.env.NODE_ENV': JSON.stringify(ctx.config.mode),
|
||||||
__NUXT_VERSION__: JSON.stringify(ctx.nuxt._version),
|
__NUXT_VERSION__: JSON.stringify(ctx.nuxt._version),
|
||||||
'process.env.VUE_ENV': JSON.stringify(ctx.name),
|
'process.env.VUE_ENV': JSON.stringify(ctx.name),
|
||||||
|
'process.env.NUXT_ASYNC_CONTEXT': ctx.options.experimental.asyncContext,
|
||||||
'process.dev': ctx.options.dev,
|
'process.dev': ctx.options.dev,
|
||||||
'process.test': isTest,
|
'process.test': isTest,
|
||||||
'process.browser': ctx.isClient,
|
'process.browser': ctx.isClient,
|
||||||
|
@ -1901,6 +1901,12 @@ describe.skipIf(isDev() || isWindows || !isRenderingJson)('payload rendering', (
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Async context', () => {
|
||||||
|
it('should be available', async () => {
|
||||||
|
expect(await $fetch('/async-context')).toContain('"hasApp": true')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe.skipIf(isWindows)('useAsyncData', () => {
|
describe.skipIf(isWindows)('useAsyncData', () => {
|
||||||
it('single request resolves', async () => {
|
it('single request resolves', async () => {
|
||||||
await expectNoClientErrors('/useAsyncData/single')
|
await expectNoClientErrors('/useAsyncData/single')
|
||||||
|
19
test/fixtures/basic/composables/async-context.ts
vendored
Normal file
19
test/fixtures/basic/composables/async-context.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const delay = () => new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
|
||||||
|
export async function nestedAsyncComposable () {
|
||||||
|
await delay()
|
||||||
|
return await fn1()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fn1 () {
|
||||||
|
await delay()
|
||||||
|
return await fn2()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fn2 () {
|
||||||
|
await delay()
|
||||||
|
const app = useNuxtApp()
|
||||||
|
return {
|
||||||
|
hasApp: !!app
|
||||||
|
}
|
||||||
|
}
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -188,6 +188,7 @@ export default defineNuxtConfig({
|
|||||||
reactivityTransform: true,
|
reactivityTransform: true,
|
||||||
treeshakeClientOnly: true,
|
treeshakeClientOnly: true,
|
||||||
payloadExtraction: true,
|
payloadExtraction: true,
|
||||||
|
asyncContext: true,
|
||||||
headCapoPlugin: true
|
headCapoPlugin: true
|
||||||
},
|
},
|
||||||
appConfig: {
|
appConfig: {
|
||||||
|
12
test/fixtures/basic/pages/async-context.vue
vendored
Normal file
12
test/fixtures/basic/pages/async-context.vue
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Head>
|
||||||
|
<Title>Native Async Context</Title>
|
||||||
|
</Head>
|
||||||
|
{{ data }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const data = await nestedAsyncComposable()
|
||||||
|
</script>
|
@ -11,6 +11,9 @@ export default defineConfig({
|
|||||||
'@nuxt/test-utils': resolve('./packages/test-utils/src/index.ts')
|
'@nuxt/test-utils': resolve('./packages/test-utils/src/index.ts')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
'process.env.NUXT_ASYNC_CONTEXT': 'false'
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
globalSetup: './test/setup.ts',
|
globalSetup: './test/setup.ts',
|
||||||
testTimeout: isWindows ? 60000 : 10000,
|
testTimeout: isWindows ? 60000 : 10000,
|
||||||
|
Loading…
Reference in New Issue
Block a user