feat(nuxt): add nuxtApp.runWithContext (#20608)

This commit is contained in:
Daniel Roe 2023-05-03 11:02:07 +01:00 committed by GitHub
parent a81f9e4c82
commit da3357449f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 36 additions and 29 deletions

View File

@ -9,7 +9,7 @@
<script setup> <script setup>
import { defineAsyncComponent, onErrorCaptured, onServerPrefetch, provide } from 'vue' import { defineAsyncComponent, onErrorCaptured, onServerPrefetch, provide } from 'vue'
import { callWithNuxt, useNuxtApp } from '#app/nuxt' import { useNuxtApp } from '#app/nuxt'
import { isNuxtError, showError, useError } from '#app/composables/error' import { isNuxtError, showError, useError } from '#app/composables/error'
import { useRoute } from '#app/composables/router' import { useRoute } from '#app/composables/router'
import AppComponent from '#build/app-component.mjs' import AppComponent from '#build/app-component.mjs'
@ -40,7 +40,7 @@ const error = useError()
onErrorCaptured((err, target, info) => { onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError)) nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (process.server || (isNuxtError(err) && (err.fatal || err.unhandled))) { if (process.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
const p = callWithNuxt(nuxtApp, showError, [err]) const p = nuxtApp.runWithContext(() => showError(err))
onServerPrefetch(() => p) onServerPrefetch(() => p)
return false // suppress error from breaking render return false // suppress error from breaking render
} }

View File

@ -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 { callWithNuxt, useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
import { useAsyncData } from './asyncData' import { useAsyncData } from './asyncData'
import { useRoute } from './router' import { useRoute } from './router'
import { createError } from './error' import { createError } from './error'
@ -10,12 +10,12 @@ import { createError } from './error'
export const NuxtComponentIndicator = '__nuxt_component' export const NuxtComponentIndicator = '__nuxt_component'
async function runLegacyAsyncData (res: Record<string, any> | Promise<Record<string, any>>, fn: (nuxtApp: NuxtApp) => Promise<Record<string, any>>) { async function runLegacyAsyncData (res: Record<string, any> | Promise<Record<string, any>>, fn: (nuxtApp: NuxtApp) => Promise<Record<string, any>>) {
const nuxt = useNuxtApp() const nuxtApp = useNuxtApp()
const route = useRoute() const route = useRoute()
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, error } = await useAsyncData(`options:asyncdata:${key}`, () => callWithNuxt(nuxt, fn, [nuxt])) const { data, error } = await useAsyncData(`options:asyncdata:${key}`, () => nuxtApp.runWithContext(() => fn(nuxtApp)))
if (error.value) { if (error.value) {
throw createError(error.value) throw createError(error.value)
} }
@ -43,7 +43,7 @@ export const defineNuxtComponent: typeof defineComponent =
...options, ...options,
setup (props, ctx) { setup (props, ctx) {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const res = setup ? Promise.resolve(callWithNuxt(nuxtApp, setup, [props, ctx])).then(r => r || {}) : {} const res = setup ? Promise.resolve(nuxtApp.runWithContext(() => setup(props, ctx))).then(r => r || {}) : {}
const promises: Promise<any>[] = [] const promises: Promise<any>[] = []
if (options.asyncData) { if (options.asyncData) {

View File

@ -76,6 +76,8 @@ interface _NuxtApp {
hook: _NuxtApp['hooks']['hook'] hook: _NuxtApp['hooks']['hook']
callHook: _NuxtApp['hooks']['callHook'] callHook: _NuxtApp['hooks']['callHook']
runWithContext: <T extends () => any>(fn: T) => ReturnType<T> | Promise<Awaited<ReturnType<T>>>
[key: string]: unknown [key: string]: unknown
/** @internal */ /** @internal */
@ -193,6 +195,7 @@ export function createNuxtApp (options: CreateOptions) {
static: { static: {
data: {} data: {}
}, },
runWithContext: (fn: any) => callWithNuxt(nuxtApp, fn),
isHydrating: process.client, isHydrating: process.client,
deferHydration () { deferHydration () {
if (!nuxtApp.isHydrating) { return () => {} } if (!nuxtApp.isHydrating) { return () => {} }
@ -224,7 +227,7 @@ export function createNuxtApp (options: CreateOptions) {
if (process.server) { if (process.server) {
async function contextCaller (hooks: HookCallback[], args: any[]) { async function contextCaller (hooks: HookCallback[], args: any[]) {
for (const hook of hooks) { for (const hook of hooks) {
await nuxtAppCtx.callAsync(nuxtApp, () => hook(...args)) await nuxtApp.runWithContext(() => hook(...args))
} }
} }
// Patch callHook to preserve NuxtApp context on server // Patch callHook to preserve NuxtApp context on server
@ -288,7 +291,7 @@ export function createNuxtApp (options: CreateOptions) {
export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin) { export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin) {
if (typeof plugin !== 'function') { return } if (typeof plugin !== 'function') { return }
const { provide } = await callWithNuxt(nuxtApp, plugin, [nuxtApp]) || {} const { provide } = await nuxtApp.runWithContext(() => plugin(nuxtApp)) || {}
if (provide && typeof provide === 'object') { if (provide && typeof provide === 'object') {
for (const key in provide) { for (const key in provide) {
nuxtApp.provide(key, provide[key]) nuxtApp.provide(key, provide[key])

View File

@ -1,7 +1,7 @@
import { reactive, ref, shallowReactive, shallowRef } from 'vue' import { reactive, ref, shallowReactive, shallowRef } from 'vue'
import { definePayloadReviver, getNuxtClientPayload } from '#app/composables/payload' import { definePayloadReviver, getNuxtClientPayload } from '#app/composables/payload'
import { createError } from '#app/composables/error' import { createError } from '#app/composables/error'
import { callWithNuxt, defineNuxtPlugin } from '#app/nuxt' import { defineNuxtPlugin } from '#app/nuxt'
const revivers = { const revivers = {
NuxtError: (data: any) => createError(data), NuxtError: (data: any) => createError(data),
@ -20,7 +20,7 @@ export default defineNuxtPlugin({
for (const reviver in revivers) { for (const reviver in revivers) {
definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers]) definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers])
} }
Object.assign(nuxtApp.payload, await callWithNuxt(nuxtApp, getNuxtClientPayload, [])) Object.assign(nuxtApp.payload, await nuxtApp.runWithContext(getNuxtClientPayload))
// For backwards compatibility - TODO: remove later // For backwards compatibility - TODO: remove later
window.__NUXT__ = nuxtApp.payload window.__NUXT__ = nuxtApp.payload
} }

View File

@ -1,7 +1,7 @@
import { h, isReadonly, reactive } from 'vue' import { h, isReadonly, reactive } from 'vue'
import { isEqual, joinURL, parseQuery, parseURL, stringifyParsedURL, stringifyQuery, withoutBase } from 'ufo' import { isEqual, joinURL, parseQuery, parseURL, stringifyParsedURL, stringifyQuery, withoutBase } from 'ufo'
import { createError } from 'h3' import { createError } from 'h3'
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig } from '../nuxt' import { defineNuxtPlugin, useRuntimeConfig } from '../nuxt'
import { clearError, showError } from '../composables/error' import { clearError, showError } from '../composables/error'
import { navigateTo } from '../composables/router' import { navigateTo } from '../composables/router'
import { useState } from '../composables/state' import { useState } from '../composables/state'
@ -142,7 +142,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({
window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath)) window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath))
if (!nuxtApp.isHydrating) { if (!nuxtApp.isHydrating) {
// Clear any existing errors // Clear any existing errors
await callWithNuxt(nuxtApp, clearError) await nuxtApp.runWithContext(clearError)
} }
} }
// Run afterEach hooks // Run afterEach hooks
@ -238,7 +238,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({
const middlewareEntries = new Set<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global]) const middlewareEntries = new Set<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global])
for (const middleware of middlewareEntries) { for (const middleware of middlewareEntries) {
const result = await callWithNuxt(nuxtApp, middleware, [to, from]) const result = await nuxtApp.runWithContext(() => middleware(to, from))
if (process.server) { if (process.server) {
if (result === false || result instanceof Error) { if (result === false || result instanceof Error) {
const error = result || createError({ const error = result || createError({
@ -246,7 +246,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({
statusMessage: `Page Not Found: ${initialURL}` statusMessage: `Page Not Found: ${initialURL}`
}) })
delete nuxtApp._processingMiddleware delete nuxtApp._processingMiddleware
return callWithNuxt(nuxtApp, showError, [error]) return nuxtApp.runWithContext(() => showError(error))
} }
} }
if (result || result === false) { return result } if (result || result === false) { return result }
@ -257,7 +257,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({
await router.replace(initialURL) await router.replace(initialURL)
if (!isEqual(route.fullPath, initialURL)) { if (!isEqual(route.fullPath, initialURL)) {
await callWithNuxt(nuxtApp, navigateTo, [route.fullPath]) await nuxtApp.runWithContext(() => navigateTo(route.fullPath))
} }
}) })

View File

@ -11,7 +11,7 @@ import { createError } from 'h3'
import { withoutBase } from 'ufo' import { withoutBase } from 'ufo'
import type { PageMeta, Plugin, RouteMiddleware } from '../../../app/index' import type { PageMeta, Plugin, RouteMiddleware } from '../../../app/index'
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt' import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
import { clearError, showError, useError } from '#app/composables/error' import { clearError, showError, useError } from '#app/composables/error'
import { useState } from '#app/composables/state' import { useState } from '#app/composables/state'
import { navigateTo } from '#app/composables/router' import { navigateTo } from '#app/composables/router'
@ -113,7 +113,7 @@ export default defineNuxtPlugin({
await router.isReady() await router.isReady()
} catch (error: any) { } catch (error: any) {
// We'll catch 404s here // We'll catch 404s here
await callWithNuxt(nuxtApp, showError, [error]) await nuxtApp.runWithContext(() => showError(error))
} }
const initialLayout = useState('_layout') const initialLayout = useState('_layout')
@ -148,14 +148,14 @@ export default defineNuxtPlugin({
throw new Error(`Unknown route middleware: '${entry}'.`) throw new Error(`Unknown route middleware: '${entry}'.`)
} }
const result = await callWithNuxt(nuxtApp, middleware, [to, from]) const result = await nuxtApp.runWithContext(() => middleware(to, from))
if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) { if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) {
if (result === false || result instanceof Error) { if (result === false || result instanceof Error) {
const error = result || createError({ const error = result || createError({
statusCode: 404, statusCode: 404,
statusMessage: `Page Not Found: ${initialURL}` statusMessage: `Page Not Found: ${initialURL}`
}) })
await callWithNuxt(nuxtApp, showError, [error]) await nuxtApp.runWithContext(() => showError(error))
return false return false
} }
} }
@ -170,19 +170,19 @@ export default defineNuxtPlugin({
if (process.client && !nuxtApp.isHydrating && error.value) { if (process.client && !nuxtApp.isHydrating && error.value) {
// Clear any existing errors // Clear any existing errors
await callWithNuxt(nuxtApp, clearError) await nuxtApp.runWithContext(clearError)
} }
if (process.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) { if (process.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) {
return return
} }
if (to.matched.length === 0) { if (to.matched.length === 0) {
await callWithNuxt(nuxtApp, showError, [createError({ await nuxtApp.runWithContext(() => showError(createError({
statusCode: 404, statusCode: 404,
fatal: false, fatal: false,
statusMessage: `Page not found: ${to.fullPath}` statusMessage: `Page not found: ${to.fullPath}`
})]) })))
} else if (process.server && to.redirectedFrom) { } else if (process.server && to.redirectedFrom) {
await callWithNuxt(nuxtApp, navigateTo, [to.fullPath || '/']) await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
} }
}) })
@ -195,7 +195,7 @@ export default defineNuxtPlugin({
}) })
} catch (error: any) { } catch (error: any) {
// We'll catch middleware errors or deliberate exceptions here // We'll catch middleware errors or deliberate exceptions here
await callWithNuxt(nuxtApp, showError, [error]) await nuxtApp.runWithContext(() => showError(error))
} }
}) })

