Merge branch 'main' into main

This commit is contained in:
David Nahodyl 2024-06-05 14:49:34 -04:00 committed by GitHub
commit b30c465a57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1296 additions and 758 deletions

View File

@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Lychee link checker
uses: lycheeverse/lychee-action@054a8e8c7a88ada133165c6633a49825a32174e2 # for v1.8.0
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0
with:
# arguments with file types to check
args: >-

View File

@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: refs/pull/${{ github.event.issue.number }}/merge
ref: ${{ github.event.issue.pull_request.head.sha }}
fetch-depth: 0
- run: corepack enable

View File

@ -373,7 +373,7 @@ registerEndpoint('/test/', {
})
```
> **Note**: If your requests in a component go to external API, you can use `baseURL` and then make it empty using Nuxt Environment Config (`$test`) so all your requests will go to Nitro server.
> **Note**: If your requests in a component go to an external API, you can use `baseURL` and then make it empty using [Nuxt Environment Override Config](/docs/getting-started/configuration#environment-overrides) (`$test`) so all your requests will go to Nitro server.
#### Conflict with End-To-End Testing

View File

@ -48,6 +48,7 @@ export default defineNuxtConfig({
// experimental: {
// sharedPrerenderData: false,
// compileTemplate: true,
// resetAsyncDataToUndefined: true,
// templateUtils: true,
// relativeWatchPaths: true,
// defaults: {
@ -189,6 +190,63 @@ export default defineNuxtConfig({
})
```
#### Default `data` and `error` values in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal
##### What Changed
`data` and `error` objects returned from `useAsyncData` will now default to `undefined`.
##### Reasons for Change
Previously `data` was initialized to `null` but reset in `clearNuxtData` to `undefined`. `error` was initialized to `null`. This change is to bring greater consistency.
##### Migration Steps
If you encounter any issues you can revert back to the previous behavior with:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
defaults: {
useAsyncData: {
value: 'null',
errorValue: 'null'
}
}
}
})
```
Please report an issue if you are doing this, as we do not plan to keep this as configurable.
#### Respect defaults when clearing `data` in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal
##### What Changed
If you provide a custom `default` value for `useAsyncData`, this will now be used when calling `clear` or `clearNuxtData` and it will be reset to its default value rather than simply unset.
##### Reasons for Change
Often users set an appropriately empty value, such as an empty array, to avoid the need to check for `null`/`undefined` when iterating over it. This should be respected when resetting/clearing the data.
##### Migration Steps
If you encounter any issues you can revert back to the previous behavior, for now, with:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
resetAsyncDataToUndefined: true,
}
})
```
Please report an issue if you are doing so, as we do not plan to keep this as configurable.
#### Shallow Data Reactivity in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal

View File

@ -259,7 +259,7 @@ Watch Learn Vue video about Nuxt Server Components.
::
::tip{icon="i-ph-article-duotone" to="https://roe.dev/blog/nuxt-server-components" target="_blank"}
Read Daniel Roe's guide to Nuxt server components
Read Daniel Roe's guide to Nuxt Server Components.
::
### Standalone server components

View File

@ -59,13 +59,10 @@ Use these options to set the expiration of the cookie.
The given number will be converted to an integer by rounding down. By default, no maximum age is set.
`expires`: Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1).
By default, no expiration is set. Most clients will consider this a "non-persistent cookie" and
will delete it on a condition like exiting a web browser application.
By default, no expiration is set. Most clients will consider this a "non-persistent cookie" and will delete it on a condition like exiting a web browser application.
::note
The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
`maxAge` is set, then `maxAge` takes precedence, but not all clients may obey this,
so if both are set, they should point to the same date and time!
The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` is set, then `maxAge` takes precedence, but not all clients may obey this, so if both are set, they should point to the same date and time!
::
::note
@ -74,22 +71,29 @@ If neither of `expires` and `maxAge` is set, the cookie will be session-only and
### `httpOnly`
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy,
the `HttpOnly` attribute is set; otherwise it is not. By default, the `HttpOnly` attribute is not set.
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy, the `HttpOnly` attribute is set; otherwise it is not. By default, the `HttpOnly` attribute is not set.
::warning
Be careful when setting this to `true`, as compliant clients will not allow client-side
JavaScript to see the cookie in `document.cookie`.
Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
::
### `secure`
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy,
the `Secure` attribute is set; otherwise it is not. By default, the `Secure` attribute is not set.
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy, the `Secure` attribute is set; otherwise it is not. By default, the `Secure` attribute is not set.
::warning
Be careful when setting this to `true`, as compliant clients will not send the cookie back to
the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
Be careful when setting this to `true`, as compliant clients will not send the cookie back to the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
::
### `partitioned`
Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1) attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the `Partitioned` attribute is not set.
::note
This is an attribute that has not yet been fully standardized, and may change in the future.
This also means many clients may ignore this attribute until they understand it.
More information can be found in the [proposal](https://github.com/privacycg/CHIPS).
::
### `domain`
@ -114,23 +118,18 @@ More information about the different enforcement levels can be found in [the spe
### `encode`
Specifies a function that will be used to encode a cookie's value. Since the value of a cookie
has a limited character set (and must be a simple string), this function can be used to encode
a value into a string suited for a cookie's value.
Specifies a function that will be used to encode a cookie's value. Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to encode a value into a string suited for a cookie's value.
The default encoder is the `JSON.stringify` + `encodeURIComponent`.
### `decode`
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
has a limited character set (and must be a simple string), this function can be used to decode
a previously encoded cookie value into a JavaScript string or other object.
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to decode a previously encoded cookie value into a JavaScript string or other object.
The default decoder is `decodeURIComponent` + [destr](https://github.com/unjs/destr).
::note
If an error is thrown from this function, the original, non-decoded cookie value will
be returned as the cookie's value.
If an error is thrown from this function, the original, non-decoded cookie value will be returned as the cookie's value.
::
### `default`

View File

@ -32,6 +32,10 @@ We'll do our best to follow our [internal issue decision making flowchart](https
### Send a Pull Request
::Tip
On windows, you need to clone the repository with `git clone -c core.symlinks=true https://github.com/nuxt/nuxt.git` to make symlinks work.
::
We always welcome pull requests! ❤️
#### Before You Start

View File

@ -64,9 +64,9 @@ Each active version has its own nightly releases which are generated automatical
Release | | Initial release | End Of Life | Docs
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
**4.x** (scheduled) | | 2024 Q2 | |  
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label="></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
**2.x** (maintenance) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label="></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label="></a> | 2018-01-08 | 2019-09-21 | &nbsp;
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label=" class="not-prose"></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
**2.x** (maintenance) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label=" class="not-prose"></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label=" class="not-prose"></a> | 2018-01-08 | 2019-09-21 | &nbsp;
### Support Status

View File

