Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use undefined rather than null for data fetching defaults #1265

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/bridge-schema/src/config/experimental.ts
Original file line number Diff line number Diff line change
@@ -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 {3 | 4}
*/
compatibilityVersion: 3
},
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<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')
}
}
}
}
}
})
2 changes: 2 additions & 0 deletions packages/bridge-schema/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -31,6 +32,7 @@ export default {
...build,
...cli,
...common,
...experimental,
...generate,
...messages,
...render,
Expand Down
3 changes: 3 additions & 0 deletions packages/bridge/module.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion packages/bridge/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { useNuxt, addTemplate, resolveAlias, addWebpackPlugin, addVitePlugin, ad
import { NuxtModule } from '@nuxt/schema'
import { normalize, resolve } from 'pathe'
import { resolveImports } from 'mlly'
import { componentsTypeTemplate, schemaTemplate, middlewareTypeTemplate } from './type-templates'
import { applyDefaults } from 'untyped'
import { NuxtConfigSchema } from '@nuxt/bridge-schema'
import { componentsTypeTemplate, schemaTemplate, middlewareTypeTemplate, appDefaults } from './type-templates'
import { distDir } from './dirs'
import { VueCompat } from './vue-compat'
import { globalMiddlewareTemplate } from './global-middleware-template'
Expand Down Expand Up @@ -67,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
Expand Down Expand Up @@ -105,6 +108,31 @@ export async function setupAppBridge (_options: any) {

addTemplate(globalMiddlewareTemplate)

// @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
Comment on lines +112 to +117
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only want to use the default values for future and 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') })
addWebpackPlugin(VueCompat.webpack({ src: vueCompat }))
Expand Down
11 changes: 3 additions & 8 deletions packages/bridge/src/global-middleware-template.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> }
}

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(',')}}`)
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/src/imports/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const granularAppPresets: InlinePreset[] = [
from: '#app/composables/state'
},
{
imports: ['useLazyAsyncData', 'refreshNuxtData', 'clearNuxtData'],
imports: ['useLazyAsyncData', 'useNuxtData', 'refreshNuxtData', 'clearNuxtData'],
from: '#app/composables/asyncData'
},
{
Expand Down
86 changes: 59 additions & 27 deletions packages/bridge/src/runtime/composables/asyncData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { onBeforeMount, onServerPrefetch, onUnmounted, ref, shallowRef, getCurrentInstance, watch, toRef, unref, getCurrentScope, onScopeDispose } from 'vue'
import { computed, onBeforeMount, onServerPrefetch, onUnmounted, ref, shallowRef, getCurrentInstance, watch, toRef, unref, getCurrentScope, onScopeDispose } from 'vue'
import type { Ref, WatchSource } from 'vue'
import type { NuxtAppCompat } from '@nuxt/bridge-schema'
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 = any, Output = any> = (input: Input) => Output | Promise<Output>

export type PickFrom<T, K extends Array<string>> = T extends Array<any> ? T : T extends Record<string, any> ? Pick<T, K[number]> : T
Expand All @@ -19,7 +26,7 @@ export interface AsyncDataOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> {
/**
* Whether to fetch on the server side.
Expand Down Expand Up @@ -92,7 +99,7 @@ export interface _AsyncData<DataT, ErrorT> {
refresh: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
execute: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
clear: () => void
error: Ref<ErrorT | null>
error: Ref<ErrorT | DefaultAsyncDataErrorValue>
status: Ref<AsyncDataRequestStatus>
}

Expand All @@ -106,11 +113,11 @@ export function useAsyncData<
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>
export function useAsyncData<
ResT,
DataE = Error,
Expand All @@ -120,18 +127,18 @@ export function useAsyncData<
> (
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>
export function useAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
key: string,
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>
export function useAsyncData<
ResT,
DataE = Error,
Expand All @@ -142,14 +149,14 @@ export function useAsyncData<
key: string,
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>
export function useAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, DataE | null> {
DefaultT = DefaultAsyncDataValue,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, DataE | DefaultAsyncDataErrorValue> {
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) }

Expand All @@ -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
Expand All @@ -189,15 +196,16 @@ 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

nuxt._asyncData[key] = {
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!
}
}

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -336,11 +344,11 @@ export function useLazyAsyncData<
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
Expand All @@ -350,18 +358,18 @@ export function useLazyAsyncData<
> (
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
key: string,
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
Expand All @@ -372,22 +380,46 @@ export function useLazyAsyncData<
key: string,
handler: (ctx?: NuxtAppCompat) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataErrorValue>

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 | 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<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
// @ts-expect-error we pass an extra argument to prevent a key being injected
return useAsyncData(key, handler, { ...options, lazy: true }, null)
}

export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | DefaultAsyncDataErrorValue> } {
const nuxtApp = useNuxtApp()

// Initialize value when key is not already set
if (!(key in nuxtApp.payload.data)) {
nuxtApp.payload.data[key] = undefined
}

return {
data: computed({
get () {
return nuxtApp._asyncData[key]?.data.value ?? nuxtApp.payload.data[key]
},
set (value) {
if (nuxtApp._asyncData[key]) {
nuxtApp._asyncData[key]!.data.value = value
} else {
nuxtApp.payload.data[key] = value
}
}
})
}
}

export function refreshNuxtData (keys?: string | string[]): Promise<void> {
if (process.server) {
return Promise.resolve()
Expand All @@ -412,16 +444,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'
}
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge/src/runtime/composables/error.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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))
Expand Down
Loading