View File

@ -1,5 +1,5 @@
import { createError, showError } from '#app/composables/error' import { createError, showError } from '#app/composables/error'
import { callWithNuxt, useNuxtApp } from '#app/nuxt' import { useNuxtApp } from '#app/nuxt'
import { defineNuxtRouteMiddleware, useRouter } from '#app/composables/router' import { defineNuxtRouteMiddleware, useRouter } from '#app/composables/router'
export default defineNuxtRouteMiddleware(async (to) => { export default defineNuxtRouteMiddleware(async (to) => {
@ -24,7 +24,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (final === to) { if (final === to) {
const unsub = router.afterEach(async () => { const unsub = router.afterEach(async () => {
unsub() unsub()
await callWithNuxt(nuxtApp, showError, [error]) await nuxtApp.runWithContext(() => showError(error))
// We pretend to have navigated to the invalid route so // We pretend to have navigated to the invalid route so
// that the user can return to the previous page with // that the user can return to the previous page with
// the back button. // the back button.

View File

@ -34,7 +34,7 @@ describe.skipIf(isWindows || process.env.TEST_BUILDER === 'webpack' || process.e
it('default client bundle size', async () => { it('default client bundle size', async () => {
stats.client = await analyzeSizes('**/*.js', publicDir) stats.client = await analyzeSizes('**/*.js', publicDir)
expect(roundToKilobytes(stats.client.totalBytes)).toMatchInlineSnapshot('"94.0k"') expect(roundToKilobytes(stats.client.totalBytes)).toMatchInlineSnapshot('"94.1k"')
expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(` expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[ [
"_nuxt/entry.js", "_nuxt/entry.js",
@ -45,7 +45,7 @@ describe.skipIf(isWindows || process.env.TEST_BUILDER === 'webpack' || process.e
it('default server bundle size', async () => { it('default server bundle size', async () => {
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"66.6k"') expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"66.7k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2654k"') expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2654k"')

View File

@ -96,7 +96,7 @@ describe('middleware', () => {
addRouteMiddleware('example', (to, from) => { addRouteMiddleware('example', (to, from) => {
expectTypeOf(to).toEqualTypeOf<RouteLocationNormalizedLoaded>() expectTypeOf(to).toEqualTypeOf<RouteLocationNormalizedLoaded>()
expectTypeOf(from).toEqualTypeOf<RouteLocationNormalizedLoaded>() expectTypeOf(from).toEqualTypeOf<RouteLocationNormalizedLoaded>()
expectTypeOf(navigateTo).toEqualTypeOf <(to: RouteLocationRaw | null | undefined, options ?: NavigateToOptions) => RouteLocationRaw | void | false | Promise<void | NavigationFailure | false>>() expectTypeOf(navigateTo).toEqualTypeOf<(to: RouteLocationRaw | null | undefined, options?: NavigateToOptions) => RouteLocationRaw | void | false | Promise<void | NavigationFailure | false>>()
navigateTo('/') navigateTo('/')
abortNavigation() abortNavigation()
abortNavigation('error string') abortNavigation('error string')
@ -315,4 +315,8 @@ describe('composables inference', () => {
const bob = callWithNuxt({} as any, () => true) const bob = callWithNuxt({} as any, () => true)
expectTypeOf<typeof bob>().toEqualTypeOf<boolean | Promise<boolean>>() expectTypeOf<typeof bob>().toEqualTypeOf<boolean | Promise<boolean>>()
}) })
it('runWithContext', () => {
const bob = useNuxtApp().runWithContext(() => true)
expectTypeOf<typeof bob>().toEqualTypeOf<boolean | Promise<boolean>>()
})
}) })