@ -20,6 +20,7 @@
"lint:knip": "pnpx knip",
"play": "nuxi dev playground",
"play:build": "nuxi build playground",
"play:generate": "nuxi generate playground",
"play:preview": "nuxi preview playground",
"test": "pnpm test:fixtures && pnpm test:fixtures:dev && pnpm test:fixtures:webpack && pnpm test:unit && pnpm test:runtime && pnpm test:types && pnpm typecheck",
"test:prepare": "jiti ./test/prepare.ts",
@ -40,8 +41,8 @@
"@nuxt/webpack-builder": "workspace:*",
"magic-string": "^0.30.10",
"nuxt": "workspace:*",
"rollup": "^4.17.2",
"vite": "5.2.11",
"rollup": "^4.18.0",
"vite": "5.2.12",
"vue": "3.4.27"
},
"devDependencies": {
@ -53,7 +54,7 @@
"@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3",
"@types/fs-extra": "11.0.4",
"@types/node": "20.12.12",
"@types/node": "20.12.13",
"@types/semver": "7.5.8",
"@vitest/coverage-v8": "1.6.0",
"@vue/test-utils": "2.4.6",
@ -69,16 +70,16 @@
"fs-extra": "11.2.0",
"globby": "14.0.1",
"h3": "1.11.1",
"happy-dom": "14.11.0",
"happy-dom": "14.12.0",
"jiti": "1.21.0",
"markdownlint-cli": "0.40.0",
"markdownlint-cli": "0.41.0",
"nitropack": "2.9.6",
"nuxi": "3.11.1",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.0.10",
"ofetch": "1.3.4",
"pathe": "1.1.2",
"playwright-core": "1.44.0",
"playwright-core": "1.44.1",
"rimraf": "5.0.7",
"semver": "7.6.2",
"std-env": "3.7.0",
@ -90,7 +91,7 @@
"vue-router": "4.3.2",
"vue-tsc": "2.0.19"
},
"packageManager": "pnpm@9.1.1",
"packageManager": "pnpm@9.1.3",
"engines": {
"node": "^16.10.0 || >=18.0.0"
},

View File

@ -44,7 +44,7 @@
"semver": "^7.6.2",
"ufo": "^1.5.3",
"unctx": "^2.3.1",
"unimport": "^3.7.1",
"unimport": "^3.7.2",
"untyped": "^1.4.2"
},
"devDependencies": {
@ -54,7 +54,7 @@
"lodash-es": "4.17.21",
"nitropack": "2.9.6",
"unbuild": "latest",
"vite": "5.2.11",
"vite": "5.2.12",
"vitest": "1.6.0",
"webpack": "5.91.0"
},

View File

@ -73,8 +73,8 @@ export function defineNuxtModule<OptionsT extends ModuleOptions> (definition: Mo
const key = `nuxt:module:${uniqueKey || (Math.round(Math.random() * 10000))}`
const mark = performance.mark(key)
const res = await module.setup?.call(null as any, _options, nuxt) ?? {}
const perf = performance.measure(key, mark?.name) // TODO: remove when Node 14 reaches EOL
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
const perf = performance.measure(key, mark.name)
const setupTime = Math.round((perf.duration * 100)) / 100
// Measure setup time
if (setupTime > 5000 && uniqueKey !== '@nuxt/telemetry') {

6
packages/nuxt/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
src/app/components/error-404.vue
src/app/components/error-500.vue
src/app/components/error-dev.vue
src/app/components/welcome.vue
src/core/runtime/nitro/error-500.ts
src/core/runtime/nitro/error-dev.ts

View File

@ -60,14 +60,14 @@
},
"dependencies": {
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.3.1",
"@nuxt/devtools": "^1.3.2",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.4",
"@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.10",
"@unhead/ssr": "^1.9.10",
"@unhead/vue": "^1.9.10",
"@unhead/dom": "^1.9.11",
"@unhead/ssr": "^1.9.11",
"@unhead/vue": "^1.9.11",
"@vue/shared": "^3.4.27",
"acorn": "8.11.3",
"c12": "^1.10.0",
@ -76,7 +76,7 @@
"defu": "^6.1.4",
"destr": "^2.0.3",
"devalue": "^5.0.0",
"esbuild": "^0.21.3",
"esbuild": "^0.21.4",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"fs-extra": "^11.2.0",
@ -107,7 +107,7 @@
"uncrypto": "^0.1.3",
"unctx": "^2.3.1",
"unenv": "^1.9.0",
"unimport": "^3.7.1",
"unimport": "^3.7.2",
"unplugin": "^1.10.1",
"unplugin-vue-router": "^0.7.0",
"unstorage": "^1.10.2",
@ -118,13 +118,13 @@
"vue-router": "^4.3.2"
},
"devDependencies": {
"@nuxt/ui-templates": "1.3.3",
"@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5",
"@types/fs-extra": "11.0.4",
"@vitejs/plugin-vue": "5.0.4",
"unbuild": "latest",
"vite": "5.2.11",
"vite": "5.2.12",
"vitest": "1.6.0"
},
"peerDependencies": {

View File

@ -1 +0,0 @@
../../../../ui-templates/dist/templates/error-404.vue

View File

@ -1 +0,0 @@
../../../../ui-templates/dist/templates/error-500.vue

View File

@ -1 +0,0 @@
../../../../ui-templates/dist/templates/error-dev.vue

View File

@ -1,7 +1,8 @@
<template>
<Suspense @resolve="onResolve">
<div v-if="abortRender" />
<ErrorComponent
v-if="error"
v-else-if="error"
:error="error"
/>
<IslandRenderer
@ -53,6 +54,8 @@ if (import.meta.dev && results && results.some(i => i && 'then' in i)) {
// error handling
const error = useError()
// render an empty <div> when plugins have thrown an error but we're not yet rendering the error page
const abortRender = import.meta.server && error.value && !nuxtApp.ssrContext.error
onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {

View File

@ -1 +0,0 @@
../../../../ui-templates/dist/templates/welcome.vue

View File

@ -8,7 +8,10 @@ import { createError } from './error'
import { onNuxtReady } from './ready'
// @ts-expect-error virtual file
import { asyncDataDefaults } from '#build/nuxt.config.mjs'
import { asyncDataDefaults, resetAsyncDataToUndefined } from '#build/nuxt.config.mjs'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
@ -42,7 +45,7 @@ export interface AsyncDataOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> {
/**
* Whether to fetch on the server side.
@ -117,7 +120,7 @@ export interface _AsyncData<DataT, ErrorT> {
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
clear: () => void
error: Ref<ErrorT | null>
error: Ref<ErrorT | DefaultAsyncDataErrorValue>
status: Ref<AsyncDataRequestStatus>
}
@ -138,11 +141,11 @@ export function useAsyncData<
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
/**
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -158,7 +161,7 @@ export function useAsyncData<
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
/**
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -171,12 +174,12 @@ export function useAsyncData<
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
/**
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -194,14 +197,14 @@ export function useAsyncData<
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
export function useAsyncData<
ResT,
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> {
DefaultT = DefaultAsyncDataValue,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue> {
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
@ -226,14 +229,14 @@ export function useAsyncData<
const value = nuxtApp.ssrContext!._sharedPrerenderCache!.get(key)
if (value) { return value as Promise<ResT> }
const promise = nuxtApp.runWithContext(_handler)
const promise = Promise.resolve().then(() => nuxtApp.runWithContext(_handler))
nuxtApp.ssrContext!._sharedPrerenderCache!.set(key, promise)
return promise
}
// Used to get default values
const getDefault = () => null
const getDefault = () => asyncDataDefaults.value
const getDefaultCachedData = () => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]
// Apply defaults
@ -250,11 +253,12 @@ export function useAsyncData<
console.warn('[nuxt] `boolean` values are deprecated for the `dedupe` option of `useAsyncData` and will be removed in the future. Use \'cancel\' or \'defer\' instead.')
}
// TODO: make more precise when v4 lands
const hasCachedData = () => options.getCachedData!(key, nuxtApp) != null
// Create or use a shared asyncData entity
if (!nuxtApp._asyncData[key] || !options.immediate) {
nuxtApp.payload._errors[key] ??= null
nuxtApp.payload._errors[key] ??= asyncDataDefaults.errorValue
const _ref = options.deep ? ref : shallowRef
@ -263,11 +267,15 @@ export function useAsyncData<
pending: ref(!hasCachedData()),
error: toRef(nuxtApp.payload._errors, key),
status: ref('idle'),
_default: options.default!,
}
}
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher
const asyncData = { ...nuxtApp._asyncData[key] } as AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
const asyncData = { ...nuxtApp._asyncData[key] } as { _default?: unknown } & AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
// Don't expose default function to end user
delete asyncData._default
asyncData.refresh = asyncData.execute = (opts = {}) => {
if (nuxtApp._asyncDataPromises[key]) {
@ -307,7 +315,7 @@ export function useAsyncData<
nuxtApp.payload.data[key] = result
asyncData.data.value = result
asyncData.error.value = null
asyncData.error.value = asyncDataDefaults.errorValue
asyncData.status.value = 'success'
})
.catch((error: any) => {
@ -404,11 +412,11 @@ export function useLazyAsyncData<
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
@ -418,18 +426,18 @@ export function useLazyAsyncData<
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
@ -440,15 +448,15 @@ export function useLazyAsyncData<
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> {
DefaultT = DefaultAsyncDataValue,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue> {
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
const [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
@ -463,12 +471,12 @@ export function useLazyAsyncData<
}
/** @since 3.1.0 */
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | null> } {
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | DefaultAsyncDataValue> } {
const nuxtApp = useNuxtApp()
// Initialize value when key is not already set
if (!(key in nuxtApp.payload.data)) {
nuxtApp.payload.data[key] = null
nuxtApp.payload.data[key] = asyncDataDefaults.value
}
return {
@ -520,12 +528,12 @@ function clearNuxtDataByKey (nuxtApp: NuxtApp, key: string): void {
}
if (key in nuxtApp.payload._errors) {
nuxtApp.payload._errors[key] = null
nuxtApp.payload._errors[key] = asyncDataDefaults.errorValue
}
if (nuxtApp._asyncData[key]) {
nuxtApp._asyncData[key]!.data.value = undefined
nuxtApp._asyncData[key]!.error.value = null
nuxtApp._asyncData[key]!.data.value = resetAsyncDataToUndefined ? undefined : nuxtApp._asyncData[key]!._default()
nuxtApp._asyncData[key]!.error.value = asyncDataDefaults.errorValue
nuxtApp._asyncData[key]!.pending.value = false
nuxtApp._asyncData[key]!.status.value = 'idle'
}

View File

@ -4,6 +4,9 @@ import { toRef } from 'vue'
import { useNuxtApp } from '../nuxt'
import { useRouter } from './router'
// @ts-expect-error virtual file
import { nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
export const NUXT_ERROR_SIGNATURE = '__nuxt_error'
/** @since 3.0.0 */
@ -47,7 +50,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
await useRouter().replace(options.redirect)
}
error.value = null
error.value = nuxtDefaultErrorValue
}
/** @since 3.0.0 */

View File

@ -8,6 +8,9 @@ import { useRequestFetch } from './ssr'
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
import { useAsyncData } from './asyncData'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
// @ts-expect-error virtual file
import { fetchDefaults } from '#build/nuxt.config.mjs'
@ -30,7 +33,7 @@ export interface UseFetchOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
R extends NitroFetchRequest = string & {},
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>,
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> {
@ -54,11 +57,11 @@ export function useFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
/**
* Fetch data from an API endpoint with an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-fetch}
@ -77,7 +80,7 @@ export function useFetch<
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
export function useFetch<
ResT = void,
ErrorT = FetchError,
@ -86,7 +89,7 @@ export function useFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
@ -161,8 +164,10 @@ export function useFetch<
* @see https://github.com/unjs/ofetch/blob/bb2d72baa5d3f332a2185c20fc04e35d2c3e258d/src/fetch.ts#L152
*/
const timeoutLength = toValue(opts.timeout)
let timeoutId: NodeJS.Timeout
if (timeoutLength) {
setTimeout(() => controller.abort(), timeoutLength)
timeoutId = setTimeout(() => controller.abort(), timeoutLength)
controller.signal.onabort = () => clearTimeout(timeoutId)
}
let _$fetch = opts.$fetch || globalThis.$fetch
@ -175,7 +180,7 @@ export function useFetch<
}
}
return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any) as Promise<_ResT>
return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any).finally(() => { clearTimeout(timeoutId) }) as Promise<_ResT>
}, _asyncDataOptions)
return asyncData
@ -190,11 +195,11 @@ export function useLazyFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
export function useLazyFetch<
ResT = void,
ErrorT = FetchError,
@ -207,7 +212,7 @@ export function useLazyFetch<
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
export function useLazyFetch<
ResT = void,
ErrorT = FetchError,
@ -216,7 +221,7 @@ export function useLazyFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>,

