From 9d09b2d3652605d5a0b6e2a8bef1390ab957ea5d Mon Sep 17 00:00:00 2001 From: wattanx Date: Tue, 2 Jul 2024 22:31:30 +0900 Subject: [PATCH 1/7] feat: add experimental schema --- .../bridge-schema/src/config/experimental.ts | 38 +++++++++++++++++++ packages/bridge-schema/src/config/index.ts | 2 + 2 files changed, 40 insertions(+) create mode 100644 packages/bridge-schema/src/config/experimental.ts diff --git a/packages/bridge-schema/src/config/experimental.ts b/packages/bridge-schema/src/config/experimental.ts new file mode 100644 index 00000000..a9522ebd --- /dev/null +++ b/packages/bridge-schema/src/config/experimental.ts @@ -0,0 +1,38 @@ +import { defineUntypedSchema } from 'untyped' + +export default defineUntypedSchema({ + /** + * `future` is for early opting-in to new features that will become default in a future + * (possibly major) version of the framework. + */ + future: { + /** + * Enable early access to future features or flags. + * + * It is currently not configurable but may be in future. + * @type {4} + */ + compatibilityVersion: 4, + }, + experimental: { + defaults: { + /** + * 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).compatibilityVersion === 4 ? 'undefined' : 'null') + }, + }, + /** @type {'undefined' | 'null'} */ + errorValue: { + async $resolve (val, get) { + return val ?? ((await get('future') as Record).compatibilityVersion === 4 ? 'undefined' : 'null') + }, + }, + }, + } + } +}) \ No newline at end of file diff --git a/packages/bridge-schema/src/config/index.ts b/packages/bridge-schema/src/config/index.ts index c32bf907..20164bfb 100644 --- a/packages/bridge-schema/src/config/index.ts +++ b/packages/bridge-schema/src/config/index.ts @@ -2,6 +2,7 @@ import app from './app' import build from './build' import cli from './cli' import common from './common' +import experimental from './experimental' import generate from './generate' import messages from './messages' import render from './render' @@ -31,6 +32,7 @@ export default { ...build, ...cli, ...common, + ...experimental, ...generate, ...messages, ...render, From df4d36675cd025d91ef03b5752dc483376ab6ad1 Mon Sep 17 00:00:00 2001 From: wattanx Date: Tue, 2 Jul 2024 22:41:36 +0900 Subject: [PATCH 2/7] fix: fix type error --- packages/bridge/src/global-middleware-template.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/bridge/src/global-middleware-template.ts b/packages/bridge/src/global-middleware-template.ts index a48e06cb..2ae0acfa 100644 --- a/packages/bridge/src/global-middleware-template.ts +++ b/packages/bridge/src/global-middleware-template.ts @@ -1,17 +1,12 @@ -import type { Nuxt, NuxtApp } from '@nuxt/schema' +import type { NuxtTemplate } from '@nuxt/schema' import { genArrayFromRaw, genImport, genSafeVariableName } from 'knitwork' import { resolveFiles } from '@nuxt/kit' import { getNameFromPath, hasSuffix } from './utils/names' -interface TemplateContext { - nuxt: Nuxt - app: NuxtApp & { templateVars: Record } -} - -export const globalMiddlewareTemplate = { +export const globalMiddlewareTemplate: NuxtTemplate = { filename: 'global-middleware.mjs', - getContents: async ({ nuxt, app }: TemplateContext) => { + getContents: async ({ nuxt, app }) => { app.middleware = [] const middlewareDir = nuxt.options.dir.middleware || 'middleware' const middlewareFiles = await resolveFiles(nuxt.options.srcDir, `${middlewareDir}/*{${nuxt.options.extensions.join(',')}}`) From cf06a2860e4f2a23722ab9bfe2105ba14c1611ab Mon Sep 17 00:00:00 2001 From: wattanx Date: Tue, 2 Jul 2024 23:52:54 +0900 Subject: [PATCH 3/7] feat: add virtual file --- packages/bridge/module.cjs | 3 +++ packages/bridge/src/app.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/bridge/module.cjs b/packages/bridge/module.cjs index 29a9a429..8d9be4b8 100644 --- a/packages/bridge/module.cjs +++ b/packages/bridge/module.cjs @@ -19,6 +19,9 @@ module.exports.defineNuxtConfig = (config = {}) => { // Initialize nitro options config.nitro = config.nitro || {} + config.future = config.future || {} + config.experimental = config.experimental || {} + // Nuxt kit depends on this flag to check bridge compatibility config.bridge = typeof config.bridge === 'object' ? config.bridge : {} config.bridge._version = pkg.version diff --git a/packages/bridge/src/app.ts b/packages/bridge/src/app.ts index e6ab9e48..8b3588a8 100644 --- a/packages/bridge/src/app.ts +++ b/packages/bridge/src/app.ts @@ -2,6 +2,8 @@ import { useNuxt, addTemplate, resolveAlias, addWebpackPlugin, addVitePlugin, ad import { NuxtModule } from '@nuxt/schema' import { normalize, resolve } from 'pathe' import { resolveImports } from 'mlly' +import { applyDefaults } from 'untyped' +import { NuxtConfigSchema } from '@nuxt/bridge-schema' import { componentsTypeTemplate, schemaTemplate, middlewareTypeTemplate } from './type-templates' import { distDir } from './dirs' import { VueCompat } from './vue-compat' @@ -105,6 +107,21 @@ export async function setupAppBridge (_options: any) { addTemplate(globalMiddlewareTemplate) + + if (nuxt.options.future?.compatibilityVersion === 4) { + // @ts-expect-error only partially supported by nuxt bridge + nuxt.options.experimental = (await applyDefaults({ future: NuxtConfigSchema['future'], experimental: NuxtConfigSchema['experimental'] }, nuxt.options.experimental)).experimental + + addTemplate({ + filename: 'nuxt.config.mjs', + getContents: (ctx) => { + return [ + `export const asyncDataDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.useAsyncData)}`, + ].join('\n\n') + } + }) + } + // Alias vue3 utilities to vue2 const { dst: vueCompat } = addTemplate({ src: resolve(distDir, 'runtime/vue2-bridge.mjs') }) addWebpackPlugin(VueCompat.webpack({ src: vueCompat })) From 02d4dd0752c90a3afd892fd35a0acc477f935864 Mon Sep 17 00:00:00 2001 From: wattanx Date: Sat, 6 Jul 2024 23:39:12 +0900 Subject: [PATCH 4/7] fix: use undefined rather than null for data fetching defaults --- .../bridge-schema/src/config/experimental.ts | 14 ++--- packages/bridge/src/app.ts | 41 ++++++++----- .../src/runtime/composables/asyncData.ts | 60 +++++++++++-------- .../bridge/src/runtime/composables/error.ts | 4 +- packages/bridge/src/runtime/defaults.js | 2 + packages/bridge/src/type-templates.ts | 16 ++++- packages/bridge/types.d.ts | 6 ++ playground/pages/async-data.vue | 8 +++ test/bridge.test.ts | 10 ++++ 9 files changed, 111 insertions(+), 50 deletions(-) create mode 100644 packages/bridge/src/runtime/defaults.js diff --git a/packages/bridge-schema/src/config/experimental.ts b/packages/bridge-schema/src/config/experimental.ts index a9522ebd..762ebd1e 100644 --- a/packages/bridge-schema/src/config/experimental.ts +++ b/packages/bridge-schema/src/config/experimental.ts @@ -10,9 +10,9 @@ export default defineUntypedSchema({ * Enable early access to future features or flags. * * It is currently not configurable but may be in future. - * @type {4} + * @type {3 | 4} */ - compatibilityVersion: 4, + compatibilityVersion: 3 }, experimental: { defaults: { @@ -24,15 +24,15 @@ export default defineUntypedSchema({ value: { async $resolve (val, get) { return val ?? ((await get('future') as Record).compatibilityVersion === 4 ? 'undefined' : 'null') - }, + } }, /** @type {'undefined' | 'null'} */ errorValue: { async $resolve (val, get) { return val ?? ((await get('future') as Record).compatibilityVersion === 4 ? 'undefined' : 'null') - }, - }, - }, + } + } + } } } -}) \ No newline at end of file +}) diff --git a/packages/bridge/src/app.ts b/packages/bridge/src/app.ts index 8b3588a8..bc83618c 100644 --- a/packages/bridge/src/app.ts +++ b/packages/bridge/src/app.ts @@ -4,7 +4,7 @@ import { normalize, resolve } from 'pathe' import { resolveImports } from 'mlly' import { applyDefaults } from 'untyped' import { NuxtConfigSchema } from '@nuxt/bridge-schema' -import { componentsTypeTemplate, schemaTemplate, middlewareTypeTemplate } from './type-templates' +import { componentsTypeTemplate, schemaTemplate, middlewareTypeTemplate, appDefaults } from './type-templates' import { distDir } from './dirs' import { VueCompat } from './vue-compat' import { globalMiddlewareTemplate } from './global-middleware-template' @@ -69,6 +69,7 @@ export async function setupAppBridge (_options: any) { nuxt.hook('prepare:types', ({ references }) => { references.push({ path: resolve(nuxt.options.buildDir, 'types/components.d.ts') }) references.push({ path: resolve(nuxt.options.buildDir, 'types/middleware.d.ts') }) + references.push({ path: resolve(nuxt.options.buildDir, 'types/app-defaults.d.ts') }) }) // Augment schema with module types @@ -107,20 +108,30 @@ export async function setupAppBridge (_options: any) { addTemplate(globalMiddlewareTemplate) - - if (nuxt.options.future?.compatibilityVersion === 4) { - // @ts-expect-error only partially supported by nuxt bridge - nuxt.options.experimental = (await applyDefaults({ future: NuxtConfigSchema['future'], experimental: NuxtConfigSchema['experimental'] }, nuxt.options.experimental)).experimental - - addTemplate({ - filename: 'nuxt.config.mjs', - getContents: (ctx) => { - return [ - `export const asyncDataDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.useAsyncData)}`, - ].join('\n\n') - } - }) - } + // @ts-expect-error only partially supported by nuxt bridge + nuxt.options.experimental = ( + await applyDefaults( + { future: NuxtConfigSchema.future, experimental: NuxtConfigSchema.experimental }, + { future: nuxt.options.future, experimental: nuxt.options.experimental } + ) + ).experimental; + + addTemplate({ + filename: 'nuxt.config.mjs', + getContents: (ctx) => { + return [ + `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'}` + ].join('\n\n') + } + }) + + addTemplate(appDefaults) // Alias vue3 utilities to vue2 const { dst: vueCompat } = addTemplate({ src: resolve(distDir, 'runtime/vue2-bridge.mjs') }) diff --git a/packages/bridge/src/runtime/composables/asyncData.ts b/packages/bridge/src/runtime/composables/asyncData.ts index 8d7f7a43..2f33cb17 100644 --- a/packages/bridge/src/runtime/composables/asyncData.ts +++ b/packages/bridge/src/runtime/composables/asyncData.ts @@ -5,6 +5,13 @@ import { useNuxtApp } from '../nuxt' import { toArray } from '../utils/toArray' import { createError } from './error' +// @ts-expect-error virtual file +import { asyncDataDefaults, resetAsyncDataToUndefined } from '#build/nuxt.config.mjs' + +// TODO: temporary module for backwards compatibility +// @ts-expect-error virtual file +import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults' + export type _Transform = (input: Input) => Output | Promise export type PickFrom> = T extends Array ? T : T extends Record ? Pick : T @@ -19,7 +26,7 @@ export interface AsyncDataOptions< ResT, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = null, + DefaultT = DefaultAsyncDataValue, > { /** * Whether to fetch on the server side. @@ -92,7 +99,7 @@ export interface _AsyncData { refresh: (opts?: AsyncDataExecuteOptions) => Promise execute: (opts?: AsyncDataExecuteOptions) => Promise clear: () => void - error: Ref + error: Ref status: Ref } @@ -106,11 +113,11 @@ export function useAsyncData< DataE = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = null, + DefaultT = DefaultAsyncDataValue, > ( handler: (ctx?: NuxtAppCompat) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useAsyncData< ResT, DataE = Error, @@ -120,18 +127,18 @@ export function useAsyncData< > ( handler: (ctx?: NuxtAppCompat) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useAsyncData< ResT, DataE = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = null, + DefaultT = DefaultAsyncDataValue, > ( key: string, handler: (ctx?: NuxtAppCompat) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useAsyncData< ResT, DataE = Error, @@ -142,14 +149,14 @@ export function useAsyncData< key: string, handler: (ctx?: NuxtAppCompat) => Promise, options?: AsyncDataOptions -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useAsyncData< ResT, DataE = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = null, -> (...args: any[]): AsyncData, DataE | null> { + DefaultT = DefaultAsyncDataValue, +> (...args: any[]): AsyncData, DataE | DefaultAsyncDataErrorValue> { const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined if (typeof args[0] !== 'string') { args.unshift(autoKey) } @@ -168,7 +175,7 @@ export function useAsyncData< const nuxt = useNuxtApp() // Used to get default values - const getDefault = () => null + const getDefault = () => asyncDataDefaults.value const getDefaultCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key] // Apply defaults @@ -189,7 +196,7 @@ export function useAsyncData< // Create or use a shared asyncData entity if (!nuxt._asyncData[key] || !options.immediate) { - nuxt.payload._errors[key] = nuxt.payload._errors[key] ?? null + nuxt.payload._errors[key] = nuxt.payload._errors[key] ?? asyncDataDefaults.errorValue const _ref = options.deep ? ref : shallowRef @@ -197,7 +204,8 @@ export function useAsyncData< data: _ref(options.getCachedData!(key, nuxt) ?? options.default!()), pending: ref(!hasCachedData()), error: toRef(nuxt.payload._errors, key), - status: ref('idle') + status: ref('idle'), + _default: options.default!, } } @@ -242,7 +250,7 @@ export function useAsyncData< nuxt.payload.data[key] = result asyncData.data.value = result - asyncData.error.value = null + asyncData.error.value = asyncDataDefaults.errorValue asyncData.status.value = 'success' }) .catch((error: any) => { @@ -336,11 +344,11 @@ export function useLazyAsyncData< DataE = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = null, + DefaultT = DefaultAsyncDataValue, > ( handler: (ctx?: NuxtAppCompat) => Promise, options?: Omit, 'lazy'> -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useLazyAsyncData< ResT, DataE = Error, @@ -350,18 +358,18 @@ export function useLazyAsyncData< > ( handler: (ctx?: NuxtAppCompat) => Promise, options?: Omit, 'lazy'> -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = null, + DefaultT = DefaultAsyncDataValue, > ( key: string, handler: (ctx?: NuxtAppCompat) => Promise, options?: Omit, 'lazy'> -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useLazyAsyncData< ResT, DataE = Error, @@ -372,15 +380,15 @@ export function useLazyAsyncData< key: string, handler: (ctx?: NuxtAppCompat) => Promise, options?: Omit, 'lazy'> -): AsyncData | DefaultT, DataE | null> +): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, PickKeys extends KeysOf = KeysOf, - DefaultT = null, -> (...args: any[]): AsyncData | DefaultT, DataE | null> { + DefaultT = DefaultAsyncDataValue, +> (...args: any[]): AsyncData | DefaultT, DataE | DefaultAsyncDataErrorValue> { 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?: NuxtAppCompat) => Promise, AsyncDataOptions] @@ -412,16 +420,16 @@ export function clearNuxtData (keys?: string | string[] | ((key: string) => bool function clearNuxtDataByKey (nuxtApp: NuxtAppCompat, key: string): void { if (key in nuxtApp.payload.data) { - nuxtApp.payload.data[key] = undefined + nuxtApp.payload.data[key] = asyncDataDefaults.value } 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 = 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' } diff --git a/packages/bridge/src/runtime/composables/error.ts b/packages/bridge/src/runtime/composables/error.ts index 1e8a3d9d..d412c74d 100644 --- a/packages/bridge/src/runtime/composables/error.ts +++ b/packages/bridge/src/runtime/composables/error.ts @@ -1,6 +1,8 @@ import { createError as _createError, H3Error } from 'h3' import { toRef } from 'vue' import { useNuxtApp } from '../nuxt' +// @ts-expect-error virtual file +import { nuxtDefaultErrorValue } from '#build/nuxt.config.mjs' export const useError = () => toRef(useNuxtApp().payload, 'error') @@ -36,7 +38,7 @@ export const clearError = async (options: { redirect?: string } = {}) => { if (options.redirect) { await nuxtApp.$router.replace(options.redirect) } - error.value = null + error.value = nuxtDefaultErrorValue } export const isNuxtError = (err?: string | object): err is NuxtError => !!(err && typeof err === 'object' && ('__nuxt_error' in err)) diff --git a/packages/bridge/src/runtime/defaults.js b/packages/bridge/src/runtime/defaults.js new file mode 100644 index 00000000..8a0e47a1 --- /dev/null +++ b/packages/bridge/src/runtime/defaults.js @@ -0,0 +1,2 @@ +// TODO: temporary module for backwards compatibility +export {} diff --git a/packages/bridge/src/type-templates.ts b/packages/bridge/src/type-templates.ts index fe101636..2819651e 100644 --- a/packages/bridge/src/type-templates.ts +++ b/packages/bridge/src/type-templates.ts @@ -1,5 +1,5 @@ import { isAbsolute, relative, join, resolve } from 'pathe' -import type { Component, Nuxt, NuxtApp, NuxtTemplate } from '@nuxt/schema' +import type { Component, Nuxt, NuxtApp, NuxtTemplate, NuxtTypeTemplate } from '@nuxt/schema' import { genDynamicImport, genString } from 'knitwork' import { defu } from 'defu' @@ -150,3 +150,17 @@ export const schemaTemplate: NuxtTemplate = { ].join('\n') } } + +export const appDefaults: NuxtTypeTemplate = { + filename: 'types/app-defaults.d.ts', + getContents: (ctx) => { + const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4 + return ` +declare module '#app/defaults' { + type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'} + type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'} + type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'} + type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''} +}` + } +} diff --git a/packages/bridge/types.d.ts b/packages/bridge/types.d.ts index a47e798b..d87a4c28 100644 --- a/packages/bridge/types.d.ts +++ b/packages/bridge/types.d.ts @@ -80,4 +80,10 @@ declare module 'nitropack' { } } +declare module '#app/defaults' { + type DefaultAsyncDataErrorValue = undefined + type DefaultAsyncDataValue = undefined + type DefaultErrorValue = undefined +} + export declare function defineNuxtConfig (config: NuxtConfig): NuxtConfig diff --git a/playground/pages/async-data.vue b/playground/pages/async-data.vue index 05bf4536..c2333756 100644 --- a/playground/pages/async-data.vue +++ b/playground/pages/async-data.vue @@ -28,6 +28,13 @@ const { data: clearableData2, clear } = useLazyAsyncData('clearableData-2', asyn } }) +const { data: immediateFalseData } = useLazyAsyncData('immediateFalse', async () => { + const text = await $fetch('/api/hello') + return { + text + } +}, { immediate: false }) + if (process.server) { clearNuxtData('clearableData-1') clear() @@ -43,6 +50,7 @@ if (process.server) {