diff --git a/docs/2.guide/4.recipes/3.custom-usefetch.md b/docs/2.guide/4.recipes/3.custom-usefetch.md index 9ada89255..e8f25f6a2 100644 --- a/docs/2.guide/4.recipes/3.custom-usefetch.md +++ b/docs/2.guide/4.recipes/3.custom-usefetch.md @@ -79,7 +79,7 @@ import type { UseFetchOptions } from 'nuxt/app' export function useAPI( url: string | (() => string), - options: Omit, 'default'> & { default: () => T | Ref }, + options?: UseFetchOptions, ) { return useFetch(url, { ...options, diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 86006ff82..8f491750e 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -239,18 +239,17 @@ export function useAsyncData< options.deep = options.deep ?? asyncDataDefaults.deep options.dedupe = options.dedupe ?? 'cancel' - // TODO: make more precise when v4 lands - const hasCachedData = () => options.getCachedData!(key, nuxtApp) !== undefined - // Create or use a shared asyncData entity + const initialCachedData = options.getCachedData!(key, nuxtApp) + const hasCachedData = typeof initialCachedData !== 'undefined' + if (!nuxtApp._asyncData[key] || !options.immediate) { nuxtApp.payload._errors[key] ??= undefined const _ref = options.deep ? ref : shallowRef - const cachedData = options.getCachedData!(key, nuxtApp) nuxtApp._asyncData[key] = { - data: _ref(typeof cachedData !== 'undefined' ? cachedData : options.default!()), - pending: ref(!hasCachedData()), + data: _ref(hasCachedData ? initialCachedData : options.default!()), + pending: ref(!hasCachedData), error: toRef(nuxtApp.payload._errors, key), status: ref('idle'), _default: options.default!, @@ -272,8 +271,11 @@ export function useAsyncData< (nuxtApp._asyncDataPromises[key] as any).cancelled = true } // Avoid fetching same key that is already fetched - if ((opts._initial || (nuxtApp.isHydrating && opts._initial !== false)) && hasCachedData()) { - return Promise.resolve(options.getCachedData!(key, nuxtApp)) + if ((opts._initial || (nuxtApp.isHydrating && opts._initial !== false))) { + const cachedData = opts._initial ? initialCachedData : options.getCachedData!(key, nuxtApp) + if (typeof cachedData !== 'undefined') { + return Promise.resolve(cachedData) + } } asyncData.pending.value = true asyncData.status.value = 'pending' @@ -362,7 +364,7 @@ export function useAsyncData< onUnmounted(() => cbs.splice(0, cbs.length)) } - if (fetchOnServer && nuxtApp.isHydrating && (asyncData.error.value || hasCachedData())) { + if (fetchOnServer && nuxtApp.isHydrating && (asyncData.error.value || typeof initialCachedData !== 'undefined')) { // 1. Hydration (server: true): no fetch asyncData.pending.value = false asyncData.status.value = asyncData.error.value ? 'error' : 'success' diff --git a/packages/vite/src/plugins/nitro-sourcemap.ts b/packages/vite/src/plugins/nitro-sourcemap.ts new file mode 100644 index 000000000..01c2dc372 --- /dev/null +++ b/packages/vite/src/plugins/nitro-sourcemap.ts @@ -0,0 +1,63 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'pathe' + +import type { Plugin as RollupPlugin } from 'rollup' +import type { Plugin as VitePlugin } from 'vite' + +export const createSourcemapPreserver = () => { + let outputDir: string + const ids = new Set() + + const vitePlugin = { + name: 'nuxt:sourcemap-export', + configResolved (config) { + outputDir = config.build.outDir + }, + async writeBundle (_options, bundle) { + for (const chunk of Object.values(bundle)) { + if (chunk.type !== 'chunk' || !chunk.map) { continue } + + const id = resolve(outputDir, chunk.fileName) + ids.add(id) + const dest = id + '.map.json' + await mkdir(dirname(dest), { recursive: true }) + await writeFile(dest, JSON.stringify({ + file: chunk.map.file, + mappings: chunk.map.mappings, + names: chunk.map.names, + sources: chunk.map.sources, + sourcesContent: chunk.map.sourcesContent, + version: chunk.map.version, + })) + } + }, + } satisfies VitePlugin + + const nitroPlugin = { + name: 'nuxt:sourcemap-import', + async load (id) { + id = resolve(id) + if (!ids.has(id)) { return } + + const [code, map] = await Promise.all([ + readFile(id, 'utf-8').catch(() => undefined), + readFile(id + '.map.json', 'utf-8').catch(() => undefined), + ]) + + if (!code) { + this.warn('Failed loading file') + return null + } + + return { + code, + map, + } + }, + } satisfies RollupPlugin + + return { + vitePlugin, + nitroPlugin, + } +} diff --git a/packages/vite/src/server.ts b/packages/vite/src/server.ts index f7023b3cb..84f34e74e 100644 --- a/packages/vite/src/server.ts +++ b/packages/vite/src/server.ts @@ -5,11 +5,13 @@ import viteJsxPlugin from '@vitejs/plugin-vue-jsx' import { logger, resolvePath, tryImportModule } from '@nuxt/kit' import { joinURL, withTrailingSlash, withoutLeadingSlash } from 'ufo' import type { ViteConfig } from '@nuxt/schema' +import defu from 'defu' import type { ViteBuildContext } from './vite' import { createViteLogger } from './utils/logger' import { initViteNodeServer } from './vite-node' import { writeManifest } from './manifest' import { transpile } from './utils/transpile' +import { createSourcemapPreserver } from './plugins/nitro-sourcemap' export async function buildServer (ctx: ViteBuildContext) { const helper = ctx.nuxt.options.nitro.imports !== false ? '' : 'globalThis.' @@ -121,6 +123,17 @@ export async function buildServer (ctx: ViteBuildContext) { } } + // tell rollup's nitro build about the original sources of the generated vite server build + if (ctx.nuxt.options.sourcemap.server && !ctx.nuxt.options.dev) { + const { vitePlugin, nitroPlugin } = createSourcemapPreserver() + serverConfig.plugins!.push(vitePlugin) + ctx.nuxt.hook('nitro:build:before', (nitro) => { + nitro.options.rollupConfig = defu(nitro.options.rollupConfig, { + plugins: [nitroPlugin], + }) + }) + } + serverConfig.customLogger = createViteLogger(serverConfig) await ctx.nuxt.callHook('vite:extendConfig', serverConfig, { isClient: false, isServer: true }) diff --git a/test/nuxt/composables.test.ts b/test/nuxt/composables.test.ts index 9fcb97ded..d308e3cb0 100644 --- a/test/nuxt/composables.test.ts +++ b/test/nuxt/composables.test.ts @@ -224,6 +224,17 @@ describe('useAsyncData', () => { `) }) + it('should only call getCachedData once', async () => { + const getCachedData = vi.fn(() => ({ val: false })) + const { data } = await useAsyncData(() => Promise.resolve({ val: true }), { getCachedData }) + expect(data.value).toMatchInlineSnapshot(` + { + "val": false, + } + `) + expect(getCachedData).toHaveBeenCalledTimes(1) + }) + it('should use default while pending', async () => { const promise = useAsyncData(() => Promise.resolve('test'), { default: () => 'default' }) const { data, pending } = promise