From 72c85032362864e3facb599ff700b7db0d3a59ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20G=C5=82owala?= Date: Thu, 14 Dec 2023 13:41:40 +0100 Subject: [PATCH] fix(nuxt): ensure `error` in `useAsyncData` has correct type (#24396) --- .../nuxt/src/app/composables/asyncData.ts | 29 ++++++------ packages/nuxt/src/app/composables/error.ts | 47 ++++++++++++++----- test/fixtures/basic-types/types.ts | 8 +++- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index b462e0d378..f3efbe46a9 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -2,6 +2,7 @@ import { getCurrentInstance, onBeforeMount, onServerPrefetch, onUnmounted, ref, import type { Ref, WatchSource } from 'vue' import type { NuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt' +import type { NuxtError} from './error'; import { createError } from './error' import { onNuxtReady } from './ready' @@ -82,27 +83,27 @@ const isDefer = (dedupe?: boolean | 'cancel' | 'defer') => dedupe === 'defer' || export function useAsyncData< ResT, - DataE = Error, + NuxtErrorDataT = unknown, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null, > ( handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> export function useAsyncData< ResT, - DataE = Error, + NuxtErrorDataT = unknown, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = DataT, > ( handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> export function useAsyncData< ResT, - DataE = Error, + NuxtErrorDataT = unknown, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null, @@ -110,10 +111,10 @@ export function useAsyncData< key: string, handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> export function useAsyncData< ResT, - DataE = Error, + NuxtErrorDataT = unknown, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = DataT, @@ -121,14 +122,14 @@ export function useAsyncData< key: string, handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> export function useAsyncData< ResT, - DataE = Error, + NuxtErrorDataT = unknown, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null, -> (...args: any[]): AsyncData, DataE | null> { +> (...args: any[]): AsyncData, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> { const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined if (typeof args[0] !== 'string') { args.unshift(autoKey) } @@ -177,7 +178,7 @@ export function useAsyncData< } // TODO: Else, somehow check for conflicting keys with different defaults or fetcher - const asyncData = { ...nuxt._asyncData[key] } as AsyncData + const asyncData = { ...nuxt._asyncData[key] } as AsyncData)> asyncData.refresh = asyncData.execute = (opts = {}) => { if (nuxt._asyncDataPromises[key]) { @@ -224,7 +225,7 @@ export function useAsyncData< // If this request is cancelled, resolve to the latest request. if ((promise as any).cancelled) { return nuxt._asyncDataPromises[key] } - asyncData.error.value = createError(error) as DataE + asyncData.error.value = createError(error) as (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) asyncData.data.value = unref(options.default!()) asyncData.status.value = 'error' }) @@ -295,10 +296,10 @@ export function useAsyncData< } // Allow directly awaiting on asyncData - const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData + const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData)> Object.assign(asyncDataPromise, asyncData) - return asyncDataPromise as AsyncData, DataE> + return asyncDataPromise as AsyncData, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError)> } export function useLazyAsyncData< ResT, diff --git a/packages/nuxt/src/app/composables/error.ts b/packages/nuxt/src/app/composables/error.ts index 4a59bc6a7a..4ba972de71 100644 --- a/packages/nuxt/src/app/composables/error.ts +++ b/packages/nuxt/src/app/composables/error.ts @@ -1,44 +1,65 @@ import type { H3Error } from 'h3' -import { createError as _createError } from 'h3' +import { createError as createH3Error } from 'h3' import { toRef } from 'vue' import { useNuxtApp } from '../nuxt' import { useRouter } from './router' +export const NUXT_ERROR_SIGNATURE = '__nuxt_error' + export const useError = () => toRef(useNuxtApp().payload, 'error') -export interface NuxtError extends H3Error {} +export interface NuxtError extends H3Error {} -export const showError = (_err: string | Error | Partial) => { - const err = createError(_err) +export const showError = ( + error: string | Error | Partial> +) => { + const nuxtError = createError(error) try { const nuxtApp = useNuxtApp() const error = useError() + if (import.meta.client) { - nuxtApp.hooks.callHook('app:error', err) + nuxtApp.hooks.callHook('app:error', nuxtError) } - error.value = error.value || err + + error.value = error.value || nuxtError } catch { - throw err + throw nuxtError } - return err + return nuxtError } export const clearError = async (options: { redirect?: string } = {}) => { const nuxtApp = useNuxtApp() const error = useError() + nuxtApp.callHook('app:error:cleared', options) + if (options.redirect) { await useRouter().replace(options.redirect) } + error.value = null } -export const isNuxtError = (err?: string | object): err is NuxtError => !!(err && typeof err === 'object' && ('__nuxt_error' in err)) +export const isNuxtError = ( + error?: string | object +): error is NuxtError => ( + !!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error +) -export const createError = (err: string | Partial): NuxtError => { - const _err: NuxtError = _createError(err) - ;(_err as any).__nuxt_error = true - return _err +export const createError = ( + error: string | Partial> +) => { + const nuxtError: NuxtError = createH3Error(error) + + Object.defineProperty(nuxtError, NUXT_ERROR_SIGNATURE, { + value: true, + configurable: false, + writable: false + }) + + return nuxtError } diff --git a/test/fixtures/basic-types/types.ts b/test/fixtures/basic-types/types.ts index a65bdd6d09..3efc8b1dfd 100644 --- a/test/fixtures/basic-types/types.ts +++ b/test/fixtures/basic-types/types.ts @@ -6,6 +6,7 @@ import type { NavigationFailure, RouteLocationNormalized, RouteLocationRaw, Rout import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema' import { defineNuxtConfig } from 'nuxt/config' import { callWithNuxt, isVue3 } from '#app' +import type { NuxtError } from '#app' import type { NavigateToOptions } from '#app/composables/router' import { NuxtLayout, NuxtLink, NuxtPage, WithTypes } from '#components' import { useRouter } from '#imports' @@ -38,8 +39,11 @@ describe('API routes', () => { expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf>() expectTypeOf(useAsyncData('api-generics', () => $fetch('/test')).data).toEqualTypeOf>() - expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf>() - expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf>() + expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf | null>>() + expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf | null>>() + // backwards compatibility + expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf>() + expectTypeOf(useAsyncData>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf | null>>() expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf>() expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf>()