View File

@ -1,7 +1,7 @@
import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo'
import { parse } from 'devalue'
import { useHead } from '@unhead/vue'
import { getCurrentInstance } from 'vue'
import { getCurrentInstance, onServerPrefetch } from 'vue'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import { useRoute } from './router'
@ -16,9 +16,9 @@ interface LoadPayloadOptions {
}
/** @since 3.0.0 */
export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record<string, any> | Promise<Record<string, any>> | null {
export async function loadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<Record<string, any> | null> {
if (import.meta.server || !payloadExtraction) { return null }
const payloadURL = _getPayloadURL(url, opts)
const payloadURL = await _getPayloadURL(url, opts)
const nuxtApp = useNuxtApp()
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
if (payloadURL in cache) {
@ -39,26 +39,34 @@ export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record
return cache[payloadURL]
}
/** @since 3.0.0 */
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) {
const payloadURL = _getPayloadURL(url, opts)
useHead({
link: [
{ rel: 'modulepreload', href: payloadURL },
],
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<void> {
const nuxtApp = useNuxtApp()
const promise = _getPayloadURL(url, opts).then((payloadURL) => {
nuxtApp.runWithContext(() => useHead({
link: [
{ rel: 'modulepreload', href: payloadURL },
],
}))
})
if (import.meta.server) {
onServerPrefetch(() => promise)
}
return promise
}
// --- Internal ---
const filename = renderJsonPayloads ? '_payload.json' : '_payload.js'
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
async function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
const u = new URL(url, 'http://localhost')
if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) {
throw new Error('Payload URL must not include hostname: ' + url)
}
const config = useRuntimeConfig()
const hash = opts.hash || (opts.fresh ? Date.now() : config.app.buildId)
return joinURL(config.app.baseURL, u.pathname, filename + (hash ? `?${hash}` : ''))
const cdnURL = config.app.cdnURL
const baseOrCdnURL = cdnURL && await isPrerendered(url) ? cdnURL : config.app.baseURL
return joinURL(baseOrCdnURL, u.pathname, filename + (hash ? `?${hash}` : ''))
}
async function _importPayload (payloadURL: string) {

View File

@ -0,0 +1,7 @@
// TODO: temporary module for backwards compatibility
export type DefaultAsyncDataErrorValue = null
export type DefaultAsyncDataValue = null
export type DefaultErrorValue = null
export {}

View File

@ -23,6 +23,8 @@ import type { ViewTransition } from './plugins/view-transitions.client'
// @ts-expect-error virtual file
import { appId } from '#build/nuxt.config.mjs'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultErrorValue } from '#app/defaults'
import type { NuxtAppLiterals } from '#app'
function getNuxtAppCtx (appName = appId || 'nuxt-app') {
@ -92,8 +94,8 @@ export interface NuxtPayload {
state: Record<string, any>
once: Set<string>
config?: Pick<RuntimeConfig, 'public' | 'app'>
error?: NuxtError | null
_errors: Record<string, NuxtError | null>
error?: NuxtError | DefaultErrorValue
_errors: Record<string, NuxtError | DefaultAsyncDataErrorValue>
[key: string]: unknown
}
@ -120,10 +122,12 @@ interface _NuxtApp {
_asyncDataPromises: Record<string, Promise<any> | undefined>
/** @internal */
_asyncData: Record<string, {
data: Ref<any>
data: Ref<unknown>
pending: Ref<boolean>
error: Ref<Error | null>
error: Ref<Error | DefaultAsyncDataErrorValue>
status: Ref<AsyncDataRequestStatus>
/** @internal */
_default: () => unknown
} | undefined>
/** @internal */

View File

@ -1,13 +1,21 @@
import { consola, createConsola } from 'consola'
import { createConsola } from 'consola'
import type { LogObject } from 'consola'
import { parse } from 'devalue'
import { h } from 'vue'
import { defineNuxtPlugin } from '../nuxt'
// @ts-expect-error virtual file
import { devLogs, devRootDir } from '#build/nuxt.config.mjs'
export default defineNuxtPlugin((nuxtApp) => {
const devRevivers: Record<string, (data: any) => any> = import.meta.server
? {}
: {
VNode: data => h(data.type, data.props),
URL: data => new URL(data),
}
export default defineNuxtPlugin(async (nuxtApp) => {
if (import.meta.test) { return }
if (import.meta.server) {
@ -23,42 +31,18 @@ export default defineNuxtPlugin((nuxtApp) => {
date: true,
},
})
const hydrationLogs = new Set<string>()
consola.wrapConsole()
consola.addReporter({
log (logObj) {
try {
hydrationLogs.add(JSON.stringify(logObj.args))
} catch {
// silently ignore - the worst case is a user gets log twice
}
},
})
nuxtApp.hook('dev:ssr-logs', (logs) => {
for (const log of logs) {
// deduplicate so we don't print out things that are logged on client
try {
if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) {
logger.log(normalizeServerLog({ ...log }))
}
} catch {
logger.log(normalizeServerLog({ ...log }))
}
logger.log(normalizeServerLog({ ...log }))
}
})
nuxtApp.hooks.hook('app:suspense:resolve', () => consola.restoreAll())
nuxtApp.hooks.hookOnce('dev:ssr-logs', () => hydrationLogs.clear())
}
// pass SSR logs after hydration
nuxtApp.hooks.hook('app:suspense:resolve', async () => {
if (typeof window !== 'undefined') {
const content = document.getElementById('__NUXT_LOGS__')?.textContent
const logs = content ? parse(content, nuxtApp._payloadRevivers) as LogObject[] : []
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
}
})
if (typeof window !== 'undefined') {
const content = document.getElementById('__NUXT_LOGS__')?.textContent
const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
}
})
function normalizeFilenames (stack?: string) {

View File

@ -85,8 +85,8 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
changedTemplates.push(template)
}
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
const perf = performance.measure(fullPath, mark.name)
const setupTime = Math.round((perf.duration * 100)) / 100
if (nuxt.options.debug || setupTime > 500) {
logger.info(`Compiled \`${template.filename}\` in ${setupTime}ms`)

View File

@ -161,6 +161,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
'nuxt3/dist',
'nuxt-nightly/dist',
distDir,
// Ensure app config files have auto-imports injected even if they are pure .js files
...nuxt.options._layers.map(layer => resolve(layer.config.srcDir, 'app.config')),
],
traceInclude: [
// force include files used in generated code from the runtime-compiler

View File

@ -13,7 +13,7 @@ import fse from 'fs-extra'
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
import defu from 'defu'
import { gt } from 'semver'
import { gt, satisfies } from 'semver'
import pagesModule from '../pages/module'
import metaModule from '../head/module'
import componentsModule from '../components/module'
@ -129,6 +129,8 @@ async function initNuxt (nuxt: Nuxt) {
if (nuxt.options.typescript.shim) {
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/vue-shim.d.ts') })
}
// Add shims for `#build/*` imports that do not already have matching types
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/build.d.ts') })
// Add module augmentations directly to NuxtConfig
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/schema.d.ts') })
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app.config.d.ts') })
@ -270,6 +272,9 @@ async function initNuxt (nuxt: Nuxt) {
...nuxt.options._layers.filter(i => i.cwd.includes('node_modules')).map(i => i.cwd as string),
)
// Ensure we can resolve dependencies within layers
nuxt.options.modulesDir.push(...nuxt.options._layers.map(l => resolve(l.cwd, 'node_modules')))
// Init user modules
await nuxt.callHook('modules:before')
const modulesToInstall = []
@ -557,6 +562,12 @@ async function initNuxt (nuxt: Nuxt) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client'))
}
// Show compatibility version banner when Nuxt is running with a compatibility version
// that is different from the current major version
if (!(satisfies(nuxt._version, nuxt.options.future.compatibilityVersion + '.x'))) {
console.info(`Running with compatibility version \`${nuxt.options.future.compatibilityVersion}\``)
}
await nuxt.callHook('ready', nuxt)
}

View File

@ -6,11 +6,17 @@ import type { H3Event } from 'h3'
import { withTrailingSlash } from 'ufo'
import { getContext } from 'unctx'
import { isVNode } from 'vue'
import type { NitroApp } from '#internal/nitro/app'
// @ts-expect-error virtual file
import { rootDir } from '#internal/dev-server-logs-options'
const devReducers: Record<string, (data: any) => any> = {
VNode: data => isVNode(data) ? { type: data.type, props: data.props } : undefined,
URL: data => data instanceof URL ? data.toString() : undefined,
}
interface NuxtDevAsyncContext {
logs: LogObject[]
event: H3Event
@ -54,9 +60,10 @@ export default (nitroApp: NitroApp) => {
const ctx = asyncContext.tryUse()
if (!ctx) { return }
try {
htmlContext.bodyAppend.unshift(`<script type="application/json" id="__NUXT_LOGS__">${stringify(ctx.logs, ctx.event.context._payloadReducers)}</script>`)
htmlContext.bodyAppend.unshift(`<script type="application/json" id="__NUXT_LOGS__">${stringify(ctx.logs, { ...devReducers, ...ctx.event.context._payloadReducers })}</script>`)
} catch (e) {
console.warn('[nuxt] Failed to stringify dev server logs. You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.', e)
const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : ''
console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`)
}
})
}

View File

@ -1 +0,0 @@
../../../../../ui-templates/dist/templates/error-500.ts

View File

@ -1 +0,0 @@
../../../../../ui-templates/dist/templates/error-dev.ts

View File

@ -327,7 +327,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Whether we are prerendering route
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + ssrContext.runtimeConfig.app.buildId : undefined
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.cdnURL || ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + ssrContext.runtimeConfig.app.buildId : undefined
if (import.meta.prerender) {
ssrContext.payload.prerenderedAt = Date.now()
}

View File

@ -130,6 +130,12 @@ declare module '#app' {
}
}
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
}
declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { }
}
@ -266,10 +272,13 @@ export const useRuntimeConfig = () => window?.__NUXT__?.config || {}
export const appConfigDeclarationTemplate: NuxtTemplate = {
filename: 'types/app.config.d.ts',
getContents ({ app, nuxt }) {
const typesDir = join(nuxt.options.buildDir, 'types')
const configPaths = app.configs.map(path => relative(typesDir, path).replace(/\b\.\w+$/g, ''))
return `
import type { CustomAppConfig } from 'nuxt/schema'
import type { Defu } from 'defu'
${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id.replace(/\b\.\w+$/g, ''))}`).join('\n')}
${configPaths.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id)}`).join('\n')}
declare const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)}
type ResolvedAppConfig = Defu<typeof inlineConfig, [${app.configs.map((_id: string, index: number) => `typeof cfg${index}`).join(', ')}]>
@ -393,7 +402,13 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
`export const devLogs = ${JSON.stringify(ctx.nuxt.options.features.devLogs)}`,
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`,
`export const asyncDataDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.useAsyncData)}`,
`export const asyncDataDefaults = ${JSON.stringify({
...ctx.nuxt.options.experimental.defaults.useAsyncData,
value: ctx.nuxt.options.experimental.defaults.useAsyncData.value === 'null' ? null : undefined,
errorValue: ctx.nuxt.options.experimental.defaults.useAsyncData.errorValue === 'null' ? null : undefined,
})}`,
`export const resetAsyncDataToUndefined = ${ctx.nuxt.options.experimental.resetAsyncDataToUndefined}`,
`export const nuxtDefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}`,
`export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`,
`export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`,
`export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
@ -401,3 +416,29 @@ export const nuxtConfigTemplate: NuxtTemplate = {
].join('\n\n')
},
}
const TYPE_FILENAME_RE = /\.([cm])?[jt]s$/
const DECLARATION_RE = /\.d\.[cm]?ts$/
export const buildTypeTemplate: NuxtTemplate = {
filename: 'types/build.d.ts',
getContents ({ app }) {
let declarations = ''
for (const file of app.templates) {
if (file.write || !file.filename || DECLARATION_RE.test(file.filename)) {
continue
}
if (TYPE_FILENAME_RE.test(file.filename)) {
const typeFilenames = new Set([file.filename.replace(TYPE_FILENAME_RE, '.d.$1ts'), file.filename.replace(TYPE_FILENAME_RE, '.d.ts')])
if (app.templates.some(f => f.filename && typeFilenames.has(f.filename))) {
continue
}
}
declarations += 'declare module ' + JSON.stringify(join('#build', file.filename)) + ';\n'
}
return declarations
},
}

