mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
b30c465a57
2
.github/workflows/check-links.yml
vendored
2
.github/workflows/check-links.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Lychee link checker
|
||||
uses: lycheeverse/lychee-action@054a8e8c7a88ada133165c6633a49825a32174e2 # for v1.8.0
|
||||
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0
|
||||
with:
|
||||
# arguments with file types to check
|
||||
args: >-
|
||||
|
2
.github/workflows/release-pr.yml
vendored
2
.github/workflows/release-pr.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/merge
|
||||
ref: ${{ github.event.issue.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- run: corepack enable
|
||||
|
@ -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
|
||||
|
||||
|
@ -48,6 +48,7 @@ export default defineNuxtConfig({
|
||||
// experimental: {
|
||||
// sharedPrerenderData: false,
|
||||
// compileTemplate: true,
|
||||
// resetAsyncDataToUndefined: true,
|
||||
// templateUtils: true,
|
||||
// relativeWatchPaths: true,
|
||||
// defaults: {
|
||||
@ -189,6 +190,63 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
#### Default `data` and `error` values in `useAsyncData` and `useFetch`
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
||||
##### What Changed
|
||||
|
||||
`data` and `error` objects returned from `useAsyncData` will now default to `undefined`.
|
||||
|
||||
##### Reasons for Change
|
||||
|
||||
Previously `data` was initialized to `null` but reset in `clearNuxtData` to `undefined`. `error` was initialized to `null`. This change is to bring greater consistency.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
If you encounter any issues you can revert back to the previous behavior with:
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
defaults: {
|
||||
useAsyncData: {
|
||||
value: 'null',
|
||||
errorValue: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Please report an issue if you are doing this, as we do not plan to keep this as configurable.
|
||||
|
||||
#### Respect defaults when clearing `data` in `useAsyncData` and `useFetch`
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
||||
##### What Changed
|
||||
|
||||
If you provide a custom `default` value for `useAsyncData`, this will now be used when calling `clear` or `clearNuxtData` and it will be reset to its default value rather than simply unset.
|
||||
|
||||
##### Reasons for Change
|
||||
|
||||
Often users set an appropriately empty value, such as an empty array, to avoid the need to check for `null`/`undefined` when iterating over it. This should be respected when resetting/clearing the data.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
If you encounter any issues you can revert back to the previous behavior, for now, with:
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
resetAsyncDataToUndefined: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Please report an issue if you are doing so, as we do not plan to keep this as configurable.
|
||||
|
||||
#### Shallow Data Reactivity in `useAsyncData` and `useFetch`
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
@ -259,7 +259,7 @@ Watch Learn Vue video about Nuxt Server Components.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-article-duotone" to="https://roe.dev/blog/nuxt-server-components" target="_blank"}
|
||||
Read Daniel Roe's guide to Nuxt server components
|
||||
Read Daniel Roe's guide to Nuxt Server Components.
|
||||
::
|
||||
|
||||
### Standalone server components
|
||||
|
@ -59,13 +59,10 @@ Use these options to set the expiration of the cookie.
|
||||
The given number will be converted to an integer by rounding down. By default, no maximum age is set.
|
||||
|
||||
`expires`: Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1).
|
||||
By default, no expiration is set. Most clients will consider this a "non-persistent cookie" and
|
||||
will delete it on a condition like exiting a web browser application.
|
||||
By default, no expiration is set. Most clients will consider this a "non-persistent cookie" and will delete it on a condition like exiting a web browser application.
|
||||
|
||||
::note
|
||||
The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
|
||||
`maxAge` is set, then `maxAge` takes precedence, but not all clients may obey this,
|
||||
so if both are set, they should point to the same date and time!
|
||||
The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` is set, then `maxAge` takes precedence, but not all clients may obey this, so if both are set, they should point to the same date and time!
|
||||
::
|
||||
|
||||
::note
|
||||
@ -74,22 +71,29 @@ If neither of `expires` and `maxAge` is set, the cookie will be session-only and
|
||||
|
||||
### `httpOnly`
|
||||
|
||||
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy,
|
||||
the `HttpOnly` attribute is set; otherwise it is not. By default, the `HttpOnly` attribute is not set.
|
||||
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy, the `HttpOnly` attribute is set; otherwise it is not. By default, the `HttpOnly` attribute is not set.
|
||||
|
||||
::warning
|
||||
Be careful when setting this to `true`, as compliant clients will not allow client-side
|
||||
JavaScript to see the cookie in `document.cookie`.
|
||||
Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
|
||||
::
|
||||
|
||||
### `secure`
|
||||
|
||||
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy,
|
||||
the `Secure` attribute is set; otherwise it is not. By default, the `Secure` attribute is not set.
|
||||
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy, the `Secure` attribute is set; otherwise it is not. By default, the `Secure` attribute is not set.
|
||||
|
||||
::warning
|
||||
Be careful when setting this to `true`, as compliant clients will not send the cookie back to
|
||||
the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
|
||||
Be careful when setting this to `true`, as compliant clients will not send the cookie back to the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
|
||||
::
|
||||
|
||||
### `partitioned`
|
||||
|
||||
Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1) attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the `Partitioned` attribute is not set.
|
||||
|
||||
::note
|
||||
This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
This also means many clients may ignore this attribute until they understand it.
|
||||
|
||||
More information can be found in the [proposal](https://github.com/privacycg/CHIPS).
|
||||
::
|
||||
|
||||
### `domain`
|
||||
@ -114,23 +118,18 @@ More information about the different enforcement levels can be found in [the spe
|
||||
|
||||
### `encode`
|
||||
|
||||
Specifies a function that will be used to encode a cookie's value. Since the value of a cookie
|
||||
has a limited character set (and must be a simple string), this function can be used to encode
|
||||
a value into a string suited for a cookie's value.
|
||||
Specifies a function that will be used to encode a cookie's value. Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to encode a value into a string suited for a cookie's value.
|
||||
|
||||
The default encoder is the `JSON.stringify` + `encodeURIComponent`.
|
||||
|
||||
### `decode`
|
||||
|
||||
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
|
||||
has a limited character set (and must be a simple string), this function can be used to decode
|
||||
a previously encoded cookie value into a JavaScript string or other object.
|
||||
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to decode a previously encoded cookie value into a JavaScript string or other object.
|
||||
|
||||
The default decoder is `decodeURIComponent` + [destr](https://github.com/unjs/destr).
|
||||
|
||||
::note
|
||||
If an error is thrown from this function, the original, non-decoded cookie value will
|
||||
be returned as the cookie's value.
|
||||
If an error is thrown from this function, the original, non-decoded cookie value will be returned as the cookie's value.
|
||||
::
|
||||
|
||||
### `default`
|
||||
|
@ -32,6 +32,10 @@ We'll do our best to follow our [internal issue decision making flowchart](https
|
||||
|
||||
### Send a Pull Request
|
||||
|
||||
::Tip
|
||||
On windows, you need to clone the repository with `git clone -c core.symlinks=true https://github.com/nuxt/nuxt.git` to make symlinks work.
|
||||
::
|
||||
|
||||
We always welcome pull requests! ❤️
|
||||
|
||||
#### Before You Start
|
||||
|
@ -64,9 +64,9 @@ Each active version has its own nightly releases which are generated automatical
|
||||
Release | | Initial release | End Of Life | Docs
|
||||
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
|
||||
**4.x** (scheduled) | | 2024 Q2 | |
|
||||
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label="></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
|
||||
**2.x** (maintenance) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label="></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
|
||||
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label="></a> | 2018-01-08 | 2019-09-21 |
|
||||
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label=" class="not-prose"></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
|
||||
**2.x** (maintenance) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label=" class="not-prose"></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
|
||||
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label=" class="not-prose"></a> | 2018-01-08 | 2019-09-21 |
|
||||
|
||||
### Support Status
|
||||
|
||||
|
15
package.json
15
package.json
@ -20,6 +20,7 @@
|
||||
"lint:knip": "pnpx knip",
|
||||
"play": "nuxi dev playground",
|
||||
"play:build": "nuxi build playground",
|
||||
"play:generate": "nuxi generate playground",
|
||||
"play:preview": "nuxi preview playground",
|
||||
"test": "pnpm test:fixtures && pnpm test:fixtures:dev && pnpm test:fixtures:webpack && pnpm test:unit && pnpm test:runtime && pnpm test:types && pnpm typecheck",
|
||||
"test:prepare": "jiti ./test/prepare.ts",
|
||||
@ -40,8 +41,8 @@
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"magic-string": "^0.30.10",
|
||||
"nuxt": "workspace:*",
|
||||
"rollup": "^4.17.2",
|
||||
"vite": "5.2.11",
|
||||
"rollup": "^4.18.0",
|
||||
"vite": "5.2.12",
|
||||
"vue": "3.4.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -53,7 +54,7 @@
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/node": "20.12.13",
|
||||
"@types/semver": "7.5.8",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
@ -69,16 +70,16 @@
|
||||
"fs-extra": "11.2.0",
|
||||
"globby": "14.0.1",
|
||||
"h3": "1.11.1",
|
||||
"happy-dom": "14.11.0",
|
||||
"happy-dom": "14.12.0",
|
||||
"jiti": "1.21.0",
|
||||
"markdownlint-cli": "0.40.0",
|
||||
"markdownlint-cli": "0.41.0",
|
||||
"nitropack": "2.9.6",
|
||||
"nuxi": "3.11.1",
|
||||
"nuxt": "workspace:*",
|
||||
"nuxt-content-twoslash": "0.0.10",
|
||||
"ofetch": "1.3.4",
|
||||
"pathe": "1.1.2",
|
||||
"playwright-core": "1.44.0",
|
||||
"playwright-core": "1.44.1",
|
||||
"rimraf": "5.0.7",
|
||||
"semver": "7.6.2",
|
||||
"std-env": "3.7.0",
|
||||
@ -90,7 +91,7 @@
|
||||
"vue-router": "4.3.2",
|
||||
"vue-tsc": "2.0.19"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.1",
|
||||
"packageManager": "pnpm@9.1.3",
|
||||
"engines": {
|
||||
"node": "^16.10.0 || >=18.0.0"
|
||||
},
|
||||
|
@ -44,7 +44,7 @@
|
||||
"semver": "^7.6.2",
|
||||
"ufo": "^1.5.3",
|
||||
"unctx": "^2.3.1",
|
||||
"unimport": "^3.7.1",
|
||||
"unimport": "^3.7.2",
|
||||
"untyped": "^1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -54,7 +54,7 @@
|
||||
"lodash-es": "4.17.21",
|
||||
"nitropack": "2.9.6",
|
||||
"unbuild": "latest",
|
||||
"vite": "5.2.11",
|
||||
"vite": "5.2.12",
|
||||
"vitest": "1.6.0",
|
||||
"webpack": "5.91.0"
|
||||
},
|
||||
|
@ -73,8 +73,8 @@ export function defineNuxtModule<OptionsT extends ModuleOptions> (definition: Mo
|
||||
const key = `nuxt:module:${uniqueKey || (Math.round(Math.random() * 10000))}`
|
||||
const mark = performance.mark(key)
|
||||
const res = await module.setup?.call(null as any, _options, nuxt) ?? {}
|
||||
const perf = performance.measure(key, mark?.name) // TODO: remove when Node 14 reaches EOL
|
||||
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
|
||||
const perf = performance.measure(key, mark.name)
|
||||
const setupTime = Math.round((perf.duration * 100)) / 100
|
||||
|
||||
// Measure setup time
|
||||
if (setupTime > 5000 && uniqueKey !== '@nuxt/telemetry') {
|
||||
|
6
packages/nuxt/.gitignore
vendored
Normal file
6
packages/nuxt/.gitignore
vendored
Normal 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
|
@ -60,14 +60,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/devalue": "^2.0.2",
|
||||
"@nuxt/devtools": "^1.3.1",
|
||||
"@nuxt/devtools": "^1.3.2",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/telemetry": "^2.5.4",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@unhead/dom": "^1.9.10",
|
||||
"@unhead/ssr": "^1.9.10",
|
||||
"@unhead/vue": "^1.9.10",
|
||||
"@unhead/dom": "^1.9.11",
|
||||
"@unhead/ssr": "^1.9.11",
|
||||
"@unhead/vue": "^1.9.11",
|
||||
"@vue/shared": "^3.4.27",
|
||||
"acorn": "8.11.3",
|
||||
"c12": "^1.10.0",
|
||||
@ -76,7 +76,7 @@
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3",
|
||||
"devalue": "^5.0.0",
|
||||
"esbuild": "^0.21.3",
|
||||
"esbuild": "^0.21.4",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
@ -107,7 +107,7 @@
|
||||
"uncrypto": "^0.1.3",
|
||||
"unctx": "^2.3.1",
|
||||
"unenv": "^1.9.0",
|
||||
"unimport": "^3.7.1",
|
||||
"unimport": "^3.7.2",
|
||||
"unplugin": "^1.10.1",
|
||||
"unplugin-vue-router": "^0.7.0",
|
||||
"unstorage": "^1.10.2",
|
||||
@ -118,13 +118,13 @@
|
||||
"vue-router": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/ui-templates": "1.3.3",
|
||||
"@nuxt/ui-templates": "1.3.4",
|
||||
"@parcel/watcher": "2.4.1",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"unbuild": "latest",
|
||||
"vite": "5.2.11",
|
||||
"vite": "5.2.12",
|
||||
"vitest": "1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -1 +0,0 @@
|
||||
../../../../ui-templates/dist/templates/error-404.vue
|
@ -1 +0,0 @@
|
||||
../../../../ui-templates/dist/templates/error-500.vue
|
@ -1 +0,0 @@
|
||||
../../../../ui-templates/dist/templates/error-dev.vue
|
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<Suspense @resolve="onResolve">
|
||||
<div v-if="abortRender" />
|
||||
<ErrorComponent
|
||||
v-if="error"
|
||||
v-else-if="error"
|
||||
:error="error"
|
||||
/>
|
||||
<IslandRenderer
|
||||
@ -53,6 +54,8 @@ if (import.meta.dev && results && results.some(i => i && 'then' in i)) {
|
||||
|
||||
// error handling
|
||||
const error = useError()
|
||||
// render an empty <div> when plugins have thrown an error but we're not yet rendering the error page
|
||||
const abortRender = import.meta.server && error.value && !nuxtApp.ssrContext.error
|
||||
onErrorCaptured((err, target, info) => {
|
||||
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
|
||||
if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
|
||||
|
@ -1 +0,0 @@
|
||||
../../../../ui-templates/dist/templates/welcome.vue
|
@ -8,7 +8,10 @@ import { createError } from './error'
|
||||
import { onNuxtReady } from './ready'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { asyncDataDefaults } from '#build/nuxt.config.mjs'
|
||||
import { asyncDataDefaults, resetAsyncDataToUndefined } from '#build/nuxt.config.mjs'
|
||||
|
||||
// TODO: temporary module for backwards compatibility
|
||||
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
|
||||
|
||||
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
|
||||
|
||||
@ -42,7 +45,7 @@ export interface AsyncDataOptions<
|
||||
ResT,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> {
|
||||
/**
|
||||
* Whether to fetch on the server side.
|
||||
@ -117,7 +120,7 @@ export interface _AsyncData<DataT, ErrorT> {
|
||||
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
||||
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
||||
clear: () => void
|
||||
error: Ref<ErrorT | null>
|
||||
error: Ref<ErrorT | DefaultAsyncDataErrorValue>
|
||||
status: Ref<AsyncDataRequestStatus>
|
||||
}
|
||||
|
||||
@ -138,11 +141,11 @@ export function useAsyncData<
|
||||
NuxtErrorDataT = unknown,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
|
||||
@ -158,7 +161,7 @@ export function useAsyncData<
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
|
||||
@ -171,12 +174,12 @@ export function useAsyncData<
|
||||
NuxtErrorDataT = unknown,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
|
||||
@ -194,14 +197,14 @@ export function useAsyncData<
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
export function useAsyncData<
|
||||
ResT,
|
||||
NuxtErrorDataT = unknown,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> {
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue> {
|
||||
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||
|
||||
@ -226,14 +229,14 @@ export function useAsyncData<
|
||||
const value = nuxtApp.ssrContext!._sharedPrerenderCache!.get(key)
|
||||
if (value) { return value as Promise<ResT> }
|
||||
|
||||
const promise = nuxtApp.runWithContext(_handler)
|
||||
const promise = Promise.resolve().then(() => nuxtApp.runWithContext(_handler))
|
||||
|
||||
nuxtApp.ssrContext!._sharedPrerenderCache!.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
// Used to get default values
|
||||
const getDefault = () => null
|
||||
const getDefault = () => asyncDataDefaults.value
|
||||
const getDefaultCachedData = () => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]
|
||||
|
||||
// Apply defaults
|
||||
@ -250,11 +253,12 @@ export function useAsyncData<
|
||||
console.warn('[nuxt] `boolean` values are deprecated for the `dedupe` option of `useAsyncData` and will be removed in the future. Use \'cancel\' or \'defer\' instead.')
|
||||
}
|
||||
|
||||
// TODO: make more precise when v4 lands
|
||||
const hasCachedData = () => options.getCachedData!(key, nuxtApp) != null
|
||||
|
||||
// Create or use a shared asyncData entity
|
||||
if (!nuxtApp._asyncData[key] || !options.immediate) {
|
||||
nuxtApp.payload._errors[key] ??= null
|
||||
nuxtApp.payload._errors[key] ??= asyncDataDefaults.errorValue
|
||||
|
||||
const _ref = options.deep ? ref : shallowRef
|
||||
|
||||
@ -263,11 +267,15 @@ export function useAsyncData<
|
||||
pending: ref(!hasCachedData()),
|
||||
error: toRef(nuxtApp.payload._errors, key),
|
||||
status: ref('idle'),
|
||||
_default: options.default!,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher
|
||||
const asyncData = { ...nuxtApp._asyncData[key] } as AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
|
||||
const asyncData = { ...nuxtApp._asyncData[key] } as { _default?: unknown } & AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
|
||||
|
||||
// Don't expose default function to end user
|
||||
delete asyncData._default
|
||||
|
||||
asyncData.refresh = asyncData.execute = (opts = {}) => {
|
||||
if (nuxtApp._asyncDataPromises[key]) {
|
||||
@ -307,7 +315,7 @@ export function useAsyncData<
|
||||
nuxtApp.payload.data[key] = result
|
||||
|
||||
asyncData.data.value = result
|
||||
asyncData.error.value = null
|
||||
asyncData.error.value = asyncDataDefaults.errorValue
|
||||
asyncData.status.value = 'success'
|
||||
})
|
||||
.catch((error: any) => {
|
||||
@ -404,11 +412,11 @@ export function useLazyAsyncData<
|
||||
DataE = Error,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
@ -418,18 +426,18 @@ export function useLazyAsyncData<
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
@ -440,15 +448,15 @@ export function useLazyAsyncData<
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> {
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue> {
|
||||
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||
const [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
|
||||
@ -463,12 +471,12 @@ export function useLazyAsyncData<
|
||||
}
|
||||
|
||||
/** @since 3.1.0 */
|
||||
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | null> } {
|
||||
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | DefaultAsyncDataValue> } {
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
// Initialize value when key is not already set
|
||||
if (!(key in nuxtApp.payload.data)) {
|
||||
nuxtApp.payload.data[key] = null
|
||||
nuxtApp.payload.data[key] = asyncDataDefaults.value
|
||||
}
|
||||
|
||||
return {
|
||||
@ -520,12 +528,12 @@ function clearNuxtDataByKey (nuxtApp: NuxtApp, key: string): void {
|
||||
}
|
||||
|
||||
if (key in nuxtApp.payload._errors) {
|
||||
nuxtApp.payload._errors[key] = null
|
||||
nuxtApp.payload._errors[key] = asyncDataDefaults.errorValue
|
||||
}
|
||||
|
||||
if (nuxtApp._asyncData[key]) {
|
||||
nuxtApp._asyncData[key]!.data.value = undefined
|
||||
nuxtApp._asyncData[key]!.error.value = null
|
||||
nuxtApp._asyncData[key]!.data.value = resetAsyncDataToUndefined ? undefined : nuxtApp._asyncData[key]!._default()
|
||||
nuxtApp._asyncData[key]!.error.value = asyncDataDefaults.errorValue
|
||||
nuxtApp._asyncData[key]!.pending.value = false
|
||||
nuxtApp._asyncData[key]!.status.value = 'idle'
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import { toRef } from 'vue'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { useRouter } from './router'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
|
||||
|
||||
export const NUXT_ERROR_SIGNATURE = '__nuxt_error'
|
||||
|
||||
/** @since 3.0.0 */
|
||||
@ -47,7 +50,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
|
||||
await useRouter().replace(options.redirect)
|
||||
}
|
||||
|
||||
error.value = null
|
||||
error.value = nuxtDefaultErrorValue
|
||||
}
|
||||
|
||||
/** @since 3.0.0 */
|
||||
|
@ -8,6 +8,9 @@ import { useRequestFetch } from './ssr'
|
||||
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
|
||||
import { useAsyncData } from './asyncData'
|
||||
|
||||
// TODO: temporary module for backwards compatibility
|
||||
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { fetchDefaults } from '#build/nuxt.config.mjs'
|
||||
|
||||
@ -30,7 +33,7 @@ export interface UseFetchOptions<
|
||||
ResT,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
R extends NitroFetchRequest = string & {},
|
||||
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>,
|
||||
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> {
|
||||
@ -54,11 +57,11 @@ export function useFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Fetch data from an API endpoint with an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-fetch}
|
||||
@ -77,7 +80,7 @@ export function useFetch<
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
export function useFetch<
|
||||
ResT = void,
|
||||
ErrorT = FetchError,
|
||||
@ -86,7 +89,7 @@ export function useFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
|
||||
@ -161,8 +164,10 @@ export function useFetch<
|
||||
* @see https://github.com/unjs/ofetch/blob/bb2d72baa5d3f332a2185c20fc04e35d2c3e258d/src/fetch.ts#L152
|
||||
*/
|
||||
const timeoutLength = toValue(opts.timeout)
|
||||
let timeoutId: NodeJS.Timeout
|
||||
if (timeoutLength) {
|
||||
setTimeout(() => controller.abort(), timeoutLength)
|
||||
timeoutId = setTimeout(() => controller.abort(), timeoutLength)
|
||||
controller.signal.onabort = () => clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
let _$fetch = opts.$fetch || globalThis.$fetch
|
||||
@ -175,7 +180,7 @@ export function useFetch<
|
||||
}
|
||||
}
|
||||
|
||||
return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any) as Promise<_ResT>
|
||||
return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any).finally(() => { clearTimeout(timeoutId) }) as Promise<_ResT>
|
||||
}, _asyncDataOptions)
|
||||
|
||||
return asyncData
|
||||
@ -190,11 +195,11 @@ export function useLazyFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
export function useLazyFetch<
|
||||
ResT = void,
|
||||
ErrorT = FetchError,
|
||||
@ -207,7 +212,7 @@ export function useLazyFetch<
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
export function useLazyFetch<
|
||||
ResT = void,
|
||||
ErrorT = FetchError,
|
||||
@ -216,7 +221,7 @@ export function useLazyFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo'
|
||||
import { parse } from 'devalue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import { getCurrentInstance, onServerPrefetch } from 'vue'
|
||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||
|
||||
import { useRoute } from './router'
|
||||
@ -16,9 +16,9 @@ interface LoadPayloadOptions {
|
||||
}
|
||||
|
||||
/** @since 3.0.0 */
|
||||
export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record<string, any> | Promise<Record<string, any>> | null {
|
||||
export async function loadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<Record<string, any> | null> {
|
||||
if (import.meta.server || !payloadExtraction) { return null }
|
||||
const payloadURL = _getPayloadURL(url, opts)
|
||||
const payloadURL = await _getPayloadURL(url, opts)
|
||||
const nuxtApp = useNuxtApp()
|
||||
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
|
||||
if (payloadURL in cache) {
|
||||
@ -39,26 +39,34 @@ export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record
|
||||
return cache[payloadURL]
|
||||
}
|
||||
/** @since 3.0.0 */
|
||||
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) {
|
||||
const payloadURL = _getPayloadURL(url, opts)
|
||||
useHead({
|
||||
link: [
|
||||
{ rel: 'modulepreload', href: payloadURL },
|
||||
],
|
||||
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<void> {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const promise = _getPayloadURL(url, opts).then((payloadURL) => {
|
||||
nuxtApp.runWithContext(() => useHead({
|
||||
link: [
|
||||
{ rel: 'modulepreload', href: payloadURL },
|
||||
],
|
||||
}))
|
||||
})
|
||||
if (import.meta.server) {
|
||||
onServerPrefetch(() => promise)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
// --- Internal ---
|
||||
|
||||
const filename = renderJsonPayloads ? '_payload.json' : '_payload.js'
|
||||
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
||||
async function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
||||
const u = new URL(url, 'http://localhost')
|
||||
if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) {
|
||||
throw new Error('Payload URL must not include hostname: ' + url)
|
||||
}
|
||||
const config = useRuntimeConfig()
|
||||
const hash = opts.hash || (opts.fresh ? Date.now() : config.app.buildId)
|
||||
return joinURL(config.app.baseURL, u.pathname, filename + (hash ? `?${hash}` : ''))
|
||||
const cdnURL = config.app.cdnURL
|
||||
const baseOrCdnURL = cdnURL && await isPrerendered(url) ? cdnURL : config.app.baseURL
|
||||
return joinURL(baseOrCdnURL, u.pathname, filename + (hash ? `?${hash}` : ''))
|
||||
}
|
||||
|
||||
async function _importPayload (payloadURL: string) {
|
||||
|
7
packages/nuxt/src/app/defaults.ts
Normal file
7
packages/nuxt/src/app/defaults.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// TODO: temporary module for backwards compatibility
|
||||
|
||||
export type DefaultAsyncDataErrorValue = null
|
||||
export type DefaultAsyncDataValue = null
|
||||
export type DefaultErrorValue = null
|
||||
|
||||
export {}
|
@ -23,6 +23,8 @@ import type { ViewTransition } from './plugins/view-transitions.client'
|
||||
// @ts-expect-error virtual file
|
||||
import { appId } from '#build/nuxt.config.mjs'
|
||||
|
||||
// TODO: temporary module for backwards compatibility
|
||||
import type { DefaultAsyncDataErrorValue, DefaultErrorValue } from '#app/defaults'
|
||||
import type { NuxtAppLiterals } from '#app'
|
||||
|
||||
function getNuxtAppCtx (appName = appId || 'nuxt-app') {
|
||||
@ -92,8 +94,8 @@ export interface NuxtPayload {
|
||||
state: Record<string, any>
|
||||
once: Set<string>
|
||||
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
||||
error?: NuxtError | null
|
||||
_errors: Record<string, NuxtError | null>
|
||||
error?: NuxtError | DefaultErrorValue
|
||||
_errors: Record<string, NuxtError | DefaultAsyncDataErrorValue>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@ -120,10 +122,12 @@ interface _NuxtApp {
|
||||
_asyncDataPromises: Record<string, Promise<any> | undefined>
|
||||
/** @internal */
|
||||
_asyncData: Record<string, {
|
||||
data: Ref<any>
|
||||
data: Ref<unknown>
|
||||
pending: Ref<boolean>
|
||||
error: Ref<Error | null>
|
||||
error: Ref<Error | DefaultAsyncDataErrorValue>
|
||||
status: Ref<AsyncDataRequestStatus>
|
||||
/** @internal */
|
||||
_default: () => unknown
|
||||
} | undefined>
|
||||
|
||||
/** @internal */
|
||||
|
@ -1,13 +1,21 @@
|
||||
import { consola, createConsola } from 'consola'
|
||||
import { createConsola } from 'consola'
|
||||
import type { LogObject } from 'consola'
|
||||
import { parse } from 'devalue'
|
||||
|
||||
import { h } from 'vue'
|
||||
import { defineNuxtPlugin } from '../nuxt'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { devLogs, devRootDir } from '#build/nuxt.config.mjs'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const devRevivers: Record<string, (data: any) => any> = import.meta.server
|
||||
? {}
|
||||
: {
|
||||
VNode: data => h(data.type, data.props),
|
||||
URL: data => new URL(data),
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
if (import.meta.test) { return }
|
||||
|
||||
if (import.meta.server) {
|
||||
@ -23,42 +31,18 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
date: true,
|
||||
},
|
||||
})
|
||||
const hydrationLogs = new Set<string>()
|
||||
consola.wrapConsole()
|
||||
consola.addReporter({
|
||||
log (logObj) {
|
||||
try {
|
||||
hydrationLogs.add(JSON.stringify(logObj.args))
|
||||
} catch {
|
||||
// silently ignore - the worst case is a user gets log twice
|
||||
}
|
||||
},
|
||||
})
|
||||
nuxtApp.hook('dev:ssr-logs', (logs) => {
|
||||
for (const log of logs) {
|
||||
// deduplicate so we don't print out things that are logged on client
|
||||
try {
|
||||
if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) {
|
||||
logger.log(normalizeServerLog({ ...log }))
|
||||
}
|
||||
} catch {
|
||||
logger.log(normalizeServerLog({ ...log }))
|
||||
}
|
||||
logger.log(normalizeServerLog({ ...log }))
|
||||
}
|
||||
})
|
||||
|
||||
nuxtApp.hooks.hook('app:suspense:resolve', () => consola.restoreAll())
|
||||
nuxtApp.hooks.hookOnce('dev:ssr-logs', () => hydrationLogs.clear())
|
||||
}
|
||||
|
||||
// pass SSR logs after hydration
|
||||
nuxtApp.hooks.hook('app:suspense:resolve', async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const content = document.getElementById('__NUXT_LOGS__')?.textContent
|
||||
const logs = content ? parse(content, nuxtApp._payloadRevivers) as LogObject[] : []
|
||||
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
|
||||
}
|
||||
})
|
||||
if (typeof window !== 'undefined') {
|
||||
const content = document.getElementById('__NUXT_LOGS__')?.textContent
|
||||
const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
|
||||
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
|
||||
}
|
||||
})
|
||||
|
||||
function normalizeFilenames (stack?: string) {
|
||||
|
@ -85,8 +85,8 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
|
||||
changedTemplates.push(template)
|
||||
}
|
||||
|
||||
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
|
||||
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
|
||||
const perf = performance.measure(fullPath, mark.name)
|
||||
const setupTime = Math.round((perf.duration * 100)) / 100
|
||||
|
||||
if (nuxt.options.debug || setupTime > 500) {
|
||||
logger.info(`Compiled \`${template.filename}\` in ${setupTime}ms`)
|
||||
|
@ -161,6 +161,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
'nuxt3/dist',
|
||||
'nuxt-nightly/dist',
|
||||
distDir,
|
||||
// Ensure app config files have auto-imports injected even if they are pure .js files
|
||||
...nuxt.options._layers.map(layer => resolve(layer.config.srcDir, 'app.config')),
|
||||
],
|
||||
traceInclude: [
|
||||
// force include files used in generated code from the runtime-compiler
|
||||
|
@ -13,7 +13,7 @@ import fse from 'fs-extra'
|
||||
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
|
||||
|
||||
import defu from 'defu'
|
||||
import { gt } from 'semver'
|
||||
import { gt, satisfies } from 'semver'
|
||||
import pagesModule from '../pages/module'
|
||||
import metaModule from '../head/module'
|
||||
import componentsModule from '../components/module'
|
||||
@ -129,6 +129,8 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
if (nuxt.options.typescript.shim) {
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/vue-shim.d.ts') })
|
||||
}
|
||||
// Add shims for `#build/*` imports that do not already have matching types
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/build.d.ts') })
|
||||
// Add module augmentations directly to NuxtConfig
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/schema.d.ts') })
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app.config.d.ts') })
|
||||
@ -270,6 +272,9 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
...nuxt.options._layers.filter(i => i.cwd.includes('node_modules')).map(i => i.cwd as string),
|
||||
)
|
||||
|
||||
// Ensure we can resolve dependencies within layers
|
||||
nuxt.options.modulesDir.push(...nuxt.options._layers.map(l => resolve(l.cwd, 'node_modules')))
|
||||
|
||||
// Init user modules
|
||||
await nuxt.callHook('modules:before')
|
||||
const modulesToInstall = []
|
||||
@ -557,6 +562,12 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client'))
|
||||
}
|
||||
|
||||
// Show compatibility version banner when Nuxt is running with a compatibility version
|
||||
// that is different from the current major version
|
||||
if (!(satisfies(nuxt._version, nuxt.options.future.compatibilityVersion + '.x'))) {
|
||||
console.info(`Running with compatibility version \`${nuxt.options.future.compatibilityVersion}\``)
|
||||
}
|
||||
|
||||
await nuxt.callHook('ready', nuxt)
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,17 @@ import type { H3Event } from 'h3'
|
||||
import { withTrailingSlash } from 'ufo'
|
||||
import { getContext } from 'unctx'
|
||||
|
||||
import { isVNode } from 'vue'
|
||||
import type { NitroApp } from '#internal/nitro/app'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { rootDir } from '#internal/dev-server-logs-options'
|
||||
|
||||
const devReducers: Record<string, (data: any) => any> = {
|
||||
VNode: data => isVNode(data) ? { type: data.type, props: data.props } : undefined,
|
||||
URL: data => data instanceof URL ? data.toString() : undefined,
|
||||
}
|
||||
|
||||
interface NuxtDevAsyncContext {
|
||||
logs: LogObject[]
|
||||
event: H3Event
|
||||
@ -54,9 +60,10 @@ export default (nitroApp: NitroApp) => {
|
||||
const ctx = asyncContext.tryUse()
|
||||
if (!ctx) { return }
|
||||
try {
|
||||
htmlContext.bodyAppend.unshift(`<script type="application/json" id="__NUXT_LOGS__">${stringify(ctx.logs, ctx.event.context._payloadReducers)}</script>`)
|
||||
htmlContext.bodyAppend.unshift(`<script type="application/json" id="__NUXT_LOGS__">${stringify(ctx.logs, { ...devReducers, ...ctx.event.context._payloadReducers })}</script>`)
|
||||
} catch (e) {
|
||||
console.warn('[nuxt] Failed to stringify dev server logs. You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.', e)
|
||||
const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : ''
|
||||
console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
../../../../../ui-templates/dist/templates/error-500.ts
|
@ -1 +0,0 @@
|
||||
../../../../../ui-templates/dist/templates/error-dev.ts
|
@ -327,7 +327,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
|
||||
// Whether we are prerendering route
|
||||
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland
|
||||
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + ssrContext.runtimeConfig.app.buildId : undefined
|
||||
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.cdnURL || ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + ssrContext.runtimeConfig.app.buildId : undefined
|
||||
if (import.meta.prerender) {
|
||||
ssrContext.payload.prerenderedAt = Date.now()
|
||||
}
|
||||
|
@ -130,6 +130,12 @@ declare module '#app' {
|
||||
}
|
||||
}
|
||||
|
||||
declare module '#app/defaults' {
|
||||
type DefaultAsyncDataErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
|
||||
type DefaultAsyncDataValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
|
||||
type DefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties extends NuxtAppInjections { }
|
||||
}
|
||||
@ -266,10 +272,13 @@ export const useRuntimeConfig = () => window?.__NUXT__?.config || {}
|
||||
export const appConfigDeclarationTemplate: NuxtTemplate = {
|
||||
filename: 'types/app.config.d.ts',
|
||||
getContents ({ app, nuxt }) {
|
||||
const typesDir = join(nuxt.options.buildDir, 'types')
|
||||
const configPaths = app.configs.map(path => relative(typesDir, path).replace(/\b\.\w+$/g, ''))
|
||||
|
||||
return `
|
||||
import type { CustomAppConfig } from 'nuxt/schema'
|
||||
import type { Defu } from 'defu'
|
||||
${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id.replace(/\b\.\w+$/g, ''))}`).join('\n')}
|
||||
${configPaths.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id)}`).join('\n')}
|
||||
|
||||
declare const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)}
|
||||
type ResolvedAppConfig = Defu<typeof inlineConfig, [${app.configs.map((_id: string, index: number) => `typeof cfg${index}`).join(', ')}]>
|
||||
@ -393,7 +402,13 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
||||
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
|
||||
`export const devLogs = ${JSON.stringify(ctx.nuxt.options.features.devLogs)}`,
|
||||
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`,
|
||||
`export const asyncDataDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.useAsyncData)}`,
|
||||
`export const asyncDataDefaults = ${JSON.stringify({
|
||||
...ctx.nuxt.options.experimental.defaults.useAsyncData,
|
||||
value: ctx.nuxt.options.experimental.defaults.useAsyncData.value === 'null' ? null : undefined,
|
||||
errorValue: ctx.nuxt.options.experimental.defaults.useAsyncData.errorValue === 'null' ? null : undefined,
|
||||
})}`,
|
||||
`export const resetAsyncDataToUndefined = ${ctx.nuxt.options.experimental.resetAsyncDataToUndefined}`,
|
||||
`export const nuxtDefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}`,
|
||||
`export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`,
|
||||
`export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`,
|
||||
`export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
|
||||
@ -401,3 +416,29 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
||||
].join('\n\n')
|
||||
},
|
||||
}
|
||||
|
||||
const TYPE_FILENAME_RE = /\.([cm])?[jt]s$/
|
||||
const DECLARATION_RE = /\.d\.[cm]?ts$/
|
||||
export const buildTypeTemplate: NuxtTemplate = {
|
||||
filename: 'types/build.d.ts',
|
||||
getContents ({ app }) {
|
||||
let declarations = ''
|
||||
|
||||
for (const file of app.templates) {
|
||||
if (file.write || !file.filename || DECLARATION_RE.test(file.filename)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (TYPE_FILENAME_RE.test(file.filename)) {
|
||||
const typeFilenames = new Set([file.filename.replace(TYPE_FILENAME_RE, '.d.$1ts'), file.filename.replace(TYPE_FILENAME_RE, '.d.ts')])
|
||||
if (app.templates.some(f => f.filename && typeFilenames.has(f.filename))) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
declarations += 'declare module ' + JSON.stringify(join('#build', file.filename)) + ';\n'
|
||||
}
|
||||
|
||||
return declarations
|
||||
},
|
||||
}
|
||||
|
@ -135,6 +135,7 @@ export const scriptsStubsPreset = {
|
||||
'useScriptGoogleMaps',
|
||||
'useScriptNpm',
|
||||
],
|
||||
priority: -1,
|
||||
from: '#app/composables/script-stubs',
|
||||
} satisfies InlinePreset
|
||||
|
||||
|
@ -422,11 +422,6 @@ export default defineNuxtModule({
|
||||
getContents: () => 'export { START_LOCATION, useRoute } from \'vue-router\'',
|
||||
})
|
||||
|
||||
// Optimize vue-router to ensure we share the same injection symbol
|
||||
nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {}
|
||||
nuxt.options.vite.optimizeDeps.include = nuxt.options.vite.optimizeDeps.include || []
|
||||
nuxt.options.vite.optimizeDeps.include.push('vue-router')
|
||||
|
||||
nuxt.options.vite.resolve = nuxt.options.vite.resolve || {}
|
||||
nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || []
|
||||
nuxt.options.vite.resolve.dedupe.push('vue-router')
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import MagicString from 'magic-string'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
import { stripLiteral } from 'strip-literal'
|
||||
import { isVue } from '../../core/utils'
|
||||
|
||||
const INJECTION_RE = /\b_ctx\.\$route\b/g
|
||||
const INJECTION_SINGLE_RE = /\b_ctx\.\$route\b/
|
||||
const INJECTION_RE_TEMPLATE = /\b_ctx\.\$route\b/g
|
||||
const INJECTION_RE_SCRIPT = /\bthis\.\$route\b/g
|
||||
|
||||
const INJECTION_SINGLE_RE = /\bthis\.\$route\b|\b_ctx\.\$route\b/
|
||||
|
||||
export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
|
||||
return {
|
||||
@ -14,14 +17,30 @@ export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
|
||||
return isVue(id, { type: ['template', 'script'] })
|
||||
},
|
||||
transform (code) {
|
||||
if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol')) { return }
|
||||
if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol') || code.includes('this._.provides[__nuxt_route_symbol')) { return }
|
||||
|
||||
let replaced = false
|
||||
const s = new MagicString(code)
|
||||
s.replace(INJECTION_RE, () => {
|
||||
replaced = true
|
||||
return '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)'
|
||||
})
|
||||
const strippedCode = stripLiteral(code)
|
||||
|
||||
// Local helper function for regex-based replacements using `strippedCode`
|
||||
const replaceMatches = (regExp: RegExp, replacement: string) => {
|
||||
for (const match of strippedCode.matchAll(regExp)) {
|
||||
const start = match.index!
|
||||
const end = start + match[0].length
|
||||
s.overwrite(start, end, replacement)
|
||||
if (!replaced) {
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handles `$route` in template
|
||||
replaceMatches(INJECTION_RE_TEMPLATE, '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)')
|
||||
|
||||
// handles `this.$route` in script
|
||||
replaceMatches(INJECTION_RE_SCRIPT, '(this._.provides[__nuxt_route_symbol] || this.$route)')
|
||||
|
||||
if (replaced) {
|
||||
s.prepend('import { PageRouteSymbol as __nuxt_route_symbol } from \'#app/components/injections\';\n')
|
||||
}
|
||||
|
73
packages/nuxt/test/route-injection.test.ts
Normal file
73
packages/nuxt/test/route-injection.test.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
@ -35,11 +35,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/telemetry": "2.5.4",
|
||||
"@nuxt/ui-templates": "1.3.3",
|
||||
"@nuxt/ui-templates": "1.3.4",
|
||||
"@types/file-loader": "5.0.4",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/sass-loader": "8.0.8",
|
||||
"@unhead/schema": "1.9.10",
|
||||
"@unhead/schema": "1.9.11",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "3.1.0",
|
||||
"@vue/compiler-core": "3.4.27",
|
||||
@ -54,7 +54,7 @@
|
||||
"unbuild": "latest",
|
||||
"unctx": "2.3.1",
|
||||
"unenv": "1.9.0",
|
||||
"vite": "5.2.11",
|
||||
"vite": "5.2.12",
|
||||
"vue": "3.4.27",
|
||||
"vue-bundle-renderer": "2.1.0",
|
||||
"vue-loader": "17.4.2",
|
||||
@ -71,7 +71,7 @@
|
||||
"scule": "^1.3.0",
|
||||
"std-env": "^3.7.0",
|
||||
"ufo": "^1.5.3",
|
||||
"unimport": "^3.7.1",
|
||||
"unimport": "^3.7.2",
|
||||
"uncrypto": "^0.1.3",
|
||||
"untyped": "^1.4.2"
|
||||
},
|
||||
|
@ -261,6 +261,7 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* Boolean or a path to an HTML file with the contents of which will be inserted into any HTML page
|
||||
* rendered with `ssr: false`.
|
||||
*
|
||||
* - If it is unset, it will use `~/app/spa-loading-template.html` file in one of your layers, if it exists.
|
||||
* - If it is false, no SPA loading indicator will be loaded.
|
||||
* - If true, Nuxt will look for `~/app/spa-loading-template.html` file in one of your layers, or a
|
||||
|
@ -29,6 +29,7 @@ export default defineUntypedSchema({
|
||||
* compileTemplate: true,
|
||||
* templateUtils: true,
|
||||
* relativeWatchPaths: true,
|
||||
* resetAsyncDataToUndefined: true,
|
||||
* defaults: {
|
||||
* useAsyncData: {
|
||||
* deep: true
|
||||
@ -342,8 +343,10 @@ export default defineUntypedSchema({
|
||||
|
||||
/**
|
||||
* Use new experimental head optimisations:
|
||||
*
|
||||
* - Add the capo.js head plugin in order to render tags in of the head in a more performant way.
|
||||
* - Uses the hash hydration plugin to reduce initial hydration
|
||||
*
|
||||
* @see [Nuxt Discussion #22632](https://github.com/nuxt/nuxt/discussions/22632]
|
||||
*/
|
||||
headNext: true,
|
||||
@ -392,7 +395,11 @@ export default defineUntypedSchema({
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
sharedPrerenderData: false,
|
||||
sharedPrerenderData: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
|
||||
@ -415,6 +422,18 @@ export default defineUntypedSchema({
|
||||
* Options that apply to `useAsyncData` (and also therefore `useFetch`)
|
||||
*/
|
||||
useAsyncData: {
|
||||
/** @type {'undefined' | 'null'} */
|
||||
value: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
|
||||
},
|
||||
},
|
||||
/** @type {'undefined' | 'null'} */
|
||||
errorValue: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
|
||||
},
|
||||
},
|
||||
deep: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? !((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
@ -476,5 +495,15 @@ export default defineUntypedSchema({
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether `clear` and `clearNuxtData` should reset async data to its _default_ value or update
|
||||
* it to `null`/`undefined`.
|
||||
*/
|
||||
resetAsyncDataToUndefined: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -46,11 +46,13 @@ export default defineUntypedSchema({
|
||||
* Nitro server handlers.
|
||||
*
|
||||
* Each handler accepts the following options:
|
||||
*
|
||||
* - handler: The path to the file defining the handler.
|
||||
* - route: The route under which the handler is available. This follows the conventions of https://github.com/unjs/radix3.
|
||||
* - method: The HTTP method of requests that should be handled.
|
||||
* - middleware: Specifies whether it is a middleware handler.
|
||||
* - lazy: Specifies whether to use lazy loading to import the handler.
|
||||
*
|
||||
* @see https://nuxt.com/docs/guide/directory-structure/server
|
||||
* @note Files from `server/api`, `server/middleware` and `server/routes` will be automatically registered by Nuxt.
|
||||
* @example
|
||||
|
@ -157,7 +157,11 @@ export default defineUntypedSchema({
|
||||
* See https://github.com/esbuild-kit/esbuild-loader
|
||||
* @type {Omit<typeof import('esbuild-loader')['LoaderOptions'], 'loader'>}
|
||||
*/
|
||||
esbuild: {},
|
||||
esbuild: {
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
tsconfigRaw: '{}',
|
||||
},
|
||||
|
||||
/**
|
||||
* See: https://github.com/webpack-contrib/file-loader#options
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { copyFile } from 'node:fs/promises'
|
||||
import { basename, dirname, join, resolve } from 'pathe'
|
||||
import type { Plugin } from 'vite'
|
||||
// @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151
|
||||
@ -167,6 +168,15 @@ export const RenderPlugin = () => {
|
||||
unlinkSync(fileName)
|
||||
rmdirSync(dirname(fileName))
|
||||
}
|
||||
|
||||
// we manually copy files across rather than using symbolic links for better windows support
|
||||
const nuxtRoot = r('../nuxt')
|
||||
for (const file of ['error-404.vue', 'error-500.vue', 'error-dev.vue', 'welcome.vue']) {
|
||||
await copyFile(r(`dist/templates/${file}`), join(nuxtRoot, 'src/app/components', file))
|
||||
}
|
||||
for (const file of ['error-500.ts', 'error-dev.ts']) {
|
||||
await copyFile(r(`dist/templates/${file}`), join(nuxtRoot, 'src/core/runtime/nitro', file))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,6 @@
|
||||
"prettier": "3.2.5",
|
||||
"scule": "1.3.0",
|
||||
"unocss": "0.60.3",
|
||||
"vite": "5.2.11"
|
||||
"vite": "5.2.12"
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
||||
"consola": "^3.2.3",
|
||||
"cssnano": "^7.0.1",
|
||||
"defu": "^6.1.4",
|
||||
"esbuild": "^0.21.3",
|
||||
"esbuild": "^0.21.4",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"externality": "^1.0.2",
|
||||
@ -62,7 +62,7 @@
|
||||
"ufo": "^1.5.3",
|
||||
"unenv": "^1.9.0",
|
||||
"unplugin": "^1.10.1",
|
||||
"vite": "^5.2.11",
|
||||
"vite": "^5.2.12",
|
||||
"vite-node": "^1.6.0",
|
||||
"vite-plugin-checker": "^0.6.4",
|
||||
"vue-bundle-renderer": "^2.1.0"
|
||||
|
@ -63,6 +63,48 @@ export async function buildClient (ctx: ViteBuildContext) {
|
||||
},
|
||||
optimizeDeps: {
|
||||
entries: [ctx.entry],
|
||||
include: [],
|
||||
// We exclude Vue and Nuxt common dependencies from optimization
|
||||
// as they already ship ESM.
|
||||
//
|
||||
// This will help to reduce the chance for users to encounter
|
||||
// common chunk conflicts that causing browser reloads.
|
||||
// We should also encourage module authors to add their deps to
|
||||
// `exclude` if they ships bundled ESM.
|
||||
//
|
||||
// Also since `exclude` is inert, it's safe to always include
|
||||
// all possible deps even if they are not used yet.
|
||||
//
|
||||
// @see https://github.com/antfu/nuxt-better-optimize-deps#how-it-works
|
||||
exclude: [
|
||||
// Vue
|
||||
'vue',
|
||||
'@vue/runtime-core',
|
||||
'@vue/runtime-dom',
|
||||
'@vue/reactivity',
|
||||
'@vue/shared',
|
||||
'@vue/devtools-api',
|
||||
'vue-router',
|
||||
'vue-demi',
|
||||
|
||||
// Nuxt
|
||||
'nuxt',
|
||||
'nuxt/app',
|
||||
|
||||
// Nuxt Deps
|
||||
'@unhead/vue',
|
||||
'consola',
|
||||
'defu',
|
||||
'devalue',
|
||||
'h3',
|
||||
'hookable',
|
||||
'klona',
|
||||
'ofetch',
|
||||
'pathe',
|
||||
'ufo',
|
||||
'unctx',
|
||||
'unenv',
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@ -129,18 +171,22 @@ export async function buildClient (ctx: ViteBuildContext) {
|
||||
}) as any
|
||||
|
||||
if (clientConfig.server && clientConfig.server.hmr !== false) {
|
||||
const hmrPortDefault = 24678 // Vite's default HMR port
|
||||
const hmrPort = await getPort({
|
||||
port: hmrPortDefault,
|
||||
ports: Array.from({ length: 20 }, (_, i) => hmrPortDefault + 1 + i),
|
||||
})
|
||||
clientConfig.server = defu(clientConfig.server, <ServerOptions> {
|
||||
https: ctx.nuxt.options.devServer.https,
|
||||
const serverDefaults: Omit<ServerOptions, 'hmr'> & { hmr: Exclude<ServerOptions['hmr'], boolean> } = {
|
||||
hmr: {
|
||||
protocol: ctx.nuxt.options.devServer.https ? 'wss' : 'ws',
|
||||
port: hmrPort,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (typeof clientConfig.server.hmr !== 'object' || !clientConfig.server.hmr.server) {
|
||||
const hmrPortDefault = 24678 // Vite's default HMR port
|
||||
serverDefaults.hmr!.port = await getPort({
|
||||
port: hmrPortDefault,
|
||||
ports: Array.from({ length: 20 }, (_, i) => hmrPortDefault + 1 + i),
|
||||
})
|
||||
}
|
||||
if (ctx.nuxt.options.devServer.https) {
|
||||
serverDefaults.https = ctx.nuxt.options.devServer.https === true ? {} : ctx.nuxt.options.devServer.https
|
||||
}
|
||||
clientConfig.server = defu(clientConfig.server, serverDefaults as ViteConfig['server'])
|
||||
}
|
||||
|
||||
// Add analyze plugin if needed
|
||||
@ -162,6 +208,10 @@ export async function buildClient (ctx: ViteBuildContext) {
|
||||
|
||||
await ctx.nuxt.callHook('vite:configResolved', clientConfig, { isClient: true, isServer: false })
|
||||
|
||||
// Prioritize `optimizeDeps.exclude`. If same dep is in `include` and `exclude`, remove it from `include`
|
||||
clientConfig.optimizeDeps!.include = clientConfig.optimizeDeps!.include!
|
||||
.filter(dep => !clientConfig.optimizeDeps!.exclude!.includes(dep))
|
||||
|
||||
if (ctx.nuxt.options.dev) {
|
||||
// Dev
|
||||
const viteServer = await vite.createServer(clientConfig)
|
||||
|
@ -3,21 +3,13 @@ import { useNitro } from '@nuxt/kit'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import { withLeadingSlash, withTrailingSlash } from 'ufo'
|
||||
import { dirname, relative } from 'pathe'
|
||||
import MagicString from 'magic-string'
|
||||
|
||||
const PREFIX = 'virtual:public?'
|
||||
const CSS_URL_RE = /url\((\/[^)]+)\)/g
|
||||
|
||||
export const VitePublicDirsPlugin = createUnplugin(() => {
|
||||
const nitro = useNitro()
|
||||
|
||||
function resolveFromPublicAssets (id: string) {
|
||||
for (const dir of nitro.options.publicAssets) {
|
||||
if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue }
|
||||
const path = id.replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
|
||||
if (existsSync(path)) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boolean }) => {
|
||||
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
|
||||
|
||||
return {
|
||||
name: 'nuxt:vite-public-dir-resolution',
|
||||
@ -40,14 +32,33 @@ export const VitePublicDirsPlugin = createUnplugin(() => {
|
||||
}
|
||||
},
|
||||
},
|
||||
generateBundle (outputOptions, bundle) {
|
||||
renderChunk (code, chunk) {
|
||||
if (!chunk.facadeModuleId?.includes('?inline&used')) { return }
|
||||
|
||||
const s = new MagicString(code)
|
||||
const q = code.match(/(?<= = )['"`]/)?.[0] || '"'
|
||||
for (const [full, url] of code.matchAll(CSS_URL_RE)) {
|
||||
if (resolveFromPublicAssets(url)) {
|
||||
s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`)
|
||||
}
|
||||
}
|
||||
|
||||
if (s.hasChanged()) {
|
||||
s.prepend(`import { publicAssetsURL } from '#internal/nuxt/paths';`)
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined,
|
||||
}
|
||||
}
|
||||
},
|
||||
generateBundle (_outputOptions, bundle) {
|
||||
for (const file in bundle) {
|
||||
const chunk = bundle[file]
|
||||
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }
|
||||
|
||||
let css = chunk.source.toString()
|
||||
let wasReplaced = false
|
||||
for (const [full, url] of css.matchAll(/url\((\/[^)]+)\)/g)) {
|
||||
for (const [full, url] of css.matchAll(CSS_URL_RE)) {
|
||||
if (resolveFromPublicAssets(url)) {
|
||||
const relativeURL = relative(withLeadingSlash(dirname(file)), url)
|
||||
css = css.replace(full, `url(${relativeURL})`)
|
||||
@ -62,3 +73,19 @@ export const VitePublicDirsPlugin = createUnplugin(() => {
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export function useResolveFromPublicAssets () {
|
||||
const nitro = useNitro()
|
||||
|
||||
function resolveFromPublicAssets (id: string) {
|
||||
for (const dir of nitro.options.publicAssets) {
|
||||
if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue }
|
||||
const path = id.replace(/[?#].*$/, '').replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
|
||||
if (existsSync(path)) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { resolveFromPublicAssets }
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
||||
'XMLHttpRequest': 'undefined',
|
||||
},
|
||||
optimizeDeps: {
|
||||
entries: ctx.nuxt.options.ssr ? [ctx.entry] : [],
|
||||
noDiscovery: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
@ -3,6 +3,7 @@ import { logger } from '@nuxt/kit'
|
||||
import { hasTTY, isCI } from 'std-env'
|
||||
import clear from 'clear'
|
||||
import type { NuxtOptions } from '@nuxt/schema'
|
||||
import { useResolveFromPublicAssets } from '../plugins/public-dirs'
|
||||
|
||||
let duplicateCount = 0
|
||||
let lastType: vite.LogType | null = null
|
||||
@ -26,11 +27,18 @@ export function createViteLogger (config: vite.InlineConfig): vite.Logger {
|
||||
const canClearScreen = hasTTY && !isCI && config.clearScreen
|
||||
const clearScreen = canClearScreen ? clear : () => {}
|
||||
|
||||
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
|
||||
|
||||
function output (type: vite.LogType, msg: string, options: vite.LogErrorOptions = {}) {
|
||||
if (typeof msg === 'string' && !process.env.DEBUG) {
|
||||
// TODO: resolve upstream in Vite
|
||||
// Hide sourcemap warnings related to node_modules
|
||||
if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { return }
|
||||
// Hide warnings about externals produced by https://github.com/vitejs/vite/blob/v5.2.11/packages/vite/src/node/plugins/css.ts#L350-L355
|
||||
if (msg.includes('didn\'t resolve at build time, it will remain unchanged to be resolved at runtime')) {
|
||||
const id = msg.trim().match(/^([^ ]+) referenced in/m)?.[1]
|
||||
if (id && resolveFromPublicAssets(id)) { return }
|
||||
}
|
||||
}
|
||||
|
||||
const sameAsLast = lastType === type && lastMsg === msg
|
||||
|
@ -71,10 +71,6 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
'abort-controller': 'unenv/runtime/mock/empty',
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['vue'],
|
||||
exclude: ['nuxt/app'],
|
||||
},
|
||||
css: resolveCSSOptions(nuxt),
|
||||
define: {
|
||||
__NUXT_VERSION__: JSON.stringify(nuxt._version),
|
||||
@ -100,7 +96,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
},
|
||||
plugins: [
|
||||
// add resolver for files in public assets directories
|
||||
VitePublicDirsPlugin.vite(),
|
||||
VitePublicDirsPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server }),
|
||||
composableKeysPlugin.vite({
|
||||
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
|
||||
rootDir: nuxt.options.rootDir,
|
||||
|
@ -28,7 +28,7 @@
|
||||
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"css-loader": "^7.1.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
"cssnano": "^7.0.1",
|
||||
"defu": "^6.1.4",
|
||||
|
1094
pnpm-lock.yaml
1094
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1915,11 +1915,24 @@ describe('public directories', () => {
|
||||
|
||||
// TODO: dynamic paths in dev
|
||||
describe.skipIf(isDev())('dynamic paths', () => {
|
||||
const publicFiles = ['/public.svg', '/css-only-public-asset.svg']
|
||||
const isPublicFile = (base = '/', file: string) => {
|
||||
if (isWebpack) {
|
||||
// TODO: webpack does not yet support dynamic static paths
|
||||
expect(publicFiles).toContain(file)
|
||||
return true
|
||||
}
|
||||
|
||||
expect(file).toMatch(new RegExp(`^${base.replace(/\//g, '\\/')}`))
|
||||
expect(publicFiles).toContain(file.replace(base, '/'))
|
||||
return true
|
||||
}
|
||||
|
||||
it('should work with no overrides', async () => {
|
||||
const html: string = await $fetch<string>('/assets')
|
||||
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
|
||||
const url = match[2] || match[3]
|
||||
expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy()
|
||||
expect(url.startsWith('/_nuxt/') || isPublicFile('/', url)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
@ -1929,16 +1942,14 @@ describe.skipIf(isDev())('dynamic paths', () => {
|
||||
const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)).map(m => m[2] || m[3])
|
||||
const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u))
|
||||
expect(cssURL).toBeDefined()
|
||||
const css: string = await $fetch<string>(cssURL!)
|
||||
const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.'))
|
||||
expect(imageUrls).toMatchInlineSnapshot(`
|
||||
[
|
||||
"./logo.svg",
|
||||
"../public.svg",
|
||||
"../public.svg",
|
||||
"../public.svg",
|
||||
]
|
||||
`)
|
||||
const css = await $fetch<string>(cssURL!)
|
||||
const imageUrls = new Set(Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.]\w{8}\./g, '.')))
|
||||
expect([...imageUrls]).toMatchInlineSnapshot(`
|
||||
[
|
||||
"./logo.svg",
|
||||
"../public.svg",
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
it('should allow setting base URL and build assets directory', async () => {
|
||||
@ -1952,12 +1963,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
|
||||
const html = await $fetch<string>('/foo/assets')
|
||||
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
|
||||
const url = match[2] || match[3]
|
||||
expect(
|
||||
url.startsWith('/foo/_other/') ||
|
||||
url === '/foo/public.svg' ||
|
||||
// TODO: webpack does not yet support dynamic static paths
|
||||
(isWebpack && url === '/public.svg'),
|
||||
).toBeTruthy()
|
||||
expect(url.startsWith('/foo/_other/') || isPublicFile('/foo/', url)).toBeTruthy()
|
||||
}
|
||||
|
||||
expect(await $fetch<string>('/foo/url')).toContain('path: /foo/url')
|
||||
@ -1973,12 +1979,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
|
||||
const html = await $fetch<string>('/assets')
|
||||
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
|
||||
const url = match[2] || match[3]
|
||||
expect(
|
||||
url.startsWith('./_nuxt/') ||
|
||||
url === './public.svg' ||
|
||||
// TODO: webpack does not yet support dynamic static paths
|
||||
(isWebpack && url === '/public.svg'),
|
||||
).toBeTruthy()
|
||||
expect(url.startsWith('./_nuxt/') || isPublicFile('./', url)).toBeTruthy()
|
||||
expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy()
|
||||
}
|
||||
})
|
||||
@ -2007,12 +2008,7 @@ describe.skipIf(isDev())('dynamic paths', () => {
|
||||
const html = await $fetch<string>('/foo/assets')
|
||||
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*)\)/g)) {
|
||||
const url = match[2] || match[3]
|
||||
expect(
|
||||
url.startsWith('https://example.com/_cdn/') ||
|
||||
url === 'https://example.com/public.svg' ||
|
||||
// TODO: webpack does not yet support dynamic static paths
|
||||
(isWebpack && url === '/public.svg'),
|
||||
).toBeTruthy()
|
||||
expect(url.startsWith('https://example.com/_cdn/') || isPublicFile('https://example.com/', url)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
@ -2433,21 +2429,23 @@ describe.skipIf(isWindows)('useAsyncData', () => {
|
||||
})
|
||||
|
||||
it('data is null after navigation when immediate false', async () => {
|
||||
const defaultValue = isV4 ? 'undefined' : 'null'
|
||||
|
||||
const { page } = await renderPage('/useAsyncData/immediate-remove-unmounted')
|
||||
expect(await page.locator('#immediate-data').getByText('null').textContent()).toBe('null')
|
||||
expect(await page.locator('#immediate-data').getByText(defaultValue).textContent()).toBe(defaultValue)
|
||||
|
||||
await page.click('#execute-btn')
|
||||
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain('null')
|
||||
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain(defaultValue)
|
||||
|
||||
await page.click('#to-index')
|
||||
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/')
|
||||
|
||||
await page.click('#to-immediate-remove-unmounted')
|
||||
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/useAsyncData/immediate-remove-unmounted')
|
||||
expect(await page.locator('#immediate-data').getByText('null').textContent()).toBe('null')
|
||||
expect(await page.locator('#immediate-data').getByText(defaultValue).textContent()).toBe(defaultValue)
|
||||
|
||||
await page.click('#execute-btn')
|
||||
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain('null')
|
||||
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain(defaultValue)
|
||||
|
||||
await page.close()
|
||||
})
|
||||
|
97
test/fixtures/basic-types/types.ts
vendored
97
test/fixtures/basic-types/types.ts
vendored
@ -11,6 +11,9 @@ import type { NavigateToOptions } from '#app/composables/router'
|
||||
import { NuxtLayout, NuxtLink, NuxtPage, ServerComponent, WithTypes } from '#components'
|
||||
import { useRouter } from '#imports'
|
||||
|
||||
// TODO: temporary module for backwards compatibility
|
||||
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
|
||||
|
||||
interface TestResponse { message: string }
|
||||
|
||||
describe('API routes', () => {
|
||||
@ -31,61 +34,61 @@ describe('API routes', () => {
|
||||
})
|
||||
|
||||
it('works with useAsyncData', () => {
|
||||
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||
expectTypeOf(useAsyncData('api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
|
||||
expectTypeOf(useAsyncData('api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
|
||||
expectTypeOf(useAsyncData('api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
|
||||
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useAsyncData('api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useAsyncData('api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useAsyncData('api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
|
||||
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
|
||||
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
|
||||
|
||||
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | null>>()
|
||||
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
|
||||
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | DefaultAsyncDataErrorValue>>()
|
||||
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
|
||||
// backwards compatibility
|
||||
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
|
||||
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
|
||||
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
|
||||
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
|
||||
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
|
||||
expectTypeOf(useLazyAsyncData<TestResponse>('lazy-api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
|
||||
expectTypeOf(useLazyAsyncData<TestResponse>('lazy-api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
|
||||
|
||||
expectTypeOf(useLazyAsyncData('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
|
||||
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useLazyAsyncData('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
|
||||
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
|
||||
})
|
||||
|
||||
it('works with useFetch', () => {
|
||||
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
|
||||
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
|
||||
// @ts-expect-error not a valid method
|
||||
useFetch('/api/hey', { method: 'PATCH' })
|
||||
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
|
||||
expectTypeOf(useFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
|
||||
expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
|
||||
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
|
||||
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
|
||||
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | null>>()
|
||||
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
|
||||
|
||||
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
|
||||
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
|
||||
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
|
||||
|
||||
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
|
||||
expectTypeOf(useLazyFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
|
||||
expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
|
||||
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
|
||||
expectTypeOf(useLazyFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
|
||||
expectTypeOf(useLazyFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
|
||||
|
||||
expectTypeOf(useLazyFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
|
||||
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useLazyFetch('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
|
||||
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
|
||||
})
|
||||
})
|
||||
|
||||
@ -421,10 +424,10 @@ describe('composables', () => {
|
||||
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { default: () => 'test' }).data).toEqualTypeOf<Ref<string>>()
|
||||
|
||||
// transform must match the explicit generic because of typescript limitations microsoft/TypeScript#14400
|
||||
expectTypeOf(useFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
|
||||
expectTypeOf(useFetch<string>('/test', { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
|
||||
expectTypeOf(useLazyFetch<string>('/test', { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
|
||||
@ -439,7 +442,7 @@ describe('composables', () => {
|
||||
return data.foo
|
||||
},
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<Ref<'bar' | null>>()
|
||||
expectTypeOf(data).toEqualTypeOf<Ref<'bar' | DefaultAsyncDataValue>>()
|
||||
})
|
||||
|
||||
it('infer request url string literal from server/api routes', () => {
|
||||
@ -448,8 +451,8 @@ describe('composables', () => {
|
||||
expectTypeOf(useFetch(dynamicStringUrl).data).toEqualTypeOf<Ref<unknown>>()
|
||||
|
||||
// request param should infer string literal type / show auto-complete hint base on server routes, ex: '/api/hello'
|
||||
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
|
||||
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
|
||||
|
||||
// request can accept string literal and Request object type
|
||||
expectTypeOf(useFetch('https://example.com/api').data).toEqualTypeOf<Ref<unknown>>()
|
||||
@ -519,7 +522,7 @@ describe('composables', () => {
|
||||
it('correctly types returns when using with getCachedData', () => {
|
||||
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
|
||||
getCachedData: key => useNuxtApp().payload.data[key],
|
||||
}).data).toEqualTypeOf<Ref<{ foo: number } | null>>()
|
||||
}).data).toEqualTypeOf<Ref<{ foo: number } | DefaultAsyncDataValue>>()
|
||||
useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
|
||||
// @ts-expect-error cached data should return the same as value of fetcher
|
||||
getCachedData: () => ({ bar: 2 }),
|
||||
|
@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
export default defineAppConfig({
|
||||
userConfig: 123,
|
||||
nested: {
|
1
test/fixtures/basic/assets/global.css
vendored
1
test/fixtures/basic/assets/global.css
vendored
@ -1,4 +1,5 @@
|
||||
:root {
|
||||
--global: 'global';
|
||||
--asset: url('~/assets/css-only-asset.svg');
|
||||
--public-asset: url('/css-only-public-asset.svg');
|
||||
}
|
||||
|
1
test/fixtures/basic/pages/assets-custom.vue
vendored
1
test/fixtures/basic/pages/assets-custom.vue
vendored
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<img src="/public.svg">
|
||||
<img src="/public.svg?123">
|
||||
<img src="/custom/file.svg">
|
||||
</div>
|
||||
</template>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div>immediate-remove-unmounted.vue</div>
|
||||
<div id="immediate-data">
|
||||
{{ data === null ? "null" : data }}
|
||||
{{ data === null ? "null" : (data === undefined ? 'undefined' : data) }}
|
||||
</div>
|
||||
<button
|
||||
id="execute-btn"
|
||||
@ -20,9 +20,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { asyncDataDefaults } from '#build/nuxt.config.mjs'
|
||||
|
||||
const { data, execute } = await useAsyncData('immediateFalse', () => $fetch('/api/random'), { immediate: false })
|
||||
|
||||
if (data.value !== null) {
|
||||
throw new Error('Initial data should be null: ' + data.value)
|
||||
if (data.value !== asyncDataDefaults.errorValue) {
|
||||
throw new Error(`Initial data should be ${asyncDataDefaults.errorValue}: ` + data.value)
|
||||
}
|
||||
</script>
|
||||
|
1
test/fixtures/basic/public/css-only-public-asset.svg
vendored
Normal file
1
test/fixtures/basic/public/css-only-public-asset.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"></svg>
|
After Width: | Height: | Size: 67 B |
@ -20,6 +20,9 @@ import { callOnce } from '#app/composables/once'
|
||||
import { useLoadingIndicator } from '#app/composables/loading-indicator'
|
||||
import { useRouteAnnouncer } from '#app/composables/route-announcer'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { asyncDataDefaults, nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
|
||||
|
||||
registerEndpoint('/api/test', defineEventHandler(event => ({
|
||||
method: event.method,
|
||||
headers: Object.fromEntries(event.headers.entries()),
|
||||
@ -126,7 +129,7 @@ describe('useAsyncData', () => {
|
||||
]
|
||||
`)
|
||||
expect(res instanceof Promise).toBeTruthy()
|
||||
expect(res.data.value).toBe(null)
|
||||
expect(res.data.value).toBe(asyncDataDefaults.value)
|
||||
await res
|
||||
expect(res.data.value).toBe('test')
|
||||
})
|
||||
@ -138,7 +141,7 @@ describe('useAsyncData', () => {
|
||||
expect(immediate.pending.value).toBe(false)
|
||||
|
||||
const nonimmediate = await useAsyncData(() => Promise.resolve('test'), { immediate: false })
|
||||
expect(nonimmediate.data.value).toBe(null)
|
||||
expect(nonimmediate.data.value).toBe(asyncDataDefaults.value)
|
||||
expect(nonimmediate.status.value).toBe('idle')
|
||||
expect(nonimmediate.pending.value).toBe(true)
|
||||
})
|
||||
@ -163,9 +166,9 @@ describe('useAsyncData', () => {
|
||||
// https://github.com/nuxt/nuxt/issues/23411
|
||||
it('should initialize with error set to null when immediate: false', async () => {
|
||||
const { error, execute } = useAsyncData(() => ({}), { immediate: false })
|
||||
expect(error.value).toBe(null)
|
||||
expect(error.value).toBe(asyncDataDefaults.errorValue)
|
||||
await execute()
|
||||
expect(error.value).toBe(null)
|
||||
expect(error.value).toBe(asyncDataDefaults.errorValue)
|
||||
})
|
||||
|
||||
it('should be accessible with useNuxtData', async () => {
|
||||
@ -206,8 +209,9 @@ describe('useAsyncData', () => {
|
||||
|
||||
clear()
|
||||
|
||||
// TODO: update to asyncDataDefaults.value in v4
|
||||
expect(data.value).toBeUndefined()
|
||||
expect(error.value).toBeNull()
|
||||
expect(error.value).toBe(asyncDataDefaults.errorValue)
|
||||
expect(pending.value).toBe(false)
|
||||
expect(status.value).toBe('idle')
|
||||
})
|
||||
@ -345,13 +349,12 @@ describe('errors', () => {
|
||||
})
|
||||
|
||||
it('global nuxt errors', () => {
|
||||
const err = useError()
|
||||
expect(err.value).toBeUndefined()
|
||||
const error = useError()
|
||||
expect(error.value).toBeUndefined()
|
||||
showError('new error')
|
||||
expect(err.value).toMatchInlineSnapshot('[Error: new error]')
|
||||
expect(error.value).toMatchInlineSnapshot('[Error: new error]')
|
||||
clearError()
|
||||
// TODO: should this return to being undefined?
|
||||
expect(err.value).toBeNull()
|
||||
expect(error.value).toBe(nuxtDefaultErrorValue)
|
||||
})
|
||||
})
|
||||
|
||||
@ -616,7 +619,7 @@ describe('routing utilities: `abortNavigation`', () => {
|
||||
it('should throw an error if one is provided', () => {
|
||||
const error = useError()
|
||||
expect(() => abortNavigation({ message: 'Page not found' })).toThrowErrorMatchingInlineSnapshot('[Error: Page not found]')
|
||||
expect(error.value).toBeFalsy()
|
||||
expect(error.value).toBe(nuxtDefaultErrorValue)
|
||||
})
|
||||
it('should block navigation if no error is provided', () => {
|
||||
expect(abortNavigation()).toMatchInlineSnapshot('false')
|
||||
|
Loading…
Reference in New Issue
Block a user