mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-28 08:32:06 +00:00
Merge remote-tracking branch 'origin/main' into fix/14275-nuxt-3-does-not-respect-layouts-in-separate-folders
This commit is contained in:
commit
2d41400cfb
@ -350,10 +350,6 @@ In this case, the `.server` + `.client` components are two 'halves' of a compone
|
||||
</template>
|
||||
```
|
||||
|
||||
::alert{type=warning}
|
||||
It is essential that the client half of the component can 'hydrate' the server-rendered HTML. That is, it should render the same HTML on initial load, or you will experience a hydration mismatch.
|
||||
::
|
||||
|
||||
## `<DevOnly>` Component
|
||||
|
||||
Nuxt provides the `<DevOnly>` component to render a component only during development.
|
||||
|
@ -26,10 +26,12 @@ type AsyncDataOptions<DataT> = {
|
||||
server?: boolean
|
||||
lazy?: boolean
|
||||
immediate?: boolean
|
||||
deep?: boolean
|
||||
default?: () => DataT | Ref<DataT> | null
|
||||
transform?: (input: DataT) => DataT
|
||||
pick?: string[]
|
||||
watch?: WatchSource[]
|
||||
getCachedData?: (key: string) => any
|
||||
}
|
||||
|
||||
type AsyncData<DataT, ErrorT> = {
|
||||
@ -60,6 +62,8 @@ type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
|
||||
* _transform_: a function that can be used to alter `handler` function result after resolving
|
||||
* _pick_: only pick specified keys in this array from the `handler` function result
|
||||
* _watch_: watch reactive sources to auto-refresh
|
||||
* _getCachedData_: a function that receives a cache key and can return cached data if it exists (by default it returns `nuxtApp.payload.data[key]` when hydrating and `nuxtApp.static.data[key]` after the app is hydrated). You can use this to build your own custom cache for `useAsyncData`.
|
||||
* _deep_: return data in a deep ref object (it is `true` by default). It can be set to `false` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive.
|
||||
|
||||
Under the hood, `lazy: false` uses `<Suspense>` to block the loading of the route before the data has been fetched. Consider using `lazy: true` and implementing a loading state instead for a snappier user experience.
|
||||
|
||||
|
@ -26,6 +26,8 @@ type UseFetchOptions<DataT> = {
|
||||
server?: boolean
|
||||
lazy?: boolean
|
||||
immediate?: boolean
|
||||
getCachedData?: (key: string) => any
|
||||
deep?: boolean
|
||||
default?: () => DataT
|
||||
transform?: (input: DataT) => DataT
|
||||
pick?: string[]
|
||||
@ -72,6 +74,8 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
|
||||
* `transform`: a function that can be used to alter `handler` function result after resolving
|
||||
* `pick`: only pick specified keys in this array from the `handler` function result
|
||||
* `watch`: watch an array of reactive sources and auto-refresh the fetch result when they change. Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. Together with `immediate: false`, this allows for a fully-manual `useFetch`.
|
||||
* `getCachedData`: a function that receives a cache key and can return cached data if it exists (by default it returns `nuxtApp.payload.data[key]` when hydrating and `nuxtApp.static.data[key]` after the app is hydrated). You can use this to build your own custom cache for `useFetch`.
|
||||
* `deep`: return data in a deep ref object (it is `true` by default). It can be set to `false` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive.
|
||||
|
||||
::alert{type=warning}
|
||||
If you provide a function or ref as the `url` parameter, or if you provide functions as arguments to the `options` parameter, then the [`useFetch`](/docs/api/composables/use-fetch) call will not match other [`useFetch`](/docs/api/composables/use-fetch) calls elsewhere in your codebase, even if the options seem to be identical. If you wish to force a match, you may provide your own key in `options`.
|
||||
|
@ -24,3 +24,12 @@ Because the data inside [`useState`](/docs/api/composables/use-state) will be se
|
||||
|
||||
::ReadMore{link="/docs/getting-started/state-management"}
|
||||
::
|
||||
|
||||
## Using `shallowRef`
|
||||
|
||||
If you don't need your state to be deeply reactive, you can combine `useState` with [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref). This can improve performance when your state contains large objects and arrays.
|
||||
|
||||
```ts
|
||||
const state = useState('my-shallow-state', () => shallowRef({ deep: 'not reactive' }))
|
||||
// isShallow(state) === true
|
||||
```
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createElementBlock, createElementVNode, defineComponent, h, mergeProps, onMounted, ref } from 'vue'
|
||||
import type { ComponentOptions } from 'vue'
|
||||
import { createElementBlock, createElementVNode, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, ref } from 'vue'
|
||||
import type { ComponentInternalInstance, ComponentOptions } from 'vue'
|
||||
import { getFragmentHTML } from './utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ClientOnly',
|
||||
@ -39,7 +40,8 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
||||
? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
|
||||
: h(res)
|
||||
} else {
|
||||
return h('div', mergeProps(ctx.$attrs ?? ctx._.attrs, { key: 'placeholder-key' }))
|
||||
const fragment = getFragmentHTML(ctx._.vnode.el ?? null)
|
||||
return process.client ? createStaticVNode(fragment.join(''), fragment.length) : h('div', ctx.$attrs ?? ctx._.attrs)
|
||||
}
|
||||
}
|
||||
} else if (clone.template) {
|
||||
@ -51,8 +53,20 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
||||
}
|
||||
|
||||
clone.setup = (props, ctx) => {
|
||||
const instance = getCurrentInstance()!
|
||||
|
||||
const attrs = instance.attrs
|
||||
// remove existing directives during hydration
|
||||
const directives = extractDirectives(instance)
|
||||
// prevent attrs inheritance since a staticVNode is rendered before hydration
|
||||
instance.attrs = {}
|
||||
const mounted$ = ref(false)
|
||||
onMounted(() => { mounted$.value = true })
|
||||
|
||||
onMounted(() => {
|
||||
instance.attrs = attrs
|
||||
instance.vnode.dirs = directives
|
||||
mounted$.value = true
|
||||
})
|
||||
|
||||
return Promise.resolve(component.setup?.(props, ctx) || {})
|
||||
.then((setupState) => {
|
||||
@ -65,7 +79,8 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
||||
? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
|
||||
: h(res)
|
||||
} else {
|
||||
return h('div', mergeProps(ctx.attrs, { key: 'placeholder-key' }))
|
||||
const fragment = getFragmentHTML(instance?.vnode.el ?? null)
|
||||
return process.client ? createStaticVNode(fragment.join(''), fragment.length) : h('div', ctx.attrs)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -75,3 +90,10 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
function extractDirectives (instance: ComponentInternalInstance | null) {
|
||||
if (!instance || !instance.vnode.dirs) { return null }
|
||||
const directives = instance.vnode.dirs
|
||||
instance.vnode.dirs = null
|
||||
return directives
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ export function vforToArray (source: any): any[] {
|
||||
* @param withoutSlots purge all slots from the HTML string retrieved
|
||||
* @returns {string[]} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML
|
||||
*/
|
||||
export function getFragmentHTML (element: RendererNode | null, withoutSlots = false) {
|
||||
export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] {
|
||||
if (element) {
|
||||
if (element.nodeName === '#comment' && element.nodeValue === '[') {
|
||||
return getFragmentChildren(element, [], withoutSlots)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getCurrentInstance, onBeforeMount, onServerPrefetch, onUnmounted, ref, toRef, unref, watch } from 'vue'
|
||||
import { getCurrentInstance, onBeforeMount, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue'
|
||||
import type { Ref, WatchSource } from 'vue'
|
||||
import type { NuxtApp } from '../nuxt'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
@ -40,10 +40,12 @@ export interface AsyncDataOptions<
|
||||
server?: boolean
|
||||
lazy?: boolean
|
||||
default?: () => DefaultT | Ref<DefaultT>
|
||||
getCachedData?: (key: string) => DataT
|
||||
transform?: _Transform<ResT, DataT>
|
||||
pick?: PickKeys
|
||||
watch?: MultiWatchSources
|
||||
immediate?: boolean
|
||||
deep?: boolean
|
||||
}
|
||||
|
||||
export interface AsyncDataExecuteOptions {
|
||||
@ -59,15 +61,14 @@ export interface AsyncDataExecuteOptions {
|
||||
export interface _AsyncData<DataT, ErrorT> {
|
||||
data: Ref<DataT>
|
||||
pending: Ref<boolean>
|
||||
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
||||
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
||||
refresh: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
|
||||
execute: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
|
||||
error: Ref<ErrorT | null>
|
||||
status: Ref<AsyncDataRequestStatus>
|
||||
}
|
||||
|
||||
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
|
||||
|
||||
const getDefault = () => null
|
||||
export function useAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
@ -131,25 +132,31 @@ export function useAsyncData<
|
||||
throw new TypeError('[nuxt] [asyncData] handler must be a function.')
|
||||
}
|
||||
|
||||
// Setup nuxt instance payload
|
||||
const nuxt = useNuxtApp()
|
||||
|
||||
// Used to get default values
|
||||
const getDefault = () => null
|
||||
const getDefaultCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
|
||||
|
||||
// Apply defaults
|
||||
options.server = options.server ?? true
|
||||
options.default = options.default ?? (getDefault as () => DefaultT)
|
||||
options.getCachedData = options.getCachedData ?? getDefaultCachedData
|
||||
|
||||
options.lazy = options.lazy ?? false
|
||||
options.immediate = options.immediate ?? true
|
||||
|
||||
// Setup nuxt instance payload
|
||||
const nuxt = useNuxtApp()
|
||||
|
||||
const getCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
|
||||
const hasCachedData = () => getCachedData() !== undefined
|
||||
const hasCachedData = () => ![null, undefined].includes(options.getCachedData!(key) as any)
|
||||
|
||||
// Create or use a shared asyncData entity
|
||||
if (!nuxt._asyncData[key] || !options.immediate) {
|
||||
nuxt.payload._errors[key] ??= null
|
||||
|
||||
const _ref = options.deep !== true ? shallowRef : ref
|
||||
|
||||
nuxt._asyncData[key] = {
|
||||
data: ref(getCachedData() ?? options.default!()),
|
||||
data: _ref(options.getCachedData!(key) ?? options.default!()),
|
||||
pending: ref(!hasCachedData()),
|
||||
error: toRef(nuxt.payload._errors, key),
|
||||
status: ref('idle')
|
||||
@ -163,13 +170,13 @@ export function useAsyncData<
|
||||
if (nuxt._asyncDataPromises[key]) {
|
||||
if (opts.dedupe === false) {
|
||||
// Avoid fetching same key more than once at a time
|
||||
return nuxt._asyncDataPromises[key]
|
||||
return nuxt._asyncDataPromises[key]!
|
||||
}
|
||||
(nuxt._asyncDataPromises[key] as any).cancelled = true
|
||||
}
|
||||
// Avoid fetching same key that is already fetched
|
||||
if ((opts._initial || (nuxt.isHydrating && opts._initial !== false)) && hasCachedData()) {
|
||||
return getCachedData()
|
||||
return Promise.resolve(options.getCachedData!(key))
|
||||
}
|
||||
asyncData.pending.value = true
|
||||
asyncData.status.value = 'pending'
|
||||
@ -217,7 +224,7 @@ export function useAsyncData<
|
||||
delete nuxt._asyncDataPromises[key]
|
||||
})
|
||||
nuxt._asyncDataPromises[key] = promise
|
||||
return nuxt._asyncDataPromises[key]
|
||||
return nuxt._asyncDataPromises[key]!
|
||||
}
|
||||
|
||||
const initialFetch = () => asyncData.refresh({ _initial: true })
|
||||
@ -230,7 +237,7 @@ export function useAsyncData<
|
||||
if (getCurrentInstance()) {
|
||||
onServerPrefetch(() => promise)
|
||||
} else {
|
||||
nuxt.hook('app:created', () => promise)
|
||||
nuxt.hook('app:created', async () => { await promise })
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,7 +257,7 @@ export function useAsyncData<
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchOnServer && nuxt.isHydrating && hasCachedData()) {
|
||||
if (asyncData.error.value || (fetchOnServer && nuxt.isHydrating && hasCachedData())) {
|
||||
// 1. Hydration (server: true): no fetch
|
||||
asyncData.pending.value = false
|
||||
asyncData.status.value = asyncData.error.value ? 'error' : 'success'
|
||||
@ -265,9 +272,9 @@ export function useAsyncData<
|
||||
if (options.watch) {
|
||||
watch(options.watch, () => asyncData.refresh())
|
||||
}
|
||||
const off = nuxt.hook('app:data:refresh', (keys) => {
|
||||
const off = nuxt.hook('app:data:refresh', async (keys) => {
|
||||
if (!keys || keys.includes(key)) {
|
||||
return asyncData.refresh()
|
||||
await asyncData.refresh()
|
||||
}
|
||||
})
|
||||
if (instance) {
|
||||
|
@ -35,8 +35,6 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
||||
|
||||
if (import.meta.client) {
|
||||
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`)
|
||||
if (getCurrentScope()) { onScopeDispose(() => { channel?.close() }) }
|
||||
|
||||
const callback = () => {
|
||||
writeClientCookie(name, cookie.value, opts as CookieSerializeOptions)
|
||||
channel?.postMessage(opts.encode(cookie.value as T))
|
||||
@ -44,6 +42,14 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
||||
|
||||
let watchPaused = false
|
||||
|
||||
if (getCurrentScope()) {
|
||||
onScopeDispose(() => {
|
||||
watchPaused = true
|
||||
callback()
|
||||
channel?.close()
|
||||
})
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
channel.onmessage = (event) => {
|
||||
watchPaused = true
|
||||
|
@ -13,7 +13,7 @@ type AvailableRouterMethod<R extends NitroFetchRequest> = _AvailableRouterMethod
|
||||
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, Lowercase<M>>
|
||||
|
||||
type ComputedOptions<T extends Record<string, any>> = {
|
||||
[K in keyof T]: T[K] extends Function ? T[K] : T[K] extends Record<string, any> ? ComputedOptions<T[K]> | Ref<T[K]> | T[K] : Ref<T[K]> | T[K]
|
||||
[K in keyof T]: T[K] extends Function ? T[K] : ComputedOptions<T[K]> | Ref<T[K]> | T[K]
|
||||
}
|
||||
|
||||
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> extends FetchOptions {
|
||||
@ -107,6 +107,8 @@ export function useFetch<
|
||||
pick,
|
||||
watch,
|
||||
immediate,
|
||||
getCachedData,
|
||||
deep,
|
||||
...fetchOptions
|
||||
} = opts
|
||||
|
||||
@ -122,6 +124,8 @@ export function useFetch<
|
||||
transform,
|
||||
pick,
|
||||
immediate,
|
||||
getCachedData,
|
||||
deep,
|
||||
watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])]
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"h3": "^1.8.2",
|
||||
"hash-sum": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"klona": "^2.0.6",
|
||||
"magic-string": "^0.30.4",
|
||||
"memfs": "^4.6.0",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
@ -65,7 +65,6 @@
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/hash-sum": "1.0.0",
|
||||
"@types/lodash-es": "4.17.9",
|
||||
"@types/pify": "5.0.2",
|
||||
"@types/webpack-bundle-analyzer": "4.6.1",
|
||||
"@types/webpack-hot-middleware": "2.25.7",
|
||||
|
@ -6,7 +6,6 @@
|
||||
import { normalizeWebpackManifest } from 'vue-bundle-renderer'
|
||||
import { dirname } from 'pathe'
|
||||
import hash from 'hash-sum'
|
||||
import { uniq } from 'lodash-es'
|
||||
import fse from 'fs-extra'
|
||||
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
@ -18,6 +17,10 @@ interface PluginOptions {
|
||||
nuxt: Nuxt
|
||||
}
|
||||
|
||||
function uniq <T> (items: T[]) {
|
||||
return [...new Set(items)]
|
||||
}
|
||||
|
||||
export default class VueSSRClientPlugin {
|
||||
options: PluginOptions
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { Configuration } from 'webpack'
|
||||
import type { Nuxt, NuxtOptions } from '@nuxt/schema'
|
||||
import { logger } from '@nuxt/kit'
|
||||
import { klona } from 'klona'
|
||||
|
||||
export interface WebpackConfigContext {
|
||||
nuxt: Nuxt
|
||||
@ -67,6 +67,6 @@ export function fileName (ctx: WebpackConfigContext, key: string) {
|
||||
}
|
||||
|
||||
export function getWebpackConfig (ctx: WebpackConfigContext): Configuration {
|
||||
// Clone deep avoid leaking config between Client and Server
|
||||
return cloneDeep(ctx.config)
|
||||
// Clone to avoid leaking config between Client and Server
|
||||
return klona(ctx.config)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createCommonJS } from 'mlly'
|
||||
import { cloneDeep, defaults, merge } from 'lodash-es'
|
||||
import { requireModule } from '@nuxt/kit'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
import { defu } from 'defu'
|
||||
|
||||
const isPureObject = (obj: unknown): obj is Object => obj !== null && !Array.isArray(obj) && typeof obj === 'object'
|
||||
|
||||
@ -26,15 +26,6 @@ const orderPresets = {
|
||||
}
|
||||
|
||||
export const getPostcssConfig = (nuxt: Nuxt) => {
|
||||
function defaultConfig () {
|
||||
return {
|
||||
sourceMap: nuxt.options.webpack.cssSourceMap,
|
||||
plugins: nuxt.options.postcss.plugins,
|
||||
// Array, String or Function
|
||||
order: 'autoprefixerAndCssnanoLast'
|
||||
}
|
||||
}
|
||||
|
||||
function sortPlugins ({ plugins, order }: any) {
|
||||
const names = Object.keys(plugins)
|
||||
if (typeof order === 'string') {
|
||||
@ -43,38 +34,31 @@ export const getPostcssConfig = (nuxt: Nuxt) => {
|
||||
return typeof order === 'function' ? order(names, orderPresets) : (order || names)
|
||||
}
|
||||
|
||||
function loadPlugins (config: any) {
|
||||
if (!isPureObject(config.plugins)) { return }
|
||||
if (!nuxt.options.webpack.postcss || !nuxt.options.postcss) {
|
||||
return false
|
||||
}
|
||||
|
||||
const postcssOptions = defu({}, nuxt.options.postcss, {
|
||||
sourceMap: nuxt.options.webpack.cssSourceMap,
|
||||
// Array, String or Function
|
||||
order: 'autoprefixerAndCssnanoLast'
|
||||
})
|
||||
|
||||
// Keep the order of default plugins
|
||||
if (!Array.isArray(postcssOptions.plugins) && isPureObject(postcssOptions.plugins)) {
|
||||
// Map postcss plugins into instances on object mode once
|
||||
const cjs = createCommonJS(import.meta.url)
|
||||
config.plugins = sortPlugins(config).map((pluginName: string) => {
|
||||
postcssOptions.plugins = sortPlugins(postcssOptions).map((pluginName: string) => {
|
||||
const pluginFn = requireModule(pluginName, { paths: [cjs.__dirname] })
|
||||
const pluginOptions = config.plugins[pluginName]
|
||||
const pluginOptions = postcssOptions.plugins[pluginName]
|
||||
if (!pluginOptions || typeof pluginFn !== 'function') { return null }
|
||||
return pluginFn(pluginOptions)
|
||||
}).filter(Boolean)
|
||||
}
|
||||
|
||||
if (!nuxt.options.webpack.postcss || !nuxt.options.postcss) {
|
||||
return false
|
||||
}
|
||||
|
||||
let postcssOptions = cloneDeep(nuxt.options.postcss)
|
||||
// Apply default plugins
|
||||
if (isPureObject(postcssOptions)) {
|
||||
if (Array.isArray(postcssOptions.plugins)) {
|
||||
defaults(postcssOptions, defaultConfig())
|
||||
} else {
|
||||
// Keep the order of default plugins
|
||||
postcssOptions = merge({}, defaultConfig(), postcssOptions)
|
||||
loadPlugins(postcssOptions)
|
||||
}
|
||||
|
||||
return {
|
||||
sourceMap: nuxt.options.webpack.cssSourceMap,
|
||||
...nuxt.options.webpack.postcss,
|
||||
postcssOptions
|
||||
}
|
||||
return {
|
||||
sourceMap: nuxt.options.webpack.cssSourceMap,
|
||||
...nuxt.options.webpack.postcss,
|
||||
postcssOptions
|
||||
}
|
||||
}
|
||||
|
@ -744,9 +744,9 @@ importers:
|
||||
hash-sum:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
klona:
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6
|
||||
magic-string:
|
||||
specifier: ^0.30.4
|
||||
version: 0.30.4
|
||||
@ -832,9 +832,6 @@ importers:
|
||||
'@types/hash-sum':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
'@types/lodash-es':
|
||||
specifier: 4.17.9
|
||||
version: 4.17.9
|
||||
'@types/pify':
|
||||
specifier: 5.0.2
|
||||
version: 5.0.2
|
||||
@ -1272,6 +1269,7 @@ packages:
|
||||
/@babel/highlight@7.22.20:
|
||||
resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
chalk: 2.4.2
|
||||
@ -7994,6 +7992,7 @@ packages:
|
||||
|
||||
/lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
dev: true
|
||||
|
||||
/lodash._reinterpolate@3.0.0:
|
||||
resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==}
|
||||
|
@ -271,6 +271,24 @@ describe('pages', () => {
|
||||
await expectNoClientErrors('/another-parent')
|
||||
})
|
||||
|
||||
it('/client-server', async () => {
|
||||
// expect no hydration issues
|
||||
await expectNoClientErrors('/client-server')
|
||||
const page = await createPage('/client-server')
|
||||
await page.waitForLoadState('networkidle')
|
||||
const bodyHTML = await page.innerHTML('body')
|
||||
expect(await page.locator('.placeholder-to-ensure-no-override').all()).toHaveLength(5)
|
||||
expect(await page.locator('.server').all()).toHaveLength(0)
|
||||
expect(await page.locator('.client-fragment-server.client').all()).toHaveLength(2)
|
||||
expect(await page.locator('.client-fragment-server-fragment.client').all()).toHaveLength(2)
|
||||
expect(await page.locator('.client-server.client').all()).toHaveLength(1)
|
||||
expect(await page.locator('.client-server-fragment.client').all()).toHaveLength(1)
|
||||
expect(await page.locator('.client-server-fragment.client').all()).toHaveLength(1)
|
||||
|
||||
expect(bodyHTML).not.toContain('hello')
|
||||
expect(bodyHTML).toContain('world')
|
||||
})
|
||||
|
||||
it('/client-only-components', async () => {
|
||||
const html = await $fetch('/client-only-components')
|
||||
// ensure fallbacks with classes and arbitrary attributes are rendered
|
||||
@ -1907,6 +1925,12 @@ describe.skipIf(process.env.TEST_CONTEXT !== 'async')('Async context', () => {
|
||||
})
|
||||
|
||||
describe.skipIf(isWindows)('useAsyncData', () => {
|
||||
it('works after useNuxtData call', async () => {
|
||||
const page = await createPage('/useAsyncData/nuxt-data')
|
||||
expect(await page.locator('body').getByText('resolved:true').textContent()).toContain('resolved:true')
|
||||
await page.close()
|
||||
})
|
||||
|
||||
it('single request resolves', async () => {
|
||||
await expectNoClientErrors('/useAsyncData/single')
|
||||
})
|
||||
|
11
test/fixtures/basic-types/types.ts
vendored
11
test/fixtures/basic-types/types.ts
vendored
@ -406,6 +406,17 @@ describe('composables', () => {
|
||||
expectTypeOf(test).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('allows passing reactive values in useFetch', () => {
|
||||
useFetch('/api/hey', {
|
||||
headers: {
|
||||
key: ref('test')
|
||||
},
|
||||
query: {
|
||||
param: computed(() => 'thing')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly types returns with key signatures', () => {
|
||||
interface TestType {
|
||||
id: string
|
||||
|
8
test/fixtures/basic/components/client/FragmentServer.client.vue
vendored
Normal file
8
test/fixtures/basic/components/client/FragmentServer.client.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="client-fragment-server client">
|
||||
world
|
||||
</div>
|
||||
<div class="client-fragment-server client">
|
||||
world
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/components/client/FragmentServer.server.vue
vendored
Normal file
5
test/fixtures/basic/components/client/FragmentServer.server.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="client-fragment-server server">
|
||||
hello
|
||||
</div>
|
||||
</template>
|
8
test/fixtures/basic/components/client/FragmentServerFragment.client.vue
vendored
Normal file
8
test/fixtures/basic/components/client/FragmentServerFragment.client.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="client-fragment-server-fragment client">
|
||||
world
|
||||
</div>
|
||||
<div class="client-fragment-server-fragment client">
|
||||
world
|
||||
</div>
|
||||
</template>
|
8
test/fixtures/basic/components/client/FragmentServerFragment.server.vue
vendored
Normal file
8
test/fixtures/basic/components/client/FragmentServerFragment.server.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="client-fragment-server-fragment server">
|
||||
hello
|
||||
</div>
|
||||
<div class="client-fragment-server-fragment server">
|
||||
hello
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/components/client/Server.client.vue
vendored
Normal file
5
test/fixtures/basic/components/client/Server.client.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="client-server client">
|
||||
world !
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/components/client/Server.server.vue
vendored
Normal file
5
test/fixtures/basic/components/client/Server.server.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="client-server server">
|
||||
hello
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/components/client/ServerFragment.client.vue
vendored
Normal file
5
test/fixtures/basic/components/client/ServerFragment.client.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="client-server-fragment client">
|
||||
world
|
||||
</div>
|
||||
</template>
|
8
test/fixtures/basic/components/client/ServerFragment.server.vue
vendored
Normal file
8
test/fixtures/basic/components/client/ServerFragment.server.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="client-server-fragment server">
|
||||
hello
|
||||
</div>
|
||||
<div class="client-server-fragment server">
|
||||
hello
|
||||
</div>
|
||||
</template>
|
28
test/fixtures/basic/pages/client-server.vue
vendored
Normal file
28
test/fixtures/basic/pages/client-server.vue
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="placeholder-to-ensure-no-override">
|
||||
this should not be removed by hydration
|
||||
</div>
|
||||
<ClientFragmentServer />
|
||||
|
||||
<div class="placeholder-to-ensure-no-override">
|
||||
this should not be removed by hydration
|
||||
</div>
|
||||
<ClientServerFragment />
|
||||
|
||||
<div class="placeholder-to-ensure-no-override">
|
||||
this should not be removed by hydration
|
||||
</div>
|
||||
<ClientServer />
|
||||
|
||||
<div class="placeholder-to-ensure-no-override">
|
||||
this should not be removed by hydration
|
||||
</div>
|
||||
|
||||
<ClientFragmentServerFragment />
|
||||
|
||||
<div class="placeholder-to-ensure-no-override">
|
||||
this should not be removed by hydration
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
15
test/fixtures/basic/pages/useAsyncData/nuxt-data.vue
vendored
Normal file
15
test/fixtures/basic/pages/useAsyncData/nuxt-data.vue
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!pending"
|
||||
v-text="'resolved:' + data.resolved"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-text="'loading'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
useNuxtData('call')
|
||||
const { data, pending } = await useAsyncData('call', () => Promise.resolve({ resolved: true }), { server: false })
|
||||
</script>
|
@ -143,6 +143,20 @@ describe('useAsyncData', () => {
|
||||
expect(useNuxtData('key').data.value).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should be usable _after_ a useNuxtData call', async () => {
|
||||
useNuxtApp().payload.data.call = null
|
||||
const { data: cachedData } = useNuxtData('call')
|
||||
expect(cachedData.value).toMatchInlineSnapshot('null')
|
||||
const { data } = await useAsyncData('call', () => Promise.resolve({ resolved: true }), { server: false })
|
||||
expect(cachedData.value).toMatchInlineSnapshot(`
|
||||
{
|
||||
"resolved": true,
|
||||
}
|
||||
`)
|
||||
expect(data.value).toEqual(cachedData.value)
|
||||
clearNuxtData('call')
|
||||
})
|
||||
|
||||
it('should be refreshable', async () => {
|
||||
await useAsyncData('key', () => Promise.resolve('test'))
|
||||
clearNuxtData('key')
|
||||
@ -151,6 +165,15 @@ describe('useAsyncData', () => {
|
||||
await refreshNuxtData('key')
|
||||
expect(data.data.value).toMatchInlineSnapshot('"test"')
|
||||
})
|
||||
|
||||
it('allows custom access to a cache', async () => {
|
||||
const { data } = await useAsyncData(() => ({ val: true }), { getCachedData: () => ({ val: false }) })
|
||||
expect(data.value).toMatchInlineSnapshot(`
|
||||
{
|
||||
"val": false,
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user