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 - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Lychee link checker - name: Lychee link checker
uses: lycheeverse/lychee-action@054a8e8c7a88ada133165c6633a49825a32174e2 # for v1.8.0 uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0
with: with:
# arguments with file types to check # arguments with file types to check
args: >- args: >-

View File

@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with: with:
ref: refs/pull/${{ github.event.issue.number }}/merge ref: ${{ github.event.issue.pull_request.head.sha }}
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - 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 #### Conflict with End-To-End Testing

View File

@ -48,6 +48,7 @@ export default defineNuxtConfig({
// experimental: { // experimental: {
// sharedPrerenderData: false, // sharedPrerenderData: false,
// compileTemplate: true, // compileTemplate: true,
// resetAsyncDataToUndefined: true,
// templateUtils: true, // templateUtils: true,
// relativeWatchPaths: true, // relativeWatchPaths: true,
// defaults: { // 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` #### Shallow Data Reactivity in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal 🚦 **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"} ::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 ### 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. 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). `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 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.
will delete it on a condition like exiting a web browser application.
::note ::note
The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and 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!
`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 ::note
@ -74,22 +71,29 @@ If neither of `expires` and `maxAge` is set, the cookie will be session-only and
### `httpOnly` ### `httpOnly`
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy, 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.
the `HttpOnly` attribute is set; otherwise it is not. By default, the `HttpOnly` attribute is not set.
::warning ::warning
Be careful when setting this to `true`, as compliant clients will not allow client-side Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
JavaScript to see the cookie in `document.cookie`.
:: ::
### `secure` ### `secure`
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy, 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.
the `Secure` attribute is set; otherwise it is not. By default, the `Secure` attribute is not set.
::warning ::warning
Be careful when setting this to `true`, as compliant clients will not send the cookie back to 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.
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` ### `domain`
@ -114,23 +118,18 @@ More information about the different enforcement levels can be found in [the spe
### `encode` ### `encode`
Specifies a function that will be used to encode a cookie's value. Since the value of a cookie 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.
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`. The default encoder is the `JSON.stringify` + `encodeURIComponent`.
### `decode` ### `decode`
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie 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.
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). The default decoder is `decodeURIComponent` + [destr](https://github.com/unjs/destr).
::note ::note
If an error is thrown from this function, the original, non-decoded cookie value will If an error is thrown from this function, the original, non-decoded cookie value will be returned as the cookie's value.
be returned as the cookie's value.
:: ::
### `default` ### `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 ### 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! ❤️ We always welcome pull requests! ❤️
#### Before You Start #### 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 Release | | Initial release | End Of Life | Docs
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|------- ----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
**4.x** (scheduled) | | 2024 Q2 | |   **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) **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="></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.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="></a> | 2018-01-08 | 2019-09-21 | &nbsp; **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 ### Support Status

View File

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

View File

@ -44,7 +44,7 @@
"semver": "^7.6.2", "semver": "^7.6.2",
"ufo": "^1.5.3", "ufo": "^1.5.3",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unimport": "^3.7.1", "unimport": "^3.7.2",
"untyped": "^1.4.2" "untyped": "^1.4.2"
}, },
"devDependencies": { "devDependencies": {
@ -54,7 +54,7 @@
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"nitropack": "2.9.6", "nitropack": "2.9.6",
"unbuild": "latest", "unbuild": "latest",
"vite": "5.2.11", "vite": "5.2.12",
"vitest": "1.6.0", "vitest": "1.6.0",
"webpack": "5.91.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 key = `nuxt:module:${uniqueKey || (Math.round(Math.random() * 10000))}`
const mark = performance.mark(key) const mark = performance.mark(key)
const res = await module.setup?.call(null as any, _options, nuxt) ?? {} 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 perf = performance.measure(key, mark.name)
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL const setupTime = Math.round((perf.duration * 100)) / 100
// Measure setup time // Measure setup time
if (setupTime > 5000 && uniqueKey !== '@nuxt/telemetry') { 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": { "dependencies": {
"@nuxt/devalue": "^2.0.2", "@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.3.1", "@nuxt/devtools": "^1.3.2",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.4", "@nuxt/telemetry": "^2.5.4",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.10", "@unhead/dom": "^1.9.11",
"@unhead/ssr": "^1.9.10", "@unhead/ssr": "^1.9.11",
"@unhead/vue": "^1.9.10", "@unhead/vue": "^1.9.11",
"@vue/shared": "^3.4.27", "@vue/shared": "^3.4.27",
"acorn": "8.11.3", "acorn": "8.11.3",
"c12": "^1.10.0", "c12": "^1.10.0",
@ -76,7 +76,7 @@
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
"devalue": "^5.0.0", "devalue": "^5.0.0",
"esbuild": "^0.21.3", "esbuild": "^0.21.4",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
@ -107,7 +107,7 @@
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unenv": "^1.9.0", "unenv": "^1.9.0",
"unimport": "^3.7.1", "unimport": "^3.7.2",
"unplugin": "^1.10.1", "unplugin": "^1.10.1",
"unplugin-vue-router": "^0.7.0", "unplugin-vue-router": "^0.7.0",
"unstorage": "^1.10.2", "unstorage": "^1.10.2",
@ -118,13 +118,13 @@
"vue-router": "^4.3.2" "vue-router": "^4.3.2"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/ui-templates": "1.3.3", "@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1", "@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"unbuild": "latest", "unbuild": "latest",
"vite": "5.2.11", "vite": "5.2.12",
"vitest": "1.6.0" "vitest": "1.6.0"
}, },
"peerDependencies": { "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> <template>
<Suspense @resolve="onResolve"> <Suspense @resolve="onResolve">
<div v-if="abortRender" />
<ErrorComponent <ErrorComponent
v-if="error" v-else-if="error"
:error="error" :error="error"
/> />
<IslandRenderer <IslandRenderer
@ -53,6 +54,8 @@ if (import.meta.dev && results && results.some(i => i && 'then' in i)) {
// error handling // error handling
const error = useError() 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) => { onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError)) nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) { 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' import { onNuxtReady } from './ready'
// @ts-expect-error virtual file // @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' export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
@ -42,7 +45,7 @@ export interface AsyncDataOptions<
ResT, ResT,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = DefaultAsyncDataValue,
> { > {
/** /**
* Whether to fetch on the server side. * Whether to fetch on the server side.
@ -117,7 +120,7 @@ export interface _AsyncData<DataT, ErrorT> {
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void> refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void> execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
clear: () => void clear: () => void
error: Ref<ErrorT | null> error: Ref<ErrorT | DefaultAsyncDataErrorValue>
status: Ref<AsyncDataRequestStatus> status: Ref<AsyncDataRequestStatus>
} }
@ -138,11 +141,11 @@ export function useAsyncData<
NuxtErrorDataT = unknown, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = DefaultAsyncDataValue,
> ( > (
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> 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. * Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data} * See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -158,7 +161,7 @@ export function useAsyncData<
> ( > (
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> 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. * Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data} * See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -171,12 +174,12 @@ export function useAsyncData<
NuxtErrorDataT = unknown, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = DefaultAsyncDataValue,
> ( > (
key: string, key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> 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. * Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data} * See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -194,14 +197,14 @@ export function useAsyncData<
key: string, key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT> 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< export function useAsyncData<
ResT, ResT,
NuxtErrorDataT = unknown, NuxtErrorDataT = unknown,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = DefaultAsyncDataValue,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> { > (...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 const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) } if (typeof args[0] !== 'string') { args.unshift(autoKey) }
@ -226,14 +229,14 @@ export function useAsyncData<
const value = nuxtApp.ssrContext!._sharedPrerenderCache!.get(key) const value = nuxtApp.ssrContext!._sharedPrerenderCache!.get(key)
if (value) { return value as Promise<ResT> } 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) nuxtApp.ssrContext!._sharedPrerenderCache!.set(key, promise)
return promise return promise
} }
// Used to get default values // Used to get default values
const getDefault = () => null const getDefault = () => asyncDataDefaults.value
const getDefaultCachedData = () => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key] const getDefaultCachedData = () => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]
// Apply defaults // 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.') 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 const hasCachedData = () => options.getCachedData!(key, nuxtApp) != null
// Create or use a shared asyncData entity // Create or use a shared asyncData entity
if (!nuxtApp._asyncData[key] || !options.immediate) { if (!nuxtApp._asyncData[key] || !options.immediate) {
nuxtApp.payload._errors[key] ??= null nuxtApp.payload._errors[key] ??= asyncDataDefaults.errorValue
const _ref = options.deep ? ref : shallowRef const _ref = options.deep ? ref : shallowRef
@ -263,11 +267,15 @@ export function useAsyncData<
pending: ref(!hasCachedData()), pending: ref(!hasCachedData()),
error: toRef(nuxtApp.payload._errors, key), error: toRef(nuxtApp.payload._errors, key),
status: ref('idle'), status: ref('idle'),
_default: options.default!,
} }
} }
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher // 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 = {}) => { asyncData.refresh = asyncData.execute = (opts = {}) => {
if (nuxtApp._asyncDataPromises[key]) { if (nuxtApp._asyncDataPromises[key]) {
@ -307,7 +315,7 @@ export function useAsyncData<
nuxtApp.payload.data[key] = result nuxtApp.payload.data[key] = result
asyncData.data.value = result asyncData.data.value = result
asyncData.error.value = null asyncData.error.value = asyncDataDefaults.errorValue
asyncData.status.value = 'success' asyncData.status.value = 'success'
}) })
.catch((error: any) => { .catch((error: any) => {
@ -404,11 +412,11 @@ export function useLazyAsyncData<
DataE = Error, DataE = Error,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = DefaultAsyncDataValue,
> ( > (
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'> 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< export function useLazyAsyncData<
ResT, ResT,
DataE = Error, DataE = Error,
@ -418,18 +426,18 @@ export function useLazyAsyncData<
> ( > (
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'> 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< export function useLazyAsyncData<
ResT, ResT,
DataE = Error, DataE = Error,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = DefaultAsyncDataValue,
> ( > (
key: string, key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'> 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< export function useLazyAsyncData<
ResT, ResT,
DataE = Error, DataE = Error,
@ -440,15 +448,15 @@ export function useLazyAsyncData<
key: string, key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>, handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'> 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< export function useLazyAsyncData<
ResT, ResT,
DataE = Error, DataE = Error,
DataT = ResT, DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null, DefaultT = DefaultAsyncDataValue,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> { > (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue> {
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) } if (typeof args[0] !== 'string') { args.unshift(autoKey) }
const [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>] 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 */ /** @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() const nuxtApp = useNuxtApp()
// Initialize value when key is not already set // Initialize value when key is not already set
if (!(key in nuxtApp.payload.data)) { if (!(key in nuxtApp.payload.data)) {
nuxtApp.payload.data[key] = null nuxtApp.payload.data[key] = asyncDataDefaults.value
} }
return { return {
@ -520,12 +528,12 @@ function clearNuxtDataByKey (nuxtApp: NuxtApp, key: string): void {
} }
if (key in nuxtApp.payload._errors) { if (key in nuxtApp.payload._errors) {
nuxtApp.payload._errors[key] = null nuxtApp.payload._errors[key] = asyncDataDefaults.errorValue
} }
if (nuxtApp._asyncData[key]) { if (nuxtApp._asyncData[key]) {
nuxtApp._asyncData[key]!.data.value = undefined nuxtApp._asyncData[key]!.data.value = resetAsyncDataToUndefined ? undefined : nuxtApp._asyncData[key]!._default()
nuxtApp._asyncData[key]!.error.value = null nuxtApp._asyncData[key]!.error.value = asyncDataDefaults.errorValue
nuxtApp._asyncData[key]!.pending.value = false nuxtApp._asyncData[key]!.pending.value = false
nuxtApp._asyncData[key]!.status.value = 'idle' nuxtApp._asyncData[key]!.status.value = 'idle'
} }

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo' import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo'
import { parse } from 'devalue' import { parse } from 'devalue'
import { useHead } from '@unhead/vue' import { useHead } from '@unhead/vue'
import { getCurrentInstance } from 'vue' import { getCurrentInstance, onServerPrefetch } from 'vue'
import { useNuxtApp, useRuntimeConfig } from '../nuxt' import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import { useRoute } from './router' import { useRoute } from './router'
@ -16,9 +16,9 @@ interface LoadPayloadOptions {
} }
/** @since 3.0.0 */ /** @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 } if (import.meta.server || !payloadExtraction) { return null }
const payloadURL = _getPayloadURL(url, opts) const payloadURL = await _getPayloadURL(url, opts)
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {} const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
if (payloadURL in cache) { if (payloadURL in cache) {
@ -39,26 +39,34 @@ export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record
return cache[payloadURL] return cache[payloadURL]
} }
/** @since 3.0.0 */ /** @since 3.0.0 */
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) { export function preloadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<void> {
const payloadURL = _getPayloadURL(url, opts) const nuxtApp = useNuxtApp()
useHead({ const promise = _getPayloadURL(url, opts).then((payloadURL) => {
link: [ nuxtApp.runWithContext(() => useHead({
{ rel: 'modulepreload', href: payloadURL }, link: [
], { rel: 'modulepreload', href: payloadURL },
],
}))
}) })
if (import.meta.server) {
onServerPrefetch(() => promise)
}
return promise
} }
// --- Internal --- // --- Internal ---
const filename = renderJsonPayloads ? '_payload.json' : '_payload.js' 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') const u = new URL(url, 'http://localhost')
if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) { if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) {
throw new Error('Payload URL must not include hostname: ' + url) throw new Error('Payload URL must not include hostname: ' + url)
} }
const config = useRuntimeConfig() const config = useRuntimeConfig()
const hash = opts.hash || (opts.fresh ? Date.now() : config.app.buildId) 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) { 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 // @ts-expect-error virtual file
import { appId } from '#build/nuxt.config.mjs' 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' import type { NuxtAppLiterals } from '#app'
function getNuxtAppCtx (appName = appId || 'nuxt-app') { function getNuxtAppCtx (appName = appId || 'nuxt-app') {
@ -92,8 +94,8 @@ export interface NuxtPayload {
state: Record<string, any> state: Record<string, any>
once: Set<string> once: Set<string>
config?: Pick<RuntimeConfig, 'public' | 'app'> config?: Pick<RuntimeConfig, 'public' | 'app'>
error?: NuxtError | null error?: NuxtError | DefaultErrorValue
_errors: Record<string, NuxtError | null> _errors: Record<string, NuxtError | DefaultAsyncDataErrorValue>
[key: string]: unknown [key: string]: unknown
} }
@ -120,10 +122,12 @@ interface _NuxtApp {
_asyncDataPromises: Record<string, Promise<any> | undefined> _asyncDataPromises: Record<string, Promise<any> | undefined>
/** @internal */ /** @internal */
_asyncData: Record<string, { _asyncData: Record<string, {
data: Ref<any> data: Ref<unknown>
pending: Ref<boolean> pending: Ref<boolean>
error: Ref<Error | null> error: Ref<Error | DefaultAsyncDataErrorValue>
status: Ref<AsyncDataRequestStatus> status: Ref<AsyncDataRequestStatus>
/** @internal */
_default: () => unknown
} | undefined> } | undefined>
/** @internal */ /** @internal */

View File

@ -1,13 +1,21 @@
import { consola, createConsola } from 'consola' import { createConsola } from 'consola'
import type { LogObject } from 'consola' import type { LogObject } from 'consola'
import { parse } from 'devalue' import { parse } from 'devalue'
import { h } from 'vue'
import { defineNuxtPlugin } from '../nuxt' import { defineNuxtPlugin } from '../nuxt'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { devLogs, devRootDir } from '#build/nuxt.config.mjs' 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.test) { return }
if (import.meta.server) { if (import.meta.server) {
@ -23,42 +31,18 @@ export default defineNuxtPlugin((nuxtApp) => {
date: true, 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) => { nuxtApp.hook('dev:ssr-logs', (logs) => {
for (const log of logs) { for (const log of logs) {
// deduplicate so we don't print out things that are logged on client logger.log(normalizeServerLog({ ...log }))
try {
if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) {
logger.log(normalizeServerLog({ ...log }))
}
} catch {
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 if (typeof window !== 'undefined') {
nuxtApp.hooks.hook('app:suspense:resolve', async () => { const content = document.getElementById('__NUXT_LOGS__')?.textContent
if (typeof window !== 'undefined') { const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
const content = document.getElementById('__NUXT_LOGS__')?.textContent await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
const logs = content ? parse(content, nuxtApp._payloadRevivers) as LogObject[] : [] }
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
}
})
}) })
function normalizeFilenames (stack?: string) { function normalizeFilenames (stack?: string) {

View File

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

View File

@ -161,6 +161,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
'nuxt3/dist', 'nuxt3/dist',
'nuxt-nightly/dist', 'nuxt-nightly/dist',
distDir, 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: [ traceInclude: [
// force include files used in generated code from the runtime-compiler // 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 { withTrailingSlash, withoutLeadingSlash } from 'ufo'
import defu from 'defu' import defu from 'defu'
import { gt } from 'semver' import { gt, satisfies } from 'semver'
import pagesModule from '../pages/module' import pagesModule from '../pages/module'
import metaModule from '../head/module' import metaModule from '../head/module'
import componentsModule from '../components/module' import componentsModule from '../components/module'
@ -129,6 +129,8 @@ async function initNuxt (nuxt: Nuxt) {
if (nuxt.options.typescript.shim) { if (nuxt.options.typescript.shim) {
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/vue-shim.d.ts') }) 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 // 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/schema.d.ts') })
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app.config.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), ...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 // Init user modules
await nuxt.callHook('modules:before') await nuxt.callHook('modules:before')
const modulesToInstall = [] const modulesToInstall = []
@ -557,6 +562,12 @@ async function initNuxt (nuxt: Nuxt) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client')) 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) await nuxt.callHook('ready', nuxt)
} }

View File

@ -6,11 +6,17 @@ import type { H3Event } from 'h3'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import { getContext } from 'unctx' import { getContext } from 'unctx'
import { isVNode } from 'vue'
import type { NitroApp } from '#internal/nitro/app' import type { NitroApp } from '#internal/nitro/app'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { rootDir } from '#internal/dev-server-logs-options' 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 { interface NuxtDevAsyncContext {
logs: LogObject[] logs: LogObject[]
event: H3Event event: H3Event
@ -54,9 +60,10 @@ export default (nitroApp: NitroApp) => {
const ctx = asyncContext.tryUse() const ctx = asyncContext.tryUse()
if (!ctx) { return } if (!ctx) { return }
try { 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) { } 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 // Whether we are prerendering route
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland 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) { if (import.meta.prerender) {
ssrContext.payload.prerenderedAt = Date.now() 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' { declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { } interface ComponentCustomProperties extends NuxtAppInjections { }
} }
@ -266,10 +272,13 @@ export const useRuntimeConfig = () => window?.__NUXT__?.config || {}
export const appConfigDeclarationTemplate: NuxtTemplate = { export const appConfigDeclarationTemplate: NuxtTemplate = {
filename: 'types/app.config.d.ts', filename: 'types/app.config.d.ts',
getContents ({ app, nuxt }) { getContents ({ app, nuxt }) {
const typesDir = join(nuxt.options.buildDir, 'types')
const configPaths = app.configs.map(path => relative(typesDir, path).replace(/\b\.\w+$/g, ''))
return ` return `
import type { CustomAppConfig } from 'nuxt/schema' import type { CustomAppConfig } from 'nuxt/schema'
import type { Defu } from 'defu' 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)} 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(', ')}]> 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 devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
`export const devLogs = ${JSON.stringify(ctx.nuxt.options.features.devLogs)}`, `export const devLogs = ${JSON.stringify(ctx.nuxt.options.features.devLogs)}`,
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`, `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 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 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}`, `export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
@ -401,3 +416,29 @@ export const nuxtConfigTemplate: NuxtTemplate = {
].join('\n\n') ].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', 'useScriptGoogleMaps',
'useScriptNpm', 'useScriptNpm',
], ],
priority: -1,
from: '#app/composables/script-stubs', from: '#app/composables/script-stubs',
} satisfies InlinePreset } satisfies InlinePreset

View File

@ -422,11 +422,6 @@ export default defineNuxtModule({
getContents: () => 'export { START_LOCATION, useRoute } from \'vue-router\'', 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 = nuxt.options.vite.resolve || {}
nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || [] nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || []
nuxt.options.vite.resolve.dedupe.push('vue-router') nuxt.options.vite.resolve.dedupe.push('vue-router')

View File

@ -1,10 +1,13 @@
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { Nuxt } from '@nuxt/schema' import type { Nuxt } from '@nuxt/schema'
import { stripLiteral } from 'strip-literal'
import { isVue } from '../../core/utils' import { isVue } from '../../core/utils'
const INJECTION_RE = /\b_ctx\.\$route\b/g const INJECTION_RE_TEMPLATE = /\b_ctx\.\$route\b/g
const INJECTION_SINGLE_RE = /\b_ctx\.\$route\b/ 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(() => { export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
return { return {
@ -14,14 +17,30 @@ export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
return isVue(id, { type: ['template', 'script'] }) return isVue(id, { type: ['template', 'script'] })
}, },
transform (code) { 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 let replaced = false
const s = new MagicString(code) const s = new MagicString(code)
s.replace(INJECTION_RE, () => { const strippedCode = stripLiteral(code)
replaced = true
return '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)' // 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) { if (replaced) {
s.prepend('import { PageRouteSymbol as __nuxt_route_symbol } from \'#app/components/injections\';\n') 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": { "devDependencies": {
"@nuxt/telemetry": "2.5.4", "@nuxt/telemetry": "2.5.4",
"@nuxt/ui-templates": "1.3.3", "@nuxt/ui-templates": "1.3.4",
"@types/file-loader": "5.0.4", "@types/file-loader": "5.0.4",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8", "@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": "5.0.4",
"@vitejs/plugin-vue-jsx": "3.1.0", "@vitejs/plugin-vue-jsx": "3.1.0",
"@vue/compiler-core": "3.4.27", "@vue/compiler-core": "3.4.27",
@ -54,7 +54,7 @@
"unbuild": "latest", "unbuild": "latest",
"unctx": "2.3.1", "unctx": "2.3.1",
"unenv": "1.9.0", "unenv": "1.9.0",
"vite": "5.2.11", "vite": "5.2.12",
"vue": "3.4.27", "vue": "3.4.27",
"vue-bundle-renderer": "2.1.0", "vue-bundle-renderer": "2.1.0",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
@ -71,7 +71,7 @@
"scule": "^1.3.0", "scule": "^1.3.0",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"ufo": "^1.5.3", "ufo": "^1.5.3",
"unimport": "^3.7.1", "unimport": "^3.7.2",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"untyped": "^1.4.2" "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 * Boolean or a path to an HTML file with the contents of which will be inserted into any HTML page
* rendered with `ssr: false`. * 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 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 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 * - 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, * compileTemplate: true,
* templateUtils: true, * templateUtils: true,
* relativeWatchPaths: true, * relativeWatchPaths: true,
* resetAsyncDataToUndefined: true,
* defaults: { * defaults: {
* useAsyncData: { * useAsyncData: {
* deep: true * deep: true
@ -342,8 +343,10 @@ export default defineUntypedSchema({
/** /**
* Use new experimental head optimisations: * 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. * - 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 * - Uses the hash hydration plugin to reduce initial hydration
*
* @see [Nuxt Discussion #22632](https://github.com/nuxt/nuxt/discussions/22632] * @see [Nuxt Discussion #22632](https://github.com/nuxt/nuxt/discussions/22632]
*/ */
headNext: true, 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. * 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`) * Options that apply to `useAsyncData` (and also therefore `useFetch`)
*/ */
useAsyncData: { 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: { deep: {
async $resolve (val, get) { async $resolve (val, get) {
return val ?? !((await get('future') as Record<string, unknown>).compatibilityVersion === 4) 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) 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. * Nitro server handlers.
* *
* Each handler accepts the following options: * Each handler accepts the following options:
*
* - handler: The path to the file defining the handler. * - 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. * - 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. * - method: The HTTP method of requests that should be handled.
* - middleware: Specifies whether it is a middleware handler. * - middleware: Specifies whether it is a middleware handler.
* - lazy: Specifies whether to use lazy loading to import the handler. * - lazy: Specifies whether to use lazy loading to import the handler.
*
* @see https://nuxt.com/docs/guide/directory-structure/server * @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. * @note Files from `server/api`, `server/middleware` and `server/routes` will be automatically registered by Nuxt.
* @example * @example

View File

@ -157,7 +157,11 @@ export default defineUntypedSchema({
* See https://github.com/esbuild-kit/esbuild-loader * See https://github.com/esbuild-kit/esbuild-loader
* @type {Omit<typeof import('esbuild-loader')['LoaderOptions'], '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 * See: https://github.com/webpack-contrib/file-loader#options

View File

@ -1,5 +1,6 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs' import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
import { copyFile } from 'node:fs/promises'
import { basename, dirname, join, resolve } from 'pathe' import { basename, dirname, join, resolve } from 'pathe'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
// @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151 // @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151
@ -167,6 +168,15 @@ export const RenderPlugin = () => {
unlinkSync(fileName) unlinkSync(fileName)
rmdirSync(dirname(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", "prettier": "3.2.5",
"scule": "1.3.0", "scule": "1.3.0",
"unocss": "0.60.3", "unocss": "0.60.3",
"vite": "5.2.11" "vite": "5.2.12"
} }
} }

View File

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

View File

@ -63,6 +63,48 @@ export async function buildClient (ctx: ViteBuildContext) {
}, },
optimizeDeps: { optimizeDeps: {
entries: [ctx.entry], 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: { resolve: {
alias: { alias: {
@ -129,18 +171,22 @@ export async function buildClient (ctx: ViteBuildContext) {
}) as any }) as any
if (clientConfig.server && clientConfig.server.hmr !== false) { if (clientConfig.server && clientConfig.server.hmr !== false) {
const hmrPortDefault = 24678 // Vite's default HMR port const serverDefaults: Omit<ServerOptions, 'hmr'> & { hmr: Exclude<ServerOptions['hmr'], boolean> } = {
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,
hmr: { hmr: {
protocol: ctx.nuxt.options.devServer.https ? 'wss' : 'ws', 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 // 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 }) 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) { if (ctx.nuxt.options.dev) {
// Dev // Dev
const viteServer = await vite.createServer(clientConfig) const viteServer = await vite.createServer(clientConfig)

View File

@ -3,21 +3,13 @@ import { useNitro } from '@nuxt/kit'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { withLeadingSlash, withTrailingSlash } from 'ufo' import { withLeadingSlash, withTrailingSlash } from 'ufo'
import { dirname, relative } from 'pathe' import { dirname, relative } from 'pathe'
import MagicString from 'magic-string'
const PREFIX = 'virtual:public?' const PREFIX = 'virtual:public?'
const CSS_URL_RE = /url\((\/[^)]+)\)/g
export const VitePublicDirsPlugin = createUnplugin(() => { export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boolean }) => {
const nitro = useNitro() const { resolveFromPublicAssets } = useResolveFromPublicAssets()
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
}
}
}
return { return {
name: 'nuxt:vite-public-dir-resolution', 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) { for (const file in bundle) {
const chunk = bundle[file] const chunk = bundle[file]
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue } if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }
let css = chunk.source.toString() let css = chunk.source.toString()
let wasReplaced = false 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)) { if (resolveFromPublicAssets(url)) {
const relativeURL = relative(withLeadingSlash(dirname(file)), url) const relativeURL = relative(withLeadingSlash(dirname(file)), url)
css = css.replace(full, `url(${relativeURL})`) 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', 'XMLHttpRequest': 'undefined',
}, },
optimizeDeps: { optimizeDeps: {
entries: ctx.nuxt.options.ssr ? [ctx.entry] : [], noDiscovery: true,
}, },
resolve: { resolve: {
alias: { alias: {

View File

@ -3,6 +3,7 @@ import { logger } from '@nuxt/kit'
import { hasTTY, isCI } from 'std-env' import { hasTTY, isCI } from 'std-env'
import clear from 'clear' import clear from 'clear'
import type { NuxtOptions } from '@nuxt/schema' import type { NuxtOptions } from '@nuxt/schema'
import { useResolveFromPublicAssets } from '../plugins/public-dirs'
let duplicateCount = 0 let duplicateCount = 0
let lastType: vite.LogType | null = null 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 canClearScreen = hasTTY && !isCI && config.clearScreen
const clearScreen = canClearScreen ? clear : () => {} const clearScreen = canClearScreen ? clear : () => {}
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
function output (type: vite.LogType, msg: string, options: vite.LogErrorOptions = {}) { function output (type: vite.LogType, msg: string, options: vite.LogErrorOptions = {}) {
if (typeof msg === 'string' && !process.env.DEBUG) { if (typeof msg === 'string' && !process.env.DEBUG) {
// TODO: resolve upstream in Vite // TODO: resolve upstream in Vite
// Hide sourcemap warnings related to node_modules // Hide sourcemap warnings related to node_modules
if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { return } 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 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', 'abort-controller': 'unenv/runtime/mock/empty',
}, },
}, },
optimizeDeps: {
include: ['vue'],
exclude: ['nuxt/app'],
},
css: resolveCSSOptions(nuxt), css: resolveCSSOptions(nuxt),
define: { define: {
__NUXT_VERSION__: JSON.stringify(nuxt._version), __NUXT_VERSION__: JSON.stringify(nuxt._version),
@ -100,7 +96,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
}, },
plugins: [ plugins: [
// add resolver for files in public assets directories // add resolver for files in public assets directories
VitePublicDirsPlugin.vite(), VitePublicDirsPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server }),
composableKeysPlugin.vite({ composableKeysPlugin.vite({
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,

View File

@ -28,7 +28,7 @@
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0", "@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"css-loader": "^7.1.1", "css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0", "css-minimizer-webpack-plugin": "^7.0.0",
"cssnano": "^7.0.1", "cssnano": "^7.0.1",
"defu": "^6.1.4", "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 // TODO: dynamic paths in dev
describe.skipIf(isDev())('dynamic paths', () => { 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 () => { it('should work with no overrides', async () => {
const html: string = await $fetch<string>('/assets') const html: string = await $fetch<string>('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] 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 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)) const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u))
expect(cssURL).toBeDefined() expect(cssURL).toBeDefined()
const css: string = await $fetch<string>(cssURL!) const css = await $fetch<string>(cssURL!)
const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.')) const imageUrls = new Set(Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.')))
expect(imageUrls).toMatchInlineSnapshot(` expect([...imageUrls]).toMatchInlineSnapshot(`
[ [
"./logo.svg", "./logo.svg",
"../public.svg", "../public.svg",
"../public.svg", ]
"../public.svg", `)
]
`)
}) })
it('should allow setting base URL and build assets directory', async () => { 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') const html = await $fetch<string>('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(url.startsWith('/foo/_other/') || isPublicFile('/foo/', url)).toBeTruthy()
url.startsWith('/foo/_other/') ||
url === '/foo/public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
} }
expect(await $fetch<string>('/foo/url')).toContain('path: /foo/url') 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') const html = await $fetch<string>('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(url.startsWith('./_nuxt/') || isPublicFile('./', url)).toBeTruthy()
url.startsWith('./_nuxt/') ||
url === './public.svg' ||
// TODO: webpack does not yet support dynamic static paths
(isWebpack && url === '/public.svg'),
).toBeTruthy()
expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy() expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy()
} }
}) })
@ -2007,12 +2008,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
const html = await $fetch<string>('/foo/assets') const html = await $fetch<string>('/foo/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) { for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
const url = match[2] || match[3] const url = match[2] || match[3]
expect( expect(url.startsWith('https://example.com/_cdn/') || isPublicFile('https://example.com/', url)).toBeTruthy()
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()
} }
}) })
@ -2433,21 +2429,23 @@ describe.skipIf(isWindows)('useAsyncData', () => {
}) })
it('data is null after navigation when immediate false', async () => { it('data is null after navigation when immediate false', async () => {
const defaultValue = isV4 ? 'undefined' : 'null'
const { page } = await renderPage('/useAsyncData/immediate-remove-unmounted') 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') 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.click('#to-index')
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/')
await page.click('#to-immediate-remove-unmounted') await page.click('#to-immediate-remove-unmounted')
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/useAsyncData/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') 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() 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 { NuxtLayout, NuxtLink, NuxtPage, ServerComponent, WithTypes } from '#components'
import { useRouter } from '#imports' import { useRouter } from '#imports'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
interface TestResponse { message: string } interface TestResponse { message: string }
describe('API routes', () => { describe('API routes', () => {
@ -31,61 +34,61 @@ describe('API routes', () => {
}) })
it('works with useAsyncData', () => { it('works with useAsyncData', () => {
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | 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 } | null>>() 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 } | null>>() 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 } | null>>() 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' } | null>>() 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('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('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> | null>>() expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
// backwards compatibility // backwards compatibility
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | 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> | null>>() 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-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>() 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 } | null>>() 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 } | null>>() 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' } | null>>() 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('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('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
}) })
it('works with useFetch', () => { it('works with useFetch', () => {
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>() 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 } | null>>() 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 } | null>>() 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' } | null>>() expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>() expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
// @ts-expect-error not a valid method // @ts-expect-error not a valid method
useFetch('/api/hey', { method: 'PATCH' }) useFetch('/api/hey', { method: 'PATCH' })
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | 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 } | null>>() 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' } | null>>() expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>() expectTypeOf(useFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>() expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | null>>() expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>() expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>() expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>() 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 } | null>>() 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' } | null>>() expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>() 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('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>() 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>>() expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { default: () => 'test' }).data).toEqualTypeOf<Ref<string>>()
// transform must match the explicit generic because of typescript limitations microsoft/TypeScript#14400 // 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(useFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>() 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(useFetch<string>('/test', { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useLazyFetch<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 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', () => { it('infer request url string literal from server/api routes', () => {
@ -448,8 +451,8 @@ describe('composables', () => {
expectTypeOf(useFetch(dynamicStringUrl).data).toEqualTypeOf<Ref<unknown>>() 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' // 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(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
// request can accept string literal and Request object type // request can accept string literal and Request object type
expectTypeOf(useFetch('https://example.com/api').data).toEqualTypeOf<Ref<unknown>>() expectTypeOf(useFetch('https://example.com/api').data).toEqualTypeOf<Ref<unknown>>()
@ -519,7 +522,7 @@ describe('composables', () => {
it('correctly types returns when using with getCachedData', () => { it('correctly types returns when using with getCachedData', () => {
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: 1 }), { expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
getCachedData: key => useNuxtApp().payload.data[key], getCachedData: key => useNuxtApp().payload.data[key],
}).data).toEqualTypeOf<Ref<{ foo: number } | null>>() }).data).toEqualTypeOf<Ref<{ foo: number } | DefaultAsyncDataValue>>()
useAsyncData('test', () => Promise.resolve({ foo: 1 }), { useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
// @ts-expect-error cached data should return the same as value of fetcher // @ts-expect-error cached data should return the same as value of fetcher
getCachedData: () => ({ bar: 2 }), getCachedData: () => ({ bar: 2 }),

View File

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

View File

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

View File

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

View File

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