View File

@ -135,6 +135,7 @@ export const scriptsStubsPreset = {
'useScriptGoogleMaps',
'useScriptNpm',
],
priority: -1,
from: '#app/composables/script-stubs',
} satisfies InlinePreset

View File

@ -422,11 +422,6 @@ export default defineNuxtModule({
getContents: () => 'export { START_LOCATION, useRoute } from \'vue-router\'',
})
// Optimize vue-router to ensure we share the same injection symbol
nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {}
nuxt.options.vite.optimizeDeps.include = nuxt.options.vite.optimizeDeps.include || []
nuxt.options.vite.optimizeDeps.include.push('vue-router')
nuxt.options.vite.resolve = nuxt.options.vite.resolve || {}
nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || []
nuxt.options.vite.resolve.dedupe.push('vue-router')

View File

@ -1,10 +1,13 @@
import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import type { Nuxt } from '@nuxt/schema'
import { stripLiteral } from 'strip-literal'
import { isVue } from '../../core/utils'
const INJECTION_RE = /\b_ctx\.\$route\b/g
const INJECTION_SINGLE_RE = /\b_ctx\.\$route\b/
const INJECTION_RE_TEMPLATE = /\b_ctx\.\$route\b/g
const INJECTION_RE_SCRIPT = /\bthis\.\$route\b/g
const INJECTION_SINGLE_RE = /\bthis\.\$route\b|\b_ctx\.\$route\b/
export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
return {
@ -14,14 +17,30 @@ export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
return isVue(id, { type: ['template', 'script'] })
},
transform (code) {
if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol')) { return }
if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol') || code.includes('this._.provides[__nuxt_route_symbol')) { return }
let replaced = false
const s = new MagicString(code)
s.replace(INJECTION_RE, () => {
replaced = true
return '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)'
})
const strippedCode = stripLiteral(code)
// Local helper function for regex-based replacements using `strippedCode`
const replaceMatches = (regExp: RegExp, replacement: string) => {
for (const match of strippedCode.matchAll(regExp)) {
const start = match.index!
const end = start + match[0].length
s.overwrite(start, end, replacement)
if (!replaced) {
replaced = true
}
}
}
// handles `$route` in template
replaceMatches(INJECTION_RE_TEMPLATE, '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)')
// handles `this.$route` in script
replaceMatches(INJECTION_RE_SCRIPT, '(this._.provides[__nuxt_route_symbol] || this.$route)')
if (replaced) {
s.prepend('import { PageRouteSymbol as __nuxt_route_symbol } from \'#app/components/injections\';\n')
}

View File

@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest'
import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc'
import type { Plugin } from 'vite'
import type { Nuxt } from '@nuxt/schema'
import { RouteInjectionPlugin } from '../src/pages/plugins/route-injection'
describe('route-injection:transform', () => {
const injectionPlugin = RouteInjectionPlugin({ options: { sourcemap: { client: false, server: false } } } as Nuxt).raw({}, { framework: 'rollup' }) as Plugin
const transform = async (source: string) => {
const result = await (injectionPlugin.transform! as Function).call({ error: null, warn: null } as any, source, 'test.vue')
const code: string = typeof result === 'string' ? result : result?.code
let depth = 0
return code.split('\n').map((l) => {
l = l.trim()
if (l.match(/^[}\]]/)) { depth-- }
const res = ''.padStart(depth * 2, ' ') + l
if (l.match(/[{[]$/)) { depth++ }
return res
}).join('\n')
}
it('should correctly inject route in template', async () => {
const sfc = `<template>{{ $route.path }}</template>`
const res = compileTemplate({
filename: 'test.vue',
id: 'test.vue',
source: sfc,
})
const transformResult = await transform(res.code)
expect(transformResult).toMatchInlineSnapshot(`
"import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';
import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("template", null, [
_createTextVNode(_toDisplayString((_ctx._.provides[__nuxt_route_symbol] || _ctx.$route).path), 1 /* TEXT */)
]))
}"
`)
})
it('should correctly inject route in options api', async () => {
const sfc = `
<template>{{ thing }}</template>
<script>
export default {
computed: {
thing () {
return this.$route.path
}
}
}
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'test.vue' })
const transformResult = await transform(res.content)
expect(transformResult).toMatchInlineSnapshot(`
"import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';
export default {
computed: {
thing () {
return (this._.provides[__nuxt_route_symbol] || this.$route).path
}
}
}
"
`)
})
})

View File

@ -35,11 +35,11 @@
},
"devDependencies": {
"@nuxt/telemetry": "2.5.4",
"@nuxt/ui-templates": "1.3.3",
"@nuxt/ui-templates": "1.3.4",
"@types/file-loader": "5.0.4",
"@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8",
"@unhead/schema": "1.9.10",
"@unhead/schema": "1.9.11",
"@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue-jsx": "3.1.0",
"@vue/compiler-core": "3.4.27",
@ -54,7 +54,7 @@
"unbuild": "latest",
"unctx": "2.3.1",
"unenv": "1.9.0",
"vite": "5.2.11",
"vite": "5.2.12",
"vue": "3.4.27",
"vue-bundle-renderer": "2.1.0",
"vue-loader": "17.4.2",
@ -71,7 +71,7 @@
"scule": "^1.3.0",
"std-env": "^3.7.0",
"ufo": "^1.5.3",
"unimport": "^3.7.1",
"unimport": "^3.7.2",
"uncrypto": "^0.1.3",
"untyped": "^1.4.2"
},

View File

@ -261,6 +261,7 @@ export default defineUntypedSchema({
/**
* Boolean or a path to an HTML file with the contents of which will be inserted into any HTML page
* rendered with `ssr: false`.
*
* - If it is unset, it will use `~/app/spa-loading-template.html` file in one of your layers, if it exists.
* - If it is false, no SPA loading indicator will be loaded.
* - If true, Nuxt will look for `~/app/spa-loading-template.html` file in one of your layers, or a

View File

@ -29,6 +29,7 @@ export default defineUntypedSchema({
* compileTemplate: true,
* templateUtils: true,
* relativeWatchPaths: true,
* resetAsyncDataToUndefined: true,
* defaults: {
* useAsyncData: {
* deep: true
@ -342,8 +343,10 @@ export default defineUntypedSchema({
/**
* Use new experimental head optimisations:
*
* - Add the capo.js head plugin in order to render tags in of the head in a more performant way.
* - Uses the hash hydration plugin to reduce initial hydration
*
* @see [Nuxt Discussion #22632](https://github.com/nuxt/nuxt/discussions/22632]
*/
headNext: true,
@ -392,7 +395,11 @@ export default defineUntypedSchema({
* })
* ```
*/
sharedPrerenderData: false,
sharedPrerenderData: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
},
},
/**
* Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
@ -415,6 +422,18 @@ export default defineUntypedSchema({
* Options that apply to `useAsyncData` (and also therefore `useFetch`)
*/
useAsyncData: {
/** @type {'undefined' | 'null'} */
value: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
},
},
/** @type {'undefined' | 'null'} */
errorValue: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
},
},
deep: {
async $resolve (val, get) {
return val ?? !((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
@ -476,5 +495,15 @@ export default defineUntypedSchema({
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
},
},
/**
* Whether `clear` and `clearNuxtData` should reset async data to its _default_ value or update
* it to `null`/`undefined`.
*/
resetAsyncDataToUndefined: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
},
},
},
})

