Merge remote-tracking branch 'origin/main' into fix/14275-nuxt-3-does-not-respect-layouts-in-separate-folders

This commit is contained in:
Daniel Roe 2023-10-16 22:20:27 +01:00
commit 2d41400cfb
27 changed files with 266 additions and 76 deletions

View File

@ -350,10 +350,6 @@ In this case, the `.server` + `.client` components are two 'halves' of a compone
</template> </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 ## `<DevOnly>` Component
Nuxt provides the `<DevOnly>` component to render a component only during development. Nuxt provides the `<DevOnly>` component to render a component only during development.

View File

@ -26,10 +26,12 @@ type AsyncDataOptions<DataT> = {
server?: boolean server?: boolean
lazy?: boolean lazy?: boolean
immediate?: boolean immediate?: boolean
deep?: boolean
default?: () => DataT | Ref<DataT> | null default?: () => DataT | Ref<DataT> | null
transform?: (input: DataT) => DataT transform?: (input: DataT) => DataT
pick?: string[] pick?: string[]
watch?: WatchSource[] watch?: WatchSource[]
getCachedData?: (key: string) => any
} }
type AsyncData<DataT, ErrorT> = { 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 * _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 * _pick_: only pick specified keys in this array from the `handler` function result
* _watch_: watch reactive sources to auto-refresh * _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. 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.

View File

@ -26,6 +26,8 @@ type UseFetchOptions<DataT> = {
server?: boolean server?: boolean
lazy?: boolean lazy?: boolean
immediate?: boolean immediate?: boolean
getCachedData?: (key: string) => any
deep?: boolean
default?: () => DataT default?: () => DataT
transform?: (input: DataT) => DataT transform?: (input: DataT) => DataT
pick?: string[] 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 * `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 * `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`. * `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} ::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`. 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`.

View File

@ -24,3 +24,12 @@ Because the data inside [`useState`](/docs/api/composables/use-state) will be se
::ReadMore{link="/docs/getting-started/state-management"} ::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
```

View File

@ -1,5 +1,6 @@
import { createElementBlock, createElementVNode, defineComponent, h, mergeProps, onMounted, ref } from 'vue' import { createElementBlock, createElementVNode, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, ref } from 'vue'
import type { ComponentOptions } from 'vue' import type { ComponentInternalInstance, ComponentOptions } from 'vue'
import { getFragmentHTML } from './utils'
export default defineComponent({ export default defineComponent({
name: 'ClientOnly', 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) ? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
: h(res) : h(res)
} else { } 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) { } else if (clone.template) {
@ -51,8 +53,20 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
} }
clone.setup = (props, ctx) => { 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) 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) || {}) return Promise.resolve(component.setup?.(props, ctx) || {})
.then((setupState) => { .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) ? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
: h(res) : h(res)
} else { } 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 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
}

View File

@ -104,7 +104,7 @@ export function vforToArray (source: any): any[] {
* @param withoutSlots purge all slots from the HTML string retrieved * @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 * @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) {
if (element.nodeName === '#comment' && element.nodeValue === '[') { if (element.nodeName === '#comment' && element.nodeValue === '[') {
return getFragmentChildren(element, [], withoutSlots) return getFragmentChildren(element, [], withoutSlots)

View File

@ -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 { Ref, WatchSource } from 'vue'
import type { NuxtApp } from '../nuxt' import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
@ -40,10 +40,12 @@ export interface AsyncDataOptions<
server?: boolean server?: boolean
lazy?: boolean lazy?: boolean
default?: () => DefaultT | Ref<DefaultT> default?: () => DefaultT | Ref<DefaultT>
getCachedData?: (key: string) => DataT
transform?: _Transform<ResT, DataT> transform?: _Transform<ResT, DataT>
pick?: PickKeys pick?: PickKeys
watch?: MultiWatchSources watch?: MultiWatchSources
immediate?: boolean immediate?: boolean
deep?: boolean
} }
export interface AsyncDataExecuteOptions { export interface AsyncDataExecuteOptions {
@ -59,15 +61,14 @@ export interface AsyncDataExecuteOptions {
export interface _AsyncData<DataT, ErrorT> { export interface _AsyncData<DataT, ErrorT> {
data: Ref<DataT> data: Ref<DataT>
pending: Ref<boolean> pending: Ref<boolean>
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void> refresh: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void> execute: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
error: Ref<ErrorT | null> error: Ref<ErrorT | null>
status: Ref<AsyncDataRequestStatus> status: Ref<AsyncDataRequestStatus>
} }
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>> export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
const getDefault = () => null
export function useAsyncData< export function useAsyncData<
ResT, ResT,
DataE = Error, DataE = Error,
@ -131,25 +132,31 @@ export function useAsyncData<
throw new TypeError('[nuxt] [asyncData] handler must be a function.') 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 // Apply defaults
options.server = options.server ?? true options.server = options.server ?? true
options.default = options.default ?? (getDefault as () => DefaultT) options.default = options.default ?? (getDefault as () => DefaultT)
options.getCachedData = options.getCachedData ?? getDefaultCachedData
options.lazy = options.lazy ?? false options.lazy = options.lazy ?? false
options.immediate = options.immediate ?? true options.immediate = options.immediate ?? true
// Setup nuxt instance payload const hasCachedData = () => ![null, undefined].includes(options.getCachedData!(key) as any)
const nuxt = useNuxtApp()
const getCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
const hasCachedData = () => getCachedData() !== undefined
// Create or use a shared asyncData entity // Create or use a shared asyncData entity
if (!nuxt._asyncData[key] || !options.immediate) { if (!nuxt._asyncData[key] || !options.immediate) {
nuxt.payload._errors[key] ??= null nuxt.payload._errors[key] ??= null
const _ref = options.deep !== true ? shallowRef : ref
nuxt._asyncData[key] = { nuxt._asyncData[key] = {
data: ref(getCachedData() ?? options.default!()), data: _ref(options.getCachedData!(key) ?? options.default!()),
pending: ref(!hasCachedData()), pending: ref(!hasCachedData()),
error: toRef(nuxt.payload._errors, key), error: toRef(nuxt.payload._errors, key),
status: ref('idle') status: ref('idle')
@ -163,13 +170,13 @@ export function useAsyncData<
if (nuxt._asyncDataPromises[key]) { if (nuxt._asyncDataPromises[key]) {
if (opts.dedupe === false) { if (opts.dedupe === false) {
// Avoid fetching same key more than once at a time // 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 (nuxt._asyncDataPromises[key] as any).cancelled = true
} }
// Avoid fetching same key that is already fetched // Avoid fetching same key that is already fetched
if ((opts._initial || (nuxt.isHydrating && opts._initial !== false)) && hasCachedData()) { if ((opts._initial || (nuxt.isHydrating && opts._initial !== false)) && hasCachedData()) {
return getCachedData() return Promise.resolve(options.getCachedData!(key))
} }
asyncData.pending.value = true asyncData.pending.value = true
asyncData.status.value = 'pending' asyncData.status.value = 'pending'
@ -217,7 +224,7 @@ export function useAsyncData<
delete nuxt._asyncDataPromises[key] delete nuxt._asyncDataPromises[key]
}) })
nuxt._asyncDataPromises[key] = promise nuxt._asyncDataPromises[key] = promise
return nuxt._asyncDataPromises[key] return nuxt._asyncDataPromises[key]!
} }
const initialFetch = () => asyncData.refresh({ _initial: true }) const initialFetch = () => asyncData.refresh({ _initial: true })
@ -230,7 +237,7 @@ export function useAsyncData<
if (getCurrentInstance()) { if (getCurrentInstance()) {
onServerPrefetch(() => promise) onServerPrefetch(() => promise)
} else { } 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 // 1. Hydration (server: true): no fetch
asyncData.pending.value = false asyncData.pending.value = false
asyncData.status.value = asyncData.error.value ? 'error' : 'success' asyncData.status.value = asyncData.error.value ? 'error' : 'success'
@ -265,9 +272,9 @@ export function useAsyncData<
if (options.watch) { if (options.watch) {
watch(options.watch, () => asyncData.refresh()) 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)) { if (!keys || keys.includes(key)) {
return asyncData.refresh() await asyncData.refresh()
} }
}) })
if (instance) { if (instance) {

View File

@ -35,8 +35,6 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
if (import.meta.client) { if (import.meta.client) {
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`) const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`)
if (getCurrentScope()) { onScopeDispose(() => { channel?.close() }) }
const callback = () => { const callback = () => {
writeClientCookie(name, cookie.value, opts as CookieSerializeOptions) writeClientCookie(name, cookie.value, opts as CookieSerializeOptions)
channel?.postMessage(opts.encode(cookie.value as T)) 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 let watchPaused = false
if (getCurrentScope()) {
onScopeDispose(() => {
watchPaused = true
callback()
channel?.close()
})
}
if (channel) { if (channel) {
channel.onmessage = (event) => { channel.onmessage = (event) => {
watchPaused = true watchPaused = true

View File

@ -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>> export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, Lowercase<M>>
type ComputedOptions<T extends Record<string, any>> = { 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 { interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> extends FetchOptions {
@ -107,6 +107,8 @@ export function useFetch<
pick, pick,
watch, watch,
immediate, immediate,
getCachedData,
deep,
...fetchOptions ...fetchOptions
} = opts } = opts
@ -122,6 +124,8 @@ export function useFetch<
transform, transform,
pick, pick,
immediate, immediate,
getCachedData,
deep,
watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])] watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])]
} }

View File

@ -34,7 +34,7 @@
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"h3": "^1.8.2", "h3": "^1.8.2",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lodash-es": "^4.17.21", "klona": "^2.0.6",
"magic-string": "^0.30.4", "magic-string": "^0.30.4",
"memfs": "^4.6.0", "memfs": "^4.6.0",
"mini-css-extract-plugin": "^2.7.6", "mini-css-extract-plugin": "^2.7.6",
@ -65,7 +65,6 @@
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@types/fs-extra": "11.0.2", "@types/fs-extra": "11.0.2",
"@types/hash-sum": "1.0.0", "@types/hash-sum": "1.0.0",
"@types/lodash-es": "4.17.9",
"@types/pify": "5.0.2", "@types/pify": "5.0.2",
"@types/webpack-bundle-analyzer": "4.6.1", "@types/webpack-bundle-analyzer": "4.6.1",
"@types/webpack-hot-middleware": "2.25.7", "@types/webpack-hot-middleware": "2.25.7",

View File

@ -6,7 +6,6 @@
import { normalizeWebpackManifest } from 'vue-bundle-renderer' import { normalizeWebpackManifest } from 'vue-bundle-renderer'
import { dirname } from 'pathe' import { dirname } from 'pathe'
import hash from 'hash-sum' import hash from 'hash-sum'
import { uniq } from 'lodash-es'
import fse from 'fs-extra' import fse from 'fs-extra'
import type { Nuxt } from '@nuxt/schema' import type { Nuxt } from '@nuxt/schema'
@ -18,6 +17,10 @@ interface PluginOptions {
nuxt: Nuxt nuxt: Nuxt
} }
function uniq <T> (items: T[]) {
return [...new Set(items)]
}
export default class VueSSRClientPlugin { export default class VueSSRClientPlugin {
options: PluginOptions options: PluginOptions

View File

@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash-es'
import type { Configuration } from 'webpack' import type { Configuration } from 'webpack'
import type { Nuxt, NuxtOptions } from '@nuxt/schema' import type { Nuxt, NuxtOptions } from '@nuxt/schema'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
import { klona } from 'klona'
export interface WebpackConfigContext { export interface WebpackConfigContext {
nuxt: Nuxt nuxt: Nuxt
@ -67,6 +67,6 @@ export function fileName (ctx: WebpackConfigContext, key: string) {
} }
export function getWebpackConfig (ctx: WebpackConfigContext): Configuration { export function getWebpackConfig (ctx: WebpackConfigContext): Configuration {
// Clone deep avoid leaking config between Client and Server // Clone to avoid leaking config between Client and Server
return cloneDeep(ctx.config) return klona(ctx.config)
} }

View File

@ -1,7 +1,7 @@
import { createCommonJS } from 'mlly' import { createCommonJS } from 'mlly'
import { cloneDeep, defaults, merge } from 'lodash-es'
import { requireModule } from '@nuxt/kit' import { requireModule } from '@nuxt/kit'
import type { Nuxt } from '@nuxt/schema' 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' 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) => { 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) { function sortPlugins ({ plugins, order }: any) {
const names = Object.keys(plugins) const names = Object.keys(plugins)
if (typeof order === 'string') { if (typeof order === 'string') {
@ -43,32 +34,26 @@ export const getPostcssConfig = (nuxt: Nuxt) => {
return typeof order === 'function' ? order(names, orderPresets) : (order || names) return typeof order === 'function' ? order(names, orderPresets) : (order || names)
} }
function loadPlugins (config: any) {
if (!isPureObject(config.plugins)) { return }
// Map postcss plugins into instances on object mode once
const cjs = createCommonJS(import.meta.url)
config.plugins = sortPlugins(config).map((pluginName: string) => {
const pluginFn = requireModule(pluginName, { paths: [cjs.__dirname] })
const pluginOptions = config.plugins[pluginName]
if (!pluginOptions || typeof pluginFn !== 'function') { return null }
return pluginFn(pluginOptions)
}).filter(Boolean)
}
if (!nuxt.options.webpack.postcss || !nuxt.options.postcss) { if (!nuxt.options.webpack.postcss || !nuxt.options.postcss) {
return false return false
} }
let postcssOptions = cloneDeep(nuxt.options.postcss) const postcssOptions = defu({}, nuxt.options.postcss, {
// Apply default plugins sourceMap: nuxt.options.webpack.cssSourceMap,
if (isPureObject(postcssOptions)) { // Array, String or Function
if (Array.isArray(postcssOptions.plugins)) { order: 'autoprefixerAndCssnanoLast'
defaults(postcssOptions, defaultConfig()) })
} else {
// Keep the order of default plugins // Keep the order of default plugins
postcssOptions = merge({}, defaultConfig(), postcssOptions) if (!Array.isArray(postcssOptions.plugins) && isPureObject(postcssOptions.plugins)) {
loadPlugins(postcssOptions) // Map postcss plugins into instances on object mode once
const cjs = createCommonJS(import.meta.url)
postcssOptions.plugins = sortPlugins(postcssOptions).map((pluginName: string) => {
const pluginFn = requireModule(pluginName, { paths: [cjs.__dirname] })
const pluginOptions = postcssOptions.plugins[pluginName]
if (!pluginOptions || typeof pluginFn !== 'function') { return null }
return pluginFn(pluginOptions)
}).filter(Boolean)
} }
return { return {
@ -77,4 +62,3 @@ export const getPostcssConfig = (nuxt: Nuxt) => {
postcssOptions postcssOptions
} }
} }
}

View File

@ -744,9 +744,9 @@ importers:
hash-sum: hash-sum:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
lodash-es: klona:
specifier: ^4.17.21 specifier: ^2.0.6
version: 4.17.21 version: 2.0.6
magic-string: magic-string:
specifier: ^0.30.4 specifier: ^0.30.4
version: 0.30.4 version: 0.30.4
@ -832,9 +832,6 @@ importers:
'@types/hash-sum': '@types/hash-sum':
specifier: 1.0.0 specifier: 1.0.0
version: 1.0.0 version: 1.0.0
'@types/lodash-es':
specifier: 4.17.9
version: 4.17.9
'@types/pify': '@types/pify':
specifier: 5.0.2 specifier: 5.0.2
version: 5.0.2 version: 5.0.2
@ -1272,6 +1269,7 @@ packages:
/@babel/highlight@7.22.20: /@babel/highlight@7.22.20:
resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
requiresBuild: true
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.22.20 '@babel/helper-validator-identifier': 7.22.20
chalk: 2.4.2 chalk: 2.4.2
@ -7994,6 +7992,7 @@ packages:
/lodash-es@4.17.21: /lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: true
/lodash._reinterpolate@3.0.0: /lodash._reinterpolate@3.0.0:
resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==}

View File

@ -271,6 +271,24 @@ describe('pages', () => {
await expectNoClientErrors('/another-parent') 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 () => { it('/client-only-components', async () => {
const html = await $fetch('/client-only-components') const html = await $fetch('/client-only-components')
// ensure fallbacks with classes and arbitrary attributes are rendered // 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', () => { 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 () => { it('single request resolves', async () => {
await expectNoClientErrors('/useAsyncData/single') await expectNoClientErrors('/useAsyncData/single')
}) })

View File

@ -406,6 +406,17 @@ describe('composables', () => {
expectTypeOf(test).toEqualTypeOf<string | undefined>() 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', () => { it('correctly types returns with key signatures', () => {
interface TestType { interface TestType {
id: string id: string

View File

@ -0,0 +1,8 @@
<template>
<div class="client-fragment-server client">
world
</div>
<div class="client-fragment-server client">
world
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div class="client-fragment-server server">
hello
</div>
</template>

View 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>

View 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>

View File

@ -0,0 +1,5 @@
<template>
<div class="client-server client">
world !
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div class="client-server server">
hello
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div class="client-server-fragment client">
world
</div>
</template>

View File

@ -0,0 +1,8 @@
<template>
<div class="client-server-fragment server">
hello
</div>
<div class="client-server-fragment server">
hello
</div>
</template>

View 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>

View 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>

View File

@ -143,6 +143,20 @@ describe('useAsyncData', () => {
expect(useNuxtData('key').data.value).toBeUndefined() 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 () => { it('should be refreshable', async () => {
await useAsyncData('key', () => Promise.resolve('test')) await useAsyncData('key', () => Promise.resolve('test'))
clearNuxtData('key') clearNuxtData('key')
@ -151,6 +165,15 @@ describe('useAsyncData', () => {
await refreshNuxtData('key') await refreshNuxtData('key')
expect(data.data.value).toMatchInlineSnapshot('"test"') 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', () => { describe('errors', () => {