mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nitro, nuxt3): allow handling otherwise unhandled runtime errors (#3464)
Co-authored-by: pooya parsa <pyapar@gmail.com>
This commit is contained in:
parent
cff2f37cc8
commit
5d58ef48af
90
docs/content/3.docs/1.usage/8.error-handling.md
Normal file
90
docs/content/3.docs/1.usage/8.error-handling.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Error handling
|
||||
|
||||
## Handling errors
|
||||
|
||||
Nuxt 3 is a full-stack framework, which means there are several sources of unpreventable user runtime errors that can happen in different contexts:
|
||||
|
||||
1. Errors during the Vue rendering lifecycle (SSR + SPA)
|
||||
1. Errors during API or Nitro server lifecycle
|
||||
1. Server and client startup errors (SSR + SPA)
|
||||
|
||||
### Errors during the Vue rendering lifecycle (SSR + SPA)
|
||||
|
||||
You can hook into Vue errors using [`onErrorCaptured`](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured).
|
||||
|
||||
In addition, Nuxt provides a `vue:error` hook that will be called if there are any errors that propagate up to the top level.
|
||||
|
||||
If you are using a error reporting framework, you can provide a global handler through [`vueApp.config.errorHandler`](https://vuejs.org/api/application.html#app-config-errorhandler). It will receive all Vue errors, even if they are handled.
|
||||
|
||||
#### Example with global error reporting framework
|
||||
|
||||
```js
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.config.errorHandler = (error, context) => {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Server and client startup errors (SSR + SPA)
|
||||
|
||||
Nuxt provides an `app:error` hook that will be called if there are any errors in starting your Nuxt application.
|
||||
|
||||
This includes:
|
||||
|
||||
* running Nuxt plugins
|
||||
* processing `app:created` and `app:beforeMount` hooks
|
||||
* mounting the app (on client-side), though you should handle this case with `onErrorCaptured` or with `vue:error`
|
||||
* processing the `app:mounted` hook
|
||||
|
||||
### Errors during API or Nitro server lifecycle
|
||||
|
||||
You cannot currently define a server-side handler for these errors, but can render an error page (see the next section).
|
||||
|
||||
## Rendering an error page
|
||||
|
||||
When Nuxt encounters a fatal error, whether during the server lifecycle, or when rendering your Vue application (both SSR and SPA), it will either render a JSON response (if requested with `Accept: application/json` header) or an HTML error page.
|
||||
|
||||
You can customize this error page by adding `~/error.vue` in the source directory of your application, alongside `app.vue`. This page has a single prop - `error` which contains an error for you to handle.
|
||||
|
||||
When you are ready to remove the error page, you can call the `clearError` helper function, which takes an optional path to redirect to (for example, if you want to navigate to a 'safe' page).
|
||||
|
||||
::alert{type="warning"}
|
||||
Make sure to check before using anything dependent on Nuxt plugins, such as `$route` or `useRouter`, as if a plugin threw an error, then it won't be re-run until you clear the error.
|
||||
::
|
||||
|
||||
### Example
|
||||
|
||||
```vue [error.vue]
|
||||
<template>
|
||||
<button @click="handleError">Clear errors</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
error: Object
|
||||
})
|
||||
|
||||
const handleError = () => clearError({ redirect: '/' })
|
||||
</script>
|
||||
```
|
||||
|
||||
## Error helper methods
|
||||
|
||||
### useError
|
||||
|
||||
* `function useError (): Ref<any>`
|
||||
|
||||
This function will return the global Nuxt error that is being handled.
|
||||
|
||||
### throwError
|
||||
|
||||
* `function throwError (err: string | Error): Error`
|
||||
|
||||
You can call this function at any point on client-side, or (on server side) directly within middleware, plugins or `setup()` functions. It will trigger a full-screen error page (as above) which you can clear with `clearError`.
|
||||
|
||||
### clearError
|
||||
|
||||
* `function clearError (redirect?: string): Promise<void>`
|
||||
|
||||
This function will clear the currently handled Nuxt error. It also takes an optional path to redirect to (for example, if you want to navigate to a 'safe' page).
|
45
examples/with-errors/app.vue
Normal file
45
examples/with-errors/app.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { throwError } from '#app'
|
||||
const route = useRoute()
|
||||
if ('setup' in route.query) {
|
||||
throw new Error('error in setup')
|
||||
}
|
||||
if ('mounted' in route.query) {
|
||||
onMounted(() => {
|
||||
throw new Error('error in mounted')
|
||||
})
|
||||
}
|
||||
function triggerError () {
|
||||
throw new Error('manually triggered error')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtExampleLayout example="with-errors">
|
||||
<template #nav>
|
||||
<nav class="flex align-center gap-4 p-4">
|
||||
<NuxtLink to="/" class="n-link-base">
|
||||
Home
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/404" class="n-link-base">
|
||||
404
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/?middleware" class="n-link-base">
|
||||
Middleware
|
||||
</NuxtLink>
|
||||
<button class="n-link-base" @click="throwError">
|
||||
Trigger fatal error
|
||||
</button>
|
||||
<button class="n-link-base" @click="triggerError">
|
||||
Trigger non-fatal error
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="text-center p-4 op-50">
|
||||
Current route: <code>{{ route.path }}</code>
|
||||
</div>
|
||||
</template>
|
||||
</NuxtExampleLayout>
|
||||
</template>
|
26
examples/with-errors/error.vue
Normal file
26
examples/with-errors/error.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="relative font-sans" n="green6">
|
||||
<div class="container max-w-200 mx-auto py-10 px-4">
|
||||
<h1>{{ error.message }}</h1>
|
||||
There was an error 😱
|
||||
|
||||
<button @click="handleError">
|
||||
Clear error
|
||||
</button>
|
||||
<NuxtLink to="/404">
|
||||
Trigger another error
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/">
|
||||
Navigate home
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
error: Object
|
||||
})
|
||||
|
||||
const handleError = () => clearError({ redirect: '/' })
|
||||
</script>
|
5
examples/with-errors/middleware/error.global.ts
Normal file
5
examples/with-errors/middleware/error.global.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default defineNuxtRouteMiddleware((to) => {
|
||||
if ('middleware' in to.query) {
|
||||
return throwError('error in middleware')
|
||||
}
|
||||
})
|
7
examples/with-errors/nuxt.config.ts
Normal file
7
examples/with-errors/nuxt.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineNuxtConfig } from 'nuxt3'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/ui'
|
||||
]
|
||||
})
|
13
examples/with-errors/package.json
Normal file
13
examples/with-errors/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "example-with-errors",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxi build",
|
||||
"dev": "nuxi dev",
|
||||
"start": "nuxi preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/ui": "npm:@nuxt/ui-edge@latest",
|
||||
"nuxt3": "latest"
|
||||
}
|
||||
}
|
0
examples/with-errors/pages/index.vue
Normal file
0
examples/with-errors/pages/index.vue
Normal file
20
examples/with-errors/plugins/error.ts
Normal file
20
examples/with-errors/plugins/error.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('vue:error', (..._args) => {
|
||||
console.log('vue:error')
|
||||
// if (process.client) {
|
||||
// console.log(..._args)
|
||||
// }
|
||||
})
|
||||
nuxtApp.hook('app:error', (..._args) => {
|
||||
console.log('app:error')
|
||||
// if (process.client) {
|
||||
// console.log(..._args)
|
||||
// }
|
||||
})
|
||||
nuxtApp.vueApp.config.errorHandler = (..._args) => {
|
||||
console.log('global error handler')
|
||||
// if (process.client) {
|
||||
// console.log(..._args)
|
||||
// }
|
||||
}
|
||||
})
|
8
examples/with-errors/server/middleware/error.ts
Normal file
8
examples/with-errors/server/middleware/error.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { useQuery, defineMiddleware } from 'h3'
|
||||
|
||||
export default defineMiddleware((req, res, next) => {
|
||||
if ('api' in useQuery(req)) {
|
||||
throw new Error('Server middleware error')
|
||||
}
|
||||
next()
|
||||
})
|
3
examples/with-errors/tsconfig.json
Normal file
3
examples/with-errors/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
@ -4,7 +4,7 @@ import type { Preset } from 'unimport'
|
||||
import autoImports from '../../nuxt3/src/auto-imports/module'
|
||||
import { vuePreset, commonPresets, appPreset } from '../../nuxt3/src/auto-imports/presets'
|
||||
|
||||
const UnsupportedImports = new Set(['useAsyncData', 'useFetch'])
|
||||
const UnsupportedImports = new Set(['useAsyncData', 'useFetch', 'useError', 'throwError', 'clearError'])
|
||||
const CapiHelpers = new Set(Object.keys(CompositionApi))
|
||||
|
||||
export function setupAutoImports () {
|
||||
|
@ -15,9 +15,9 @@
|
||||
"dependencies": {
|
||||
"@cloudflare/kv-asset-handler": "^0.2.0",
|
||||
"@netlify/functions": "^1.0.0",
|
||||
"@nuxt/design": "0.1.5",
|
||||
"@nuxt/devalue": "^2.0.0",
|
||||
"@nuxt/kit": "3.0.0",
|
||||
"@nuxt/ui-templates": "npm:@nuxt/ui-templates-edge@latest",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"@rollup/plugin-inject": "^4.0.4",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { ServerResponse } from 'http'
|
||||
import { createRenderer } from 'vue-bundle-renderer'
|
||||
import devalue from '@nuxt/devalue'
|
||||
import { useQuery } from 'h3'
|
||||
import { privateConfig, publicConfig } from './config'
|
||||
import { buildAssetsURL } from './paths'
|
||||
// @ts-ignore
|
||||
@ -70,7 +71,9 @@ function renderToString (ssrContext) {
|
||||
}
|
||||
|
||||
export async function renderMiddleware (req, res: ServerResponse) {
|
||||
let url = req.url
|
||||
// Whether we're rendering an error page
|
||||
const ssrError = req.url.startsWith('/__error') ? useQuery(req) : null
|
||||
let url = ssrError?.url || req.url
|
||||
|
||||
// payload.json request detection
|
||||
let isPayloadReq = false
|
||||
@ -86,15 +89,23 @@ export async function renderMiddleware (req, res: ServerResponse) {
|
||||
res,
|
||||
runtimeConfig: { private: privateConfig, public: publicConfig },
|
||||
noSSR: req.spa || req.headers['x-nuxt-no-ssr'],
|
||||
...(req.context || {})
|
||||
...(req.context || {}),
|
||||
error: ssrError
|
||||
}
|
||||
|
||||
// Render app
|
||||
const rendered = await renderToString(ssrContext)
|
||||
const rendered = await renderToString(ssrContext).catch((e) => {
|
||||
if (!ssrError) { throw e }
|
||||
})
|
||||
|
||||
// If we error on rendering error page, we bail out and directly return to the error handler
|
||||
if (!rendered) { return }
|
||||
|
||||
// Handle errors
|
||||
if (ssrContext.error) {
|
||||
throw ssrContext.error
|
||||
const error = ssrContext.error /* nuxt 3 */ || ssrContext.nuxt?.error /* nuxt 2 */
|
||||
if (error && !ssrError) {
|
||||
// trigger a re-render by onError handler
|
||||
throw error
|
||||
}
|
||||
|
||||
if (ssrContext.redirected || res.writableEnded) {
|
||||
@ -107,7 +118,6 @@ export async function renderMiddleware (req, res: ServerResponse) {
|
||||
|
||||
// TODO: nuxt3 should not reuse `nuxt` property for different purpose!
|
||||
const payload = ssrContext.payload /* nuxt 3 */ || ssrContext.nuxt /* nuxt 2 */
|
||||
|
||||
if (process.env.NUXT_FULL_STATIC) {
|
||||
payload.staticAssetsBase = STATIC_ASSETS_BASE
|
||||
}
|
||||
@ -121,8 +131,7 @@ export async function renderMiddleware (req, res: ServerResponse) {
|
||||
res.setHeader('Content-Type', 'text/html;charset=UTF-8')
|
||||
}
|
||||
|
||||
const error = ssrContext.nuxt && ssrContext.nuxt.error
|
||||
res.statusCode = error ? error.statusCode : 200
|
||||
res.statusCode = res.statusCode || 200
|
||||
res.end(data, 'utf-8')
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
// import ansiHTML from 'ansi-html'
|
||||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import { error500, error404, errorDev } from '@nuxt/design'
|
||||
import { withQuery } from 'ufo'
|
||||
import { $fetch } from '.'
|
||||
const cwd = process.cwd()
|
||||
|
||||
const hasReqHeader = (req, header, includes) => req.headers[header] && req.headers[header].toLowerCase().includes(includes)
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
export function handleError (error, req: IncomingMessage, res: ServerResponse) {
|
||||
export async function handleError (error, req: IncomingMessage, res: ServerResponse) {
|
||||
const isJsonRequest = hasReqHeader(req, 'accept', 'application/json') || hasReqHeader(req, 'user-agent', 'curl/') || hasReqHeader(req, 'user-agent', 'httpie/')
|
||||
|
||||
const stack = (error.stack || '')
|
||||
@ -31,18 +32,17 @@ export function handleError (error, req: IncomingMessage, res: ServerResponse) {
|
||||
const is404 = error.statusCode === 404
|
||||
|
||||
const errorObject = {
|
||||
url: req.url,
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: is404 ? 'Page Not Found' : 'Internal Server Error',
|
||||
statusMessage: error.statusMessage ?? is404 ? 'Page Not Found' : 'Internal Server Error',
|
||||
message: error.message || error.toString(),
|
||||
description: isDev && !is404
|
||||
? `
|
||||
<h1>${error.message}</h1>
|
||||
<pre>${stack.map(i => `<span class="stack${i.internal ? ' internal' : ''}">${i.text}</span>`).join('\n')}</pre>
|
||||
`
|
||||
? `<pre>${stack.map(i => `<span class="stack${i.internal ? ' internal' : ''}">${i.text}</span>`).join('\n')}</pre>`
|
||||
: ''
|
||||
}
|
||||
|
||||
res.statusCode = error.statusCode || 500
|
||||
res.statusMessage = error.statusMessage || 'Internal Server Error'
|
||||
res.statusCode = errorObject.statusCode
|
||||
res.statusMessage = errorObject.statusMessage
|
||||
|
||||
// Console output
|
||||
if (!is404) {
|
||||
@ -56,8 +56,9 @@ export function handleError (error, req: IncomingMessage, res: ServerResponse) {
|
||||
}
|
||||
|
||||
// HTML response
|
||||
const errorTemplate = is404 ? error404 : (isDev ? errorDev : error500)
|
||||
const html = errorTemplate(errorObject)
|
||||
const url = withQuery('/_nitro/__error', errorObject)
|
||||
const html = await $fetch(url).catch(() => errorObject.statusMessage)
|
||||
|
||||
res.setHeader('Content-Type', 'text/html;charset=UTF-8')
|
||||
res.end(html)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createApp, useBase } from 'h3'
|
||||
import { createApp, lazyHandle, useBase } from 'h3'
|
||||
import { createFetch, Headers } from 'ohmyfetch'
|
||||
import destr from 'destr'
|
||||
import { createCall, createFetch as createLocalFetch } from 'unenv/runtime/fetch/index'
|
||||
@ -13,9 +13,12 @@ const app = createApp({
|
||||
onError: handleError
|
||||
})
|
||||
|
||||
const renderMiddleware = lazyHandle(() => import('../app/render').then(e => e.renderMiddleware))
|
||||
|
||||
app.use('/_nitro', renderMiddleware)
|
||||
app.use(timingMiddleware)
|
||||
app.use(serverMiddleware)
|
||||
app.use(() => import('../app/render').then(e => e.renderMiddleware), { lazy: true })
|
||||
app.use(renderMiddleware)
|
||||
|
||||
export const stack = app.stack
|
||||
export const handle = useBase(baseURL(), app)
|
||||
|
@ -2,7 +2,7 @@ import { Worker } from 'worker_threads'
|
||||
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { existsSync, promises as fsp } from 'fs'
|
||||
import { loading as loadingTemplate } from '@nuxt/design'
|
||||
import { loading as loadingTemplate } from '@nuxt/ui-templates'
|
||||
import chokidar, { FSWatcher } from 'chokidar'
|
||||
import debounce from 'p-debounce'
|
||||
import { promisifyHandle, createApp, Middleware, useBase } from 'h3'
|
||||
|
@ -17,9 +17,9 @@
|
||||
"prepack": "unbuild"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/design": "0.1.5",
|
||||
"@nuxt/kit": "3.0.0",
|
||||
"@nuxt/schema": "3.0.0",
|
||||
"@nuxt/ui-templates": "npm:@nuxt/ui-templates-edge@latest",
|
||||
"@types/clear": "^0",
|
||||
"@types/mri": "^1.1.1",
|
||||
"@types/rimraf": "^3",
|
||||
|
@ -86,7 +86,7 @@ export default defineNuxtCommand({
|
||||
dLoad(true, `Directory \`${dir}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
|
||||
}
|
||||
} else if (isFileChange) {
|
||||
if (file.match(/app\.(js|ts|mjs|jsx|tsx|vue)$/)) {
|
||||
if (file.match(/(app|error)\.(js|ts|mjs|jsx|tsx|vue)$/)) {
|
||||
dLoad(true, `\`${relative(rootDir, file)}\` ${event === 'add' ? 'created' : 'removed'}`)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { RequestListener } from 'http'
|
||||
import type { ListenOptions } from 'listhen'
|
||||
import { loading } from '@nuxt/design'
|
||||
import { loading } from '@nuxt/ui-templates'
|
||||
|
||||
export function createServer (defaultApp?) {
|
||||
const listener = createDynamicFunction(defaultApp || createLoadingHandler('Loading...'))
|
||||
|
@ -30,10 +30,10 @@
|
||||
"prepack": "unbuild"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/design": "^0.1.5",
|
||||
"@nuxt/kit": "3.0.0",
|
||||
"@nuxt/nitro": "3.0.0",
|
||||
"@nuxt/schema": "3.0.0",
|
||||
"@nuxt/ui-templates": "npm:@nuxt/ui-templates-edge@latest",
|
||||
"@nuxt/vite-builder": "3.0.0",
|
||||
"@vue/reactivity": "^3.2.31",
|
||||
"@vue/shared": "^3.2.31",
|
||||
|
42
packages/nuxt3/src/app/components/nuxt-error-page.vue
Normal file
42
packages/nuxt3/src/app/components/nuxt-error-page.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<ErrorTemplate v-bind="{ statusCode, statusMessage, description, stack }" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Error404 from '@nuxt/ui-templates/templates/error-404.vue'
|
||||
import Error500 from '@nuxt/ui-templates/templates/error-500.vue'
|
||||
import ErrorDev from '@nuxt/ui-templates/templates/error-dev.vue'
|
||||
|
||||
const props = defineProps({
|
||||
error: Object
|
||||
})
|
||||
|
||||
const error = props.error
|
||||
|
||||
// TODO: extract to a separate utility
|
||||
const stacktrace = (error.stack || '')
|
||||
.split('\n')
|
||||
.splice(1)
|
||||
.map((line) => {
|
||||
const text = line
|
||||
.replace('webpack:/', '')
|
||||
.replace('.vue', '.js') // TODO: Support sourcemap
|
||||
.trim()
|
||||
return {
|
||||
text,
|
||||
internal: (line.includes('node_modules') && !line.includes('.cache')) ||
|
||||
line.includes('internal') ||
|
||||
line.includes('new Promise')
|
||||
}
|
||||
}).map(i => `<span class="stack${i.internal ? ' internal' : ''}">${i.text}</span>`).join('\n')
|
||||
|
||||
// Error page props
|
||||
const statusCode = String(error.statusCode || 500)
|
||||
const is404 = statusCode === '404'
|
||||
|
||||
const statusMessage = error.statusMessage ?? is404 ? 'Page Not Found' : 'Internal Server Error'
|
||||
const description = error.message || error.toString()
|
||||
const stack = process.dev && !is404 ? error.description || `<pre>${stacktrace}</pre>` : undefined
|
||||
|
||||
const ErrorTemplate = is404 ? Error404 : process.dev ? ErrorDev : Error500
|
||||
</script>
|
@ -1,22 +1,31 @@
|
||||
<template>
|
||||
<Suspense @resolve="onResolve">
|
||||
<App />
|
||||
<ErrorComponent v-if="error" :error="error" />
|
||||
<App v-else />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useNuxtApp } from '#app'
|
||||
<script setup lang="ts">
|
||||
import { onErrorCaptured } from 'vue'
|
||||
import { callWithNuxt, throwError, useError, useNuxtApp } from '#app'
|
||||
// @ts-ignore
|
||||
import ErrorComponent from '#build/error-component.mjs'
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const results = nuxtApp.hooks.callHookWith(hooks => hooks.map(hook => hook()), 'vue:setup')
|
||||
if (process.dev && results && results.some(i => i && 'then' in i)) {
|
||||
console.error('[nuxt] Error in `vue:setup`. Callbacks must be synchronous.')
|
||||
}
|
||||
return {
|
||||
onResolve: () => nuxtApp.callHook('app:suspense:resolve')
|
||||
}
|
||||
}
|
||||
const nuxtApp = useNuxtApp()
|
||||
const onResolve = () => nuxtApp.callHook('app:suspense:resolve')
|
||||
|
||||
// vue:setup hook
|
||||
const results = nuxtApp.hooks.callHookWith(hooks => hooks.map(hook => hook()), 'vue:setup')
|
||||
if (process.dev && results && results.some(i => i && 'then' in i)) {
|
||||
console.error('[nuxt] Error in `vue:setup`. Callbacks must be synchronous.')
|
||||
}
|
||||
|
||||
// error handling
|
||||
const error = useError()
|
||||
onErrorCaptured((err, target, info) => {
|
||||
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
|
||||
if (process.server) {
|
||||
callWithNuxt(nuxtApp, throwError, [err])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="html" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { welcome as welcomeTemplate } from '@nuxt/design'
|
||||
export default ({
|
||||
computed: {
|
||||
html: () => welcomeTemplate({})
|
||||
}
|
||||
})
|
||||
</script>
|
30
packages/nuxt3/src/app/composables/error.ts
Normal file
30
packages/nuxt3/src/app/composables/error.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useNuxtApp, useState } from '#app'
|
||||
|
||||
export const useError = () => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
return useState('error', () => process.server ? nuxtApp.ssrContext.error : nuxtApp.payload.error)
|
||||
}
|
||||
|
||||
export const throwError = (_err: string | Error) => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const error = useError()
|
||||
const err = typeof _err === 'string' ? new Error(_err) : _err
|
||||
nuxtApp.callHook('app:error', err)
|
||||
if (process.server) {
|
||||
nuxtApp.ssrContext.error = nuxtApp.ssrContext.error || err
|
||||
} else {
|
||||
error.value = error.value || err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
export const clearError = async (options: { redirect?: string } = {}) => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const error = useError()
|
||||
nuxtApp.callHook('app:error:cleared', options)
|
||||
if (options.redirect) {
|
||||
await nuxtApp.$router.replace(options.redirect)
|
||||
}
|
||||
error.value = null
|
||||
}
|
@ -3,6 +3,7 @@ export { useAsyncData, useLazyAsyncData } from './asyncData'
|
||||
export type { AsyncDataOptions, AsyncData } from './asyncData'
|
||||
export { useHydration } from './hydrate'
|
||||
export { useState } from './state'
|
||||
export { clearError, throwError, useError } from './error'
|
||||
export { useFetch, useLazyFetch } from './fetch'
|
||||
export type { FetchResult, UseFetchOptions } from './fetch'
|
||||
export { useCookie } from './cookie'
|
||||
|
@ -19,9 +19,13 @@ if (process.server) {
|
||||
|
||||
const nuxt = createNuxtApp({ vueApp, ssrContext })
|
||||
|
||||
await applyPlugins(nuxt, plugins)
|
||||
|
||||
await nuxt.hooks.callHook('app:created', vueApp)
|
||||
try {
|
||||
await applyPlugins(nuxt, plugins)
|
||||
await nuxt.hooks.callHook('app:created', vueApp)
|
||||
} catch (err) {
|
||||
await nuxt.callHook('app:error', err)
|
||||
ssrContext.error = ssrContext.error || err
|
||||
}
|
||||
|
||||
return vueApp
|
||||
}
|
||||
@ -43,19 +47,27 @@ if (process.client) {
|
||||
|
||||
const nuxt = createNuxtApp({ vueApp })
|
||||
|
||||
await applyPlugins(nuxt, plugins)
|
||||
|
||||
await nuxt.hooks.callHook('app:created', vueApp)
|
||||
await nuxt.hooks.callHook('app:beforeMount', vueApp)
|
||||
|
||||
nuxt.hooks.hookOnce('app:suspense:resolve', () => {
|
||||
nuxt.isHydrating = false
|
||||
})
|
||||
|
||||
vueApp.mount('#__nuxt')
|
||||
try {
|
||||
await applyPlugins(nuxt, plugins)
|
||||
} catch (err) {
|
||||
await nuxt.callHook('app:error', err)
|
||||
nuxt.payload.error = nuxt.payload.error || err
|
||||
}
|
||||
|
||||
await nuxt.hooks.callHook('app:mounted', vueApp)
|
||||
await nextTick()
|
||||
try {
|
||||
await nuxt.hooks.callHook('app:created', vueApp)
|
||||
await nuxt.hooks.callHook('app:beforeMount', vueApp)
|
||||
vueApp.mount('#__nuxt')
|
||||
await nuxt.hooks.callHook('app:mounted', vueApp)
|
||||
await nextTick()
|
||||
} catch (err) {
|
||||
await nuxt.callHook('app:error', err)
|
||||
nuxt.payload.error = nuxt.payload.error || err
|
||||
}
|
||||
}
|
||||
|
||||
entry().catch((error) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { getCurrentInstance, reactive } from 'vue'
|
||||
import type { App, VNode } from 'vue'
|
||||
import type { App, onErrorCaptured, VNode } from 'vue'
|
||||
import { createHooks, Hookable } from 'hookable'
|
||||
import type { RuntimeConfig } from '@nuxt/schema'
|
||||
import { legacyPlugin, LegacyContext } from './compat/legacy-app'
|
||||
@ -21,10 +21,13 @@ export interface RuntimeNuxtHooks {
|
||||
'app:mounted': (app: App<Element>) => HookResult
|
||||
'app:rendered': () => HookResult
|
||||
'app:suspense:resolve': (Component?: VNode) => HookResult
|
||||
'app:error': (err: any) => HookResult
|
||||
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
||||
'page:start': (Component?: VNode) => HookResult
|
||||
'page:finish': (Component?: VNode) => HookResult
|
||||
'meta:register': (metaRenderers: Array<(nuxt: NuxtApp) => NuxtMeta | Promise<NuxtMeta>>) => HookResult
|
||||
'vue:setup': () => void
|
||||
'vue:error': (...args: Parameters<Parameters<typeof onErrorCaptured>[0]>) => HookResult
|
||||
}
|
||||
|
||||
interface _NuxtApp {
|
||||
|
@ -4,6 +4,7 @@ import { NuxtApp } from '@nuxt/schema'
|
||||
import { createError } from 'h3'
|
||||
import { defineNuxtPlugin } from '..'
|
||||
import { callWithNuxt } from '../nuxt'
|
||||
import { clearError, throwError } from '#app'
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
@ -106,6 +107,12 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
|
||||
try {
|
||||
// Resolve route
|
||||
const to = getRouteFromPath(url)
|
||||
|
||||
if (process.client && !nuxtApp.isHydrating) {
|
||||
// Clear any existing errors
|
||||
await callWithNuxt(nuxtApp as NuxtApp, clearError)
|
||||
}
|
||||
|
||||
// Run beforeEach hooks
|
||||
for (const middleware of hooks['navigate:before']) {
|
||||
const result = await middleware(to, route)
|
||||
@ -196,8 +203,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
|
||||
const error = result || createError({
|
||||
statusMessage: `Route navigation aborted: ${nuxtApp.ssrContext.url}`
|
||||
})
|
||||
nuxtApp.ssrContext.error = error
|
||||
throw error
|
||||
return callWithNuxt(nuxtApp, throwError, [error])
|
||||
}
|
||||
}
|
||||
if (result || result === false) { return result }
|
||||
|
@ -37,7 +37,10 @@ export const appPreset = defineUnimportPreset({
|
||||
'defineNuxtRouteMiddleware',
|
||||
'navigateTo',
|
||||
'abortNavigation',
|
||||
'addRouteMiddleware'
|
||||
'addRouteMiddleware',
|
||||
'throwError',
|
||||
'clearError',
|
||||
'useError'
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { promises as fsp } from 'fs'
|
||||
import { dirname, resolve } from 'pathe'
|
||||
import defu from 'defu'
|
||||
import type { Nuxt, NuxtApp, NuxtPlugin } from '@nuxt/schema'
|
||||
import { findPath, resolveFiles, normalizePlugin, normalizeTemplate, compileTemplate, templateUtils } from '@nuxt/kit'
|
||||
import { findPath, resolveFiles, normalizePlugin, normalizeTemplate, compileTemplate, templateUtils, tryResolveModule } from '@nuxt/kit'
|
||||
|
||||
import * as defaultTemplates from './templates'
|
||||
|
||||
@ -59,12 +59,17 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
||||
app.mainComponent = await findPath(['~/App', '~/app'])
|
||||
}
|
||||
if (!app.mainComponent) {
|
||||
app.mainComponent = resolve(nuxt.options.appDir, 'components/nuxt-welcome.vue')
|
||||
app.mainComponent = tryResolveModule('@nuxt/ui-templates/templates/welcome.vue')
|
||||
}
|
||||
|
||||
// Default root component
|
||||
app.rootComponent = resolve(nuxt.options.appDir, 'components/nuxt-root.vue')
|
||||
|
||||
// Resolve error component
|
||||
if (!app.errorComponent) {
|
||||
app.errorComponent = (await findPath(['~/error'])) || resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
|
||||
}
|
||||
|
||||
// Resolve plugins
|
||||
for (const config of [...nuxt.options._extends.map(layer => layer.config), nuxt.options]) {
|
||||
app.plugins.push(...[
|
||||
|
@ -10,10 +10,13 @@ export async function build (nuxt: Nuxt) {
|
||||
if (nuxt.options.dev) {
|
||||
watch(nuxt)
|
||||
nuxt.hook('builder:watch', async (event, path) => {
|
||||
if (event !== 'change' && /app|plugins/i.test(path)) {
|
||||
if (event !== 'change' && /app|error|plugins/i.test(path)) {
|
||||
if (path.match(/app/i)) {
|
||||
app.mainComponent = null
|
||||
}
|
||||
if (path.match(/error/i)) {
|
||||
app.errorComponent = null
|
||||
}
|
||||
await generateApp(nuxt, app)
|
||||
}
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resolve } from 'pathe'
|
||||
import { createHooks } from 'hookable'
|
||||
import type { Nuxt, NuxtOptions, NuxtConfig, ModuleContainer, NuxtHooks } from '@nuxt/schema'
|
||||
import { loadNuxtConfig, LoadNuxtOptions, nuxtCtx, installModule, addComponent, addVitePlugin, addWebpackPlugin } from '@nuxt/kit'
|
||||
import { loadNuxtConfig, LoadNuxtOptions, nuxtCtx, installModule, addComponent, addVitePlugin, addWebpackPlugin, tryResolveModule } from '@nuxt/kit'
|
||||
// Temporary until finding better placement
|
||||
/* eslint-disable import/no-restricted-paths */
|
||||
import pagesModule from '../pages/module'
|
||||
@ -76,7 +76,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
// Add <NuxtWelcome>
|
||||
addComponent({
|
||||
name: 'NuxtWelcome',
|
||||
filePath: resolve(nuxt.options.appDir, 'components/nuxt-welcome.vue')
|
||||
filePath: tryResolveModule('@nuxt/ui-templates/templates/welcome.vue')
|
||||
})
|
||||
|
||||
// Add <ClientOnly>
|
||||
@ -108,6 +108,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
||||
options._majorVersion = 3
|
||||
options._modules.push(pagesModule, metaModule, componentsModule, autoImportsModule)
|
||||
options.modulesDir.push(resolve(pkgDir, 'node_modules'))
|
||||
options.build.transpile.push('@nuxt/ui-templates')
|
||||
options.alias['vue-demi'] = resolve(options.appDir, 'compat/vue-demi')
|
||||
options.alias['@vue/composition-api'] = resolve(options.appDir, 'compat/capi')
|
||||
|
||||
|
@ -33,6 +33,11 @@ export const rootComponentTemplate = {
|
||||
filename: 'root-component.mjs',
|
||||
getContents: (ctx: TemplateContext) => genExport(ctx.app.rootComponent, ['default'])
|
||||
}
|
||||
// TODO: Use an alias
|
||||
export const errorComponentTemplate = {
|
||||
filename: 'error-component.mjs',
|
||||
getContents: (ctx: TemplateContext) => genExport(ctx.app.errorComponent, ['default'])
|
||||
}
|
||||
|
||||
export const cssTemplate = {
|
||||
filename: 'css.mjs',
|
||||
|
@ -42,7 +42,7 @@ export default defineNuxtModule({
|
||||
|
||||
nuxt.hook('app:resolve', (app) => {
|
||||
// Add default layout for pages
|
||||
if (app.mainComponent.includes('nuxt-welcome')) {
|
||||
if (app.mainComponent.includes('@nuxt/ui-templates')) {
|
||||
app.mainComponent = resolve(runtimeDir, 'app.vue')
|
||||
}
|
||||
})
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import { createError } from 'h3'
|
||||
import NuxtPage from './page'
|
||||
import NuxtLayout from './layout'
|
||||
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, NuxtApp } from '#app'
|
||||
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, NuxtApp, throwError, clearError } from '#app'
|
||||
// @ts-ignore
|
||||
import routes from '#build/routes'
|
||||
// @ts-ignore
|
||||
@ -86,6 +86,11 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (process.client && !nuxtApp.isHydrating) {
|
||||
// Clear any existing errors
|
||||
await callWithNuxt(nuxtApp as NuxtApp, clearError)
|
||||
}
|
||||
|
||||
for (const entry of middlewareEntries) {
|
||||
const middleware = typeof entry === 'string' ? nuxtApp._middleware.named[entry] || await namedMiddleware[entry]?.().then(r => r.default || r) : entry
|
||||
|
||||
@ -96,9 +101,10 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
const result = await callWithNuxt(nuxtApp as NuxtApp, middleware, [to, from])
|
||||
if (process.server) {
|
||||
if (result === false || result instanceof Error) {
|
||||
return result || createError({
|
||||
const error = result || createError({
|
||||
statusMessage: `Route navigation aborted: ${nuxtApp.ssrContext.url}`
|
||||
})
|
||||
return callWithNuxt(nuxtApp, throwError, [error])
|
||||
}
|
||||
}
|
||||
if (result || result === false) { return result }
|
||||
@ -110,6 +116,15 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
})
|
||||
|
||||
nuxtApp.hook('app:created', async () => {
|
||||
router.afterEach((to) => {
|
||||
if (to.matched.length === 0) {
|
||||
callWithNuxt(nuxtApp, throwError, [createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `Page not found: ${to.fullPath}`
|
||||
})])
|
||||
}
|
||||
})
|
||||
|
||||
if (process.server) {
|
||||
router.push(nuxtApp.ssrContext.url)
|
||||
|
||||
@ -124,16 +139,8 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
|
||||
try {
|
||||
await router.isReady()
|
||||
|
||||
const is404 = router.currentRoute.value.matched.length === 0
|
||||
if (process.server && is404) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `Page not found: ${nuxtApp.ssrContext.url}`
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
nuxtApp.ssrContext.error = error
|
||||
callWithNuxt(nuxtApp, throwError, [error])
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -53,6 +53,7 @@ export interface NuxtPlugin {
|
||||
export interface NuxtApp {
|
||||
mainComponent?: string
|
||||
rootComponent?: string
|
||||
errorComponent?: string
|
||||
dir: string
|
||||
extensions: string[]
|
||||
plugins: NuxtPlugin[]
|
||||
|
29
yarn.lock
29
yarn.lock
@ -2892,13 +2892,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nuxt/design@npm:0.1.5, @nuxt/design@npm:^0.1.5":
|
||||
version: 0.1.5
|
||||
resolution: "@nuxt/design@npm:0.1.5"
|
||||
checksum: 1e75ba0c7e9519754d185133e1bc97d2056201b2c6ad99a9196ffb94e787a4ee9fa2a64b1dd868febece5b3370c8a50388033b65455edc939fe67bcc1f4bbe8b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nuxt/devalue@npm:^1.2.5":
|
||||
version: 1.2.5
|
||||
resolution: "@nuxt/devalue@npm:1.2.5"
|
||||
@ -3035,10 +3028,10 @@ __metadata:
|
||||
dependencies:
|
||||
"@cloudflare/kv-asset-handler": ^0.2.0
|
||||
"@netlify/functions": ^1.0.0
|
||||
"@nuxt/design": 0.1.5
|
||||
"@nuxt/devalue": ^2.0.0
|
||||
"@nuxt/kit": 3.0.0
|
||||
"@nuxt/schema": 3.0.0
|
||||
"@nuxt/ui-templates": "npm:@nuxt/ui-templates-edge@latest"
|
||||
"@rollup/plugin-alias": ^3.1.9
|
||||
"@rollup/plugin-commonjs": ^21.0.2
|
||||
"@rollup/plugin-inject": ^4.0.4
|
||||
@ -3294,6 +3287,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nuxt/ui-templates@npm:@nuxt/ui-templates-edge@latest":
|
||||
version: 0.0.0-27449202.0cb54cc
|
||||
resolution: "@nuxt/ui-templates-edge@npm:0.0.0-27449202.0cb54cc"
|
||||
checksum: bbc709d35bf1c586bc604835caf8b6000e4cab22ad56d71d231a7d4e987ab8dd33a9adbba11af684f70826007ad3094480f38524160bf7c6f1a9dc1fcb375f79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nuxt/ui@npm:@nuxt/ui-edge@latest":
|
||||
version: 0.0.0-27376194.a859489
|
||||
resolution: "@nuxt/ui-edge@npm:0.0.0-27376194.a859489"
|
||||
@ -10570,6 +10570,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"example-with-errors@workspace:examples/with-errors":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "example-with-errors@workspace:examples/with-errors"
|
||||
dependencies:
|
||||
"@nuxt/ui": "npm:@nuxt/ui-edge@latest"
|
||||
nuxt3: latest
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"example-with-layouts@workspace:examples/with-layouts":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "example-with-layouts@workspace:examples/with-layouts"
|
||||
@ -15369,9 +15378,9 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "nuxi@workspace:packages/nuxi"
|
||||
dependencies:
|
||||
"@nuxt/design": 0.1.5
|
||||
"@nuxt/kit": 3.0.0
|
||||
"@nuxt/schema": 3.0.0
|
||||
"@nuxt/ui-templates": "npm:@nuxt/ui-templates-edge@latest"
|
||||
"@types/clear": ^0
|
||||
"@types/mri": ^1.1.1
|
||||
"@types/rimraf": ^3
|
||||
@ -15476,10 +15485,10 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "nuxt3@workspace:packages/nuxt3"
|
||||
dependencies:
|
||||
"@nuxt/design": ^0.1.5
|
||||
"@nuxt/kit": 3.0.0
|
||||
"@nuxt/nitro": 3.0.0
|
||||
"@nuxt/schema": 3.0.0
|
||||
"@nuxt/ui-templates": "npm:@nuxt/ui-templates-edge@latest"
|
||||
"@nuxt/vite-builder": 3.0.0
|
||||
"@types/fs-extra": ^9.0.13
|
||||
"@types/hash-sum": ^1.0.0
|
||||
|
Loading…
Reference in New Issue
Block a user