View File

@ -46,11 +46,13 @@ export default defineUntypedSchema({
* Nitro server handlers.
*
* Each handler accepts the following options:
*
* - handler: The path to the file defining the handler.
* - route: The route under which the handler is available. This follows the conventions of https://github.com/unjs/radix3.
* - method: The HTTP method of requests that should be handled.
* - middleware: Specifies whether it is a middleware handler.
* - lazy: Specifies whether to use lazy loading to import the handler.
*
* @see https://nuxt.com/docs/guide/directory-structure/server
* @note Files from `server/api`, `server/middleware` and `server/routes` will be automatically registered by Nuxt.
* @example

View File

@ -157,7 +157,11 @@ export default defineUntypedSchema({
* See https://github.com/esbuild-kit/esbuild-loader
* @type {Omit<typeof import('esbuild-loader')['LoaderOptions'], 'loader'>}
*/
esbuild: {},
esbuild: {
jsxFactory: 'h',
jsxFragment: 'Fragment',
tsconfigRaw: '{}',
},
/**
* See: https://github.com/webpack-contrib/file-loader#options

View File

@ -1,5 +1,6 @@
import { fileURLToPath } from 'node:url'
import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
import { copyFile } from 'node:fs/promises'
import { basename, dirname, join, resolve } from 'pathe'
import type { Plugin } from 'vite'
// @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151
@ -167,6 +168,15 @@ export const RenderPlugin = () => {
unlinkSync(fileName)
rmdirSync(dirname(fileName))
}
// we manually copy files across rather than using symbolic links for better windows support
const nuxtRoot = r('../nuxt')
for (const file of ['error-404.vue', 'error-500.vue', 'error-dev.vue', 'welcome.vue']) {
await copyFile(r(`dist/templates/${file}`), join(nuxtRoot, 'src/app/components', file))
}
for (const file of ['error-500.ts', 'error-dev.ts']) {
await copyFile(r(`dist/templates/${file}`), join(nuxtRoot, 'src/core/runtime/nitro', file))
}
},
}
}

View File

@ -31,6 +31,6 @@
"prettier": "3.2.5",
"scule": "1.3.0",
"unocss": "0.60.3",
"vite": "5.2.11"
"vite": "5.2.12"
}
}

View File

@ -41,7 +41,7 @@
"consola": "^3.2.3",
"cssnano": "^7.0.1",
"defu": "^6.1.4",
"esbuild": "^0.21.3",
"esbuild": "^0.21.4",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"externality": "^1.0.2",
@ -62,7 +62,7 @@
"ufo": "^1.5.3",
"unenv": "^1.9.0",
"unplugin": "^1.10.1",
"vite": "^5.2.11",
"vite": "^5.2.12",
"vite-node": "^1.6.0",
"vite-plugin-checker": "^0.6.4",
"vue-bundle-renderer": "^2.1.0"

View File

@ -63,6 +63,48 @@ export async function buildClient (ctx: ViteBuildContext) {
},
optimizeDeps: {
entries: [ctx.entry],
include: [],
// We exclude Vue and Nuxt common dependencies from optimization
// as they already ship ESM.
//
// This will help to reduce the chance for users to encounter
// common chunk conflicts that causing browser reloads.
// We should also encourage module authors to add their deps to
// `exclude` if they ships bundled ESM.
//
// Also since `exclude` is inert, it's safe to always include
// all possible deps even if they are not used yet.
//
// @see https://github.com/antfu/nuxt-better-optimize-deps#how-it-works
exclude: [
// Vue
'vue',
'@vue/runtime-core',
'@vue/runtime-dom',
'@vue/reactivity',
'@vue/shared',
'@vue/devtools-api',
'vue-router',
'vue-demi',
// Nuxt
'nuxt',
'nuxt/app',
// Nuxt Deps
'@unhead/vue',
'consola',
'defu',
'devalue',
'h3',
'hookable',
'klona',
'ofetch',
'pathe',
'ufo',
'unctx',
'unenv',
],
},
resolve: {
alias: {
@ -129,18 +171,22 @@ export async function buildClient (ctx: ViteBuildContext) {
}) as any
if (clientConfig.server && clientConfig.server.hmr !== false) {
const hmrPortDefault = 24678 // Vite's default HMR port
const hmrPort = await getPort({
port: hmrPortDefault,
ports: Array.from({ length: 20 }, (_, i) => hmrPortDefault + 1 + i),
})
clientConfig.server = defu(clientConfig.server, <ServerOptions> {
https: ctx.nuxt.options.devServer.https,
const serverDefaults: Omit<ServerOptions, 'hmr'> & { hmr: Exclude<ServerOptions['hmr'], boolean> } = {
hmr: {
protocol: ctx.nuxt.options.devServer.https ? 'wss' : 'ws',
port: hmrPort,
},
})
}
if (typeof clientConfig.server.hmr !== 'object' || !clientConfig.server.hmr.server) {
const hmrPortDefault = 24678 // Vite's default HMR port
serverDefaults.hmr!.port = await getPort({
port: hmrPortDefault,
ports: Array.from({ length: 20 }, (_, i) => hmrPortDefault + 1 + i),
})
}
if (ctx.nuxt.options.devServer.https) {
serverDefaults.https = ctx.nuxt.options.devServer.https === true ? {} : ctx.nuxt.options.devServer.https
}
clientConfig.server = defu(clientConfig.server, serverDefaults as ViteConfig['server'])
}
// Add analyze plugin if needed
@ -162,6 +208,10 @@ export async function buildClient (ctx: ViteBuildContext) {
await ctx.nuxt.callHook('vite:configResolved', clientConfig, { isClient: true, isServer: false })
// Prioritize `optimizeDeps.exclude`. If same dep is in `include` and `exclude`, remove it from `include`
clientConfig.optimizeDeps!.include = clientConfig.optimizeDeps!.include!
.filter(dep => !clientConfig.optimizeDeps!.exclude!.includes(dep))
if (ctx.nuxt.options.dev) {
// Dev
const viteServer = await vite.createServer(clientConfig)

View File

@ -3,21 +3,13 @@ import { useNitro } from '@nuxt/kit'
import { createUnplugin } from 'unplugin'
import { withLeadingSlash, withTrailingSlash } from 'ufo'
import { dirname, relative } from 'pathe'
import MagicString from 'magic-string'
const PREFIX = 'virtual:public?'
const CSS_URL_RE = /url\((\/[^)]+)\)/g
export const VitePublicDirsPlugin = createUnplugin(() => {
const nitro = useNitro()
function resolveFromPublicAssets (id: string) {
for (const dir of nitro.options.publicAssets) {
if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue }
const path = id.replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
if (existsSync(path)) {
return id
}
}
}
export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boolean }) => {
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
return {
name: 'nuxt:vite-public-dir-resolution',
@ -40,14 +32,33 @@ export const VitePublicDirsPlugin = createUnplugin(() => {
}
},
},
generateBundle (outputOptions, bundle) {
renderChunk (code, chunk) {
if (!chunk.facadeModuleId?.includes('?inline&used')) { return }
const s = new MagicString(code)
const q = code.match(/(?<= = )['"`]/)?.[0] || '"'
for (const [full, url] of code.matchAll(CSS_URL_RE)) {
if (resolveFromPublicAssets(url)) {
s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`)
}
}
if (s.hasChanged()) {
s.prepend(`import { publicAssetsURL } from '#internal/nuxt/paths';`)
return {
code: s.toString(),
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined,
}
}
},
generateBundle (_outputOptions, bundle) {
for (const file in bundle) {
const chunk = bundle[file]
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }
let css = chunk.source.toString()
let wasReplaced = false
for (const [full, url] of css.matchAll(/url\((\/[^)]+)\)/g)) {
for (const [full, url] of css.matchAll(CSS_URL_RE)) {
if (resolveFromPublicAssets(url)) {
const relativeURL = relative(withLeadingSlash(dirname(file)), url)
css = css.replace(full, `url(${relativeURL})`)
@ -62,3 +73,19 @@ export const VitePublicDirsPlugin = createUnplugin(() => {
},
}
})
export function useResolveFromPublicAssets () {
const nitro = useNitro()
function resolveFromPublicAssets (id: string) {
for (const dir of nitro.options.publicAssets) {
if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue }
const path = id.replace(/[?#].*$/, '').replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
if (existsSync(path)) {
return id
}
}
}
return { resolveFromPublicAssets }
}

View File

@ -51,7 +51,7 @@ export async function buildServer (ctx: ViteBuildContext) {
'XMLHttpRequest': 'undefined',
},
optimizeDeps: {
entries: ctx.nuxt.options.ssr ? [ctx.entry] : [],
noDiscovery: true,
},
resolve: {
alias: {

View File

@ -3,6 +3,7 @@ import { logger } from '@nuxt/kit'
import { hasTTY, isCI } from 'std-env'
import clear from 'clear'
import type { NuxtOptions } from '@nuxt/schema'
import { useResolveFromPublicAssets } from '../plugins/public-dirs'
let duplicateCount = 0
let lastType: vite.LogType | null = null
@ -26,11 +27,18 @@ export function createViteLogger (config: vite.InlineConfig): vite.Logger {
const canClearScreen = hasTTY && !isCI && config.clearScreen
const clearScreen = canClearScreen ? clear : () => {}
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
function output (type: vite.LogType, msg: string, options: vite.LogErrorOptions = {}) {
if (typeof msg === 'string' && !process.env.DEBUG) {
// TODO: resolve upstream in Vite
// Hide sourcemap warnings related to node_modules
if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { return }
// Hide warnings about externals produced by https://github.com/vitejs/vite/blob/v5.2.11/packages/vite/src/node/plugins/css.ts#L350-L355
if (msg.includes('didn\'t resolve at build time, it will remain unchanged to be resolved at runtime')) {
const id = msg.trim().match(/^([^ ]+) referenced in/m)?.[1]
if (id && resolveFromPublicAssets(id)) { return }
}
}
const sameAsLast = lastType === type && lastMsg === msg

View File

@ -71,10 +71,6 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
'abort-controller': 'unenv/runtime/mock/empty',
},
},
optimizeDeps: {
include: ['vue'],
exclude: ['nuxt/app'],
},
css: resolveCSSOptions(nuxt),
define: {
__NUXT_VERSION__: JSON.stringify(nuxt._version),
@ -100,7 +96,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
},
plugins: [
// add resolver for files in public assets directories
VitePublicDirsPlugin.vite(),
VitePublicDirsPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server }),
composableKeysPlugin.vite({
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
rootDir: nuxt.options.rootDir,

View File

@ -28,7 +28,7 @@
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*",
"autoprefixer": "^10.4.19",
"css-loader": "^7.1.1",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"cssnano": "^7.0.1",
"defu": "^6.1.4",

File diff suppressed because it is too large Load Diff

View File

@ -1915,11 +1915,24 @@ describe('public directories', () => {
// TODO: dynamic paths in dev
describe.skipIf(isDev())('dynamic paths', () => {
const publicFiles = ['/public.svg', '/css-only-public-asset.svg']
const isPublicFile = (base = '/', file: string) => {
if (isWebpack) {
// TODO: webpack does not yet support dynamic static paths
expect(publicFiles).toContain(file)
return true
}
expect(file).toMatch(new RegExp(`^${base.replace(/\//g, '\\/')}`))
expect(publicFiles).toContain(file.replace(base, '/'))
return true
}
it('should work with no overrides', async () => {
const html: string = await $fetch<string>('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3]
expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy()
expect(url.startsWith('/_nuxt/') || isPublicFile('/', url)).toBeTruthy()
}
})
@ -1929,16 +1942,14 @@ describe.skipIf(isDev())('dynamic paths', () => {
const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)).map(m => m[2] || m[3])
const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u))
expect(cssURL).toBeDefined()
const css: string = await $fetch<string>(cssURL!)
const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.'))
expect(imageUrls).toMatchInlineSnapshot(`
[
"./logo.svg",
"../public.svg",
"../public.svg",
"../public.svg",
]
`)
const css = await $fetch<string>(cssURL!)
const imageUrls = new Set(Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.')))
expect([...imageUrls]).toMatchInlineSnapshot(`
[
"./logo.svg",
"../public.svg",
]
`)
})
it('should allow setting base URL and build assets directory', async () => {
@ -1952,12 +1963,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3]
expect(
url.startsWith('/foo/_other/') ||
url === '/foo/public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
expect(url.startsWith('/foo/_other/') || isPublicFile('/foo/', url)).toBeTruthy()
}
expect(await $fetch<string>('/foo/url')).toContain('path: /foo/url')
@ -1973,12 +1979,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3]
expect(
url.startsWith('./_nuxt/') ||
url === './public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
expect(url.startsWith('./_nuxt/') || isPublicFile('./', url)).toBeTruthy()
expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy()
}
})
@ -2007,12 +2008,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3]
expect(
url.startsWith('https://example.com/_cdn/') ||
url === 'https://example.com/public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
expect(url.startsWith('https://example.com/_cdn/') || isPublicFile('https://example.com/', url)).toBeTruthy()
}
})
@ -2433,21 +2429,23 @@ describe.skipIf(isWindows)('useAsyncData', () => {
})
it('data is null after navigation when immediate false', async () => {
const defaultValue = isV4 ? 'undefined' : 'null'
const { page } = await renderPage('/useAsyncData/immediate-remove-unmounted')
expect(await page.locator('#immediate-data').getByText('null').textContent()).toBe('null')
expect(await page.locator('#immediate-data').getByText(defaultValue).textContent()).toBe(defaultValue)
await page.click('#execute-btn')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain('null')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain(defaultValue)
await page.click('#to-index')
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/')
await page.click('#to-immediate-remove-unmounted')
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/useAsyncData/immediate-remove-unmounted')
expect(await page.locator('#immediate-data').getByText('null').textContent()).toBe('null')
expect(await page.locator('#immediate-data').getByText(defaultValue).textContent()).toBe(defaultValue)
await page.click('#execute-btn')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain('null')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain(defaultValue)
await page.close()
})

View File

@ -11,6 +11,9 @@ import type { NavigateToOptions } from '#app/composables/router'
import { NuxtLayout, NuxtLink, NuxtPage, ServerComponent, WithTypes } from '#components'
import { useRouter } from '#imports'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
interface TestResponse { message: string }
describe('API routes', () => {
@ -31,61 +34,61 @@ describe('API routes', () => {
})
it('works with useAsyncData', () => {
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useAsyncData('api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useAsyncData('api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useAsyncData('api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | null>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
// backwards compatibility
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useLazyAsyncData<TestResponse>('lazy-api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useLazyAsyncData<TestResponse>('lazy-api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
})
it('works with useFetch', () => {
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
// @ts-expect-error not a valid method
useFetch('/api/hey', { method: 'PATCH' })
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useLazyFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useLazyFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
})
})
@ -421,10 +424,10 @@ describe('composables', () => {
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { default: () => 'test' }).data).toEqualTypeOf<Ref<string>>()
// transform must match the explicit generic because of typescript limitations microsoft/TypeScript#14400
expectTypeOf(useFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useFetch<string>('/test', { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useLazyFetch<string>('/test', { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
@ -439,7 +442,7 @@ describe('composables', () => {
return data.foo
},
})
expectTypeOf(data).toEqualTypeOf<Ref<'bar' | null>>()
expectTypeOf(data).toEqualTypeOf<Ref<'bar' | DefaultAsyncDataValue>>()
})
it('infer request url string literal from server/api routes', () => {
@ -448,8 +451,8 @@ describe('composables', () => {
expectTypeOf(useFetch(dynamicStringUrl).data).toEqualTypeOf<Ref<unknown>>()
// request param should infer string literal type / show auto-complete hint base on server routes, ex: '/api/hello'
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
// request can accept string literal and Request object type
expectTypeOf(useFetch('https://example.com/api').data).toEqualTypeOf<Ref<unknown>>()
@ -519,7 +522,7 @@ describe('composables', () => {
it('correctly types returns when using with getCachedData', () => {
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
getCachedData: key => useNuxtApp().payload.data[key],
}).data).toEqualTypeOf<Ref<{ foo: number } | null>>()
}).data).toEqualTypeOf<Ref<{ foo: number } | DefaultAsyncDataValue>>()
useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
// @ts-expect-error cached data should return the same as value of fetcher
getCachedData: () => ({ bar: 2 }),

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line no-undef
export default defineAppConfig({
userConfig: 123,
nested: {

View File

@ -1,4 +1,5 @@
:root {
--global: 'global';
--asset: url('~/assets/css-only-asset.svg');
--public-asset: url('/css-only-public-asset.svg');
}

View File

@ -1,6 +1,7 @@
<template>
<div>
<img src="/public.svg">
<img src="/public.svg?123">
<img src="/custom/file.svg">
</div>
</template>

View File

@ -2,7 +2,7 @@
<div>
<div>immediate-remove-unmounted.vue</div>
<div id="immediate-data">
{{ data === null ? "null" : data }}
{{ data === null ? "null" : (data === undefined ? 'undefined' : data) }}
</div>
<button
id="execute-btn"
@ -20,9 +20,11 @@
</template>
<script setup lang="ts">
import { asyncDataDefaults } from '#build/nuxt.config.mjs'
const { data, execute } = await useAsyncData('immediateFalse', () => $fetch('/api/random'), { immediate: false })
if (data.value !== null) {
throw new Error('Initial data should be null: ' + data.value)
if (data.value !== asyncDataDefaults.errorValue) {
throw new Error(`Initial data should be ${asyncDataDefaults.errorValue}: ` + data.value)
}
</script>

View File

@ -0,0 +1 @@
<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"></svg>

After

Width:  |  Height:  |  Size: 67 B

View File

@ -20,6 +20,9 @@ import { callOnce } from '#app/composables/once'
import { useLoadingIndicator } from '#app/composables/loading-indicator'
import { useRouteAnnouncer } from '#app/composables/route-announcer'
// @ts-expect-error virtual file
import { asyncDataDefaults, nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
registerEndpoint('/api/test', defineEventHandler(event => ({
method: event.method,
headers: Object.fromEntries(event.headers.entries()),
@ -126,7 +129,7 @@ describe('useAsyncData', () => {
]
`)
expect(res instanceof Promise).toBeTruthy()
expect(res.data.value).toBe(null)
expect(res.data.value).toBe(asyncDataDefaults.value)
await res
expect(res.data.value).toBe('test')
})
@ -138,7 +141,7 @@ describe('useAsyncData', () => {
expect(immediate.pending.value).toBe(false)
const nonimmediate = await useAsyncData(() => Promise.resolve('test'), { immediate: false })
expect(nonimmediate.data.value).toBe(null)
expect(nonimmediate.data.value).toBe(asyncDataDefaults.value)
expect(nonimmediate.status.value).toBe('idle')
expect(nonimmediate.pending.value).toBe(true)
})
@ -163,9 +166,9 @@ describe('useAsyncData', () => {
// https://github.com/nuxt/nuxt/issues/23411
it('should initialize with error set to null when immediate: false', async () => {
const { error, execute } = useAsyncData(() => ({}), { immediate: false })
expect(error.value).toBe(null)
expect(error.value).toBe(asyncDataDefaults.errorValue)
await execute()
expect(error.value).toBe(null)
expect(error.value).toBe(asyncDataDefaults.errorValue)
})
it('should be accessible with useNuxtData', async () => {
@ -206,8 +209,9 @@ describe('useAsyncData', () => {
clear()
// TODO: update to asyncDataDefaults.value in v4
expect(data.value).toBeUndefined()
expect(error.value).toBeNull()
expect(error.value).toBe(asyncDataDefaults.errorValue)
expect(pending.value).toBe(false)
expect(status.value).toBe('idle')
})
@ -345,13 +349,12 @@ describe('errors', () => {
})
it('global nuxt errors', () => {
const err = useError()
expect(err.value).toBeUndefined()
const error = useError()
expect(error.value).toBeUndefined()
showError('new error')
expect(err.value).toMatchInlineSnapshot('[Error: new error]')
expect(error.value).toMatchInlineSnapshot('[Error: new error]')
clearError()
// TODO: should this return to being undefined?
expect(err.value).toBeNull()
expect(error.value).toBe(nuxtDefaultErrorValue)
})
})
@ -616,7 +619,7 @@ describe('routing utilities: `abortNavigation`', () => {
it('should throw an error if one is provided', () => {
const error = useError()
expect(() => abortNavigation({ message: 'Page not found' })).toThrowErrorMatchingInlineSnapshot('[Error: Page not found]')
expect(error.value).toBeFalsy()
expect(error.value).toBe(nuxtDefaultErrorValue)
})
it('should block navigation if no error is provided', () => {
expect(abortNavigation()).toMatchInlineSnapshot('false')