Skip to content

Commit

Permalink
feat!: port upstream useFetch, useLazyFetch
Browse files Browse the repository at this point in the history
  • Loading branch information
wattanx committed Mar 20, 2024
1 parent c16a529 commit 2c2e693
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 36 deletions.
2 changes: 1 addition & 1 deletion packages/bridge/src/runtime/composables/asyncData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type PickFrom<T, K extends Array<string>> = T extends Array<any> ? T : T
export type KeysOf<T> = Array<keyof T extends string ? keyof T : string>
export type KeyOfRes<Transform extends _Transform> = KeysOf<ReturnType<Transform>>

type MultiWatchSources = (WatchSource<unknown> | object)[]
export type MultiWatchSources = (WatchSource<unknown> | object)[]

export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'

Expand Down
250 changes: 215 additions & 35 deletions packages/bridge/src/runtime/composables/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,248 @@
import type { FetchOptions, FetchRequest } from 'ofetch'
import type { TypedInternalResponse } from 'nitropack'
import type { FetchOptions, FetchError } from 'ofetch'
import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack'
import { hash } from 'ohash'
import { computed, isRef } from 'vue'
import { computed, unref, reactive } from 'vue'
import type { Ref } from 'vue'
import type { AsyncDataOptions, _Transform, KeyOfRes } from './asyncData'
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
import { useAsyncData } from './asyncData'
import { useRequestFetch } from './ssr'

export type FetchResult<ReqT extends FetchRequest> = TypedInternalResponse<ReqT, unknown>
// support uppercase methods, detail: https://github.com/nuxt/nuxt/issues/22313
type AvailableRouterMethod<R extends NitroFetchRequest> = _AvailableRouterMethod<R> | Uppercase<_AvailableRouterMethod<R>>

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] : ComputedOptions<T[K]> | Ref<T[K]> | T[K]
}

interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> extends FetchOptions {
method?: M;
}

type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R>> = ComputedOptions<NitroFetchOptions<R, M>>

export interface UseFetchOptions<
DataT,
Transform extends _Transform<DataT, any> = _Transform<DataT, DataT>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
> extends
AsyncDataOptions<DataT, Transform, PickKeys>, FetchOptions { key?: string }
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
R extends NitroFetchRequest = string & {},
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> {
key?: string
$fetch?: typeof globalThis.$fetch
watch?: MultiWatchSources | false
}

export function useFetch<
ResT = void,
ReqT extends FetchRequest = FetchRequest,
_ResT = ResT extends void ? FetchResult<ReqT> : ResT,
Transform extends (res: _ResT) => any = (res: _ResT) => _ResT,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
export function useFetch<
ResT = void,
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = DataT,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
export function useFetch<
ResT = void,
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts: UseFetchOptions<_ResT, Transform, PickKeys> = {}
arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
arg2?: string
) {
const key = '$f_' + (opts.key || hash([request, opts]))
const _request = computed<FetchRequest>(() => {
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]

const _request = computed(() => {
let r = request
if (typeof r === 'function') {
r = r()
}
return isRef(r) ? r.value : r
return toValue(r)
})

const _fetchOptions = {
...opts,
cache: typeof opts.cache === 'boolean' ? undefined : opts.cache
const _key = opts.key || hash([autoKey, typeof _request.value === 'string' ? _request.value : '', ...generateOptionSegments(opts)])
if (!_key || typeof _key !== 'string') {
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
}
if (!request) {
throw new Error('[nuxt] [useFetch] request is missing.')
}

const _asyncDataOptions: AsyncDataOptions<_ResT, Transform, PickKeys> = {
...opts,
watch: [
_request,
...(opts.watch || [])
]
const key = _key === autoKey ? '$f' + _key : _key

if (!opts.baseURL && typeof _request.value === 'string' && (_request.value[0] === '/' && _request.value[1] === '/')) {
throw new Error('[nuxt] [useFetch] the request URL must not start with "//".')
}

const asyncData = useAsyncData(key, () => {
return $fetch(_request.value, _fetchOptions) as Promise<_ResT>
const {
server,
lazy,
default: defaultFn,
transform,
pick,
watch,
immediate,
getCachedData,
deep,
dedupe,
...fetchOptions
} = opts

const _fetchOptions = reactive({
...fetchOptions,
cache: typeof opts.cache === 'boolean' ? undefined : opts.cache
})

const _asyncDataOptions: AsyncDataOptions<_ResT, DataT, PickKeys, DefaultT> = {
server,
lazy,
default: defaultFn,
transform,
pick,
immediate,
getCachedData,
deep,
dedupe,
watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])]
}

let controller: AbortController

const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => {
controller?.abort?.()
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController

/**
* Workaround for `timeout` not working due to custom abort controller
* TODO: remove this when upstream issue is resolved
* @see https://github.com/unjs/ofetch/issues/326
* @see https://github.com/unjs/ofetch/blob/bb2d72baa5d3f332a2185c20fc04e35d2c3e258d/src/fetch.ts#L152
*/
const timeoutLength = toValue(opts.timeout)
if (timeoutLength) {
setTimeout(() => controller.abort(), timeoutLength)
}

let _$fetch = opts.$fetch || globalThis.$fetch

// Use fetch with request context and headers for server direct API calls
if (process.server && !opts.$fetch) {
const isLocalFetch = typeof _request.value === 'string' && _request.value[0] === '/' && (!toValue(opts.baseURL) || toValue(opts.baseURL)![0] === '/')
if (isLocalFetch) {
_$fetch = useRequestFetch()
}
}

return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any) as Promise<_ResT>
}, _asyncDataOptions)

return asyncData
}

/**
* Fetch data from an API endpoint with an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-fetch}
* @param request The URL to fetch
* @param opts extends $fetch options and useAsyncData options
*/
export function useLazyFetch<
ResT = void,
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
export function useLazyFetch<
ResT = void,
ReqT extends string = string,
_ResT = ResT extends void ? FetchResult<ReqT> : ResT,
Transform extends (res: _ResT) => any = (res: _ResT) => _ResT,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = DataT,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts: Omit<UseFetchOptions<_ResT, Transform, PickKeys>, 'lazy'> = {}
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
export function useLazyFetch<
ResT = void,
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>,
arg2?: string
) {
return useFetch(request, { ...opts, lazy: true })
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
return useFetch<ResT, ErrorT, ReqT, Method, _ResT, DataT, PickKeys, DefaultT>(request, {
...opts,
lazy: true
},
// @ts-expect-error we pass an extra argument with the resolved auto-key to prevent another from being injected
autoKey)
}

type MaybeRef<T> = T | Ref<T>
type MaybeRefOrGetter<T> = MaybeRef<T> | (() => T)
type AnyFn = (...args: any[]) => any

function toValue<T> (r: MaybeRefOrGetter<T>): T {
return typeof r === 'function'
? (r as AnyFn)()
: unref(r)
}

function generateOptionSegments <_ResT, DataT, DefaultT> (opts: UseFetchOptions<_ResT, DataT, any, DefaultT, any, any>) {
const segments: Array<string | undefined | Record<string, string>> = [
toValue(opts.method as MaybeRef<string | undefined> | undefined)?.toUpperCase() || 'GET',
toValue(opts.baseURL)
]
for (const _obj of [opts.params || opts.query]) {
const obj = toValue(_obj)
if (!obj) { continue }

const unwrapped: Record<string, string> = {}
for (const [key, value] of Object.entries(obj)) {
unwrapped[toValue(key)] = toValue(value)
}
segments.push(unwrapped)
}
return segments
}
7 changes: 7 additions & 0 deletions packages/bridge/src/runtime/composables/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ export function useRequestEvent (nuxtApp: NuxtAppCompat = useNuxtApp()): H3Event
nuxtApp.ssrContext._event = createEvent(nuxtApp.ssrContext?.req, nuxtApp.ssrContext?.res)
return nuxtApp.ssrContext._event
}

export function useRequestFetch (): typeof global.$fetch {
if (process.client) {
return globalThis.$fetch
}
return useRequestEvent()?.$fetch as typeof globalThis.$fetch || globalThis.$fetch
}
17 changes: 17 additions & 0 deletions playground/pages/lazy-fetch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup>
const name = ref('')
const query = computed(() => ({ name: name.value }))
const { data, refresh } = useLazyFetch('/api/hello', { query, watch: false })
</script>

<template>
<div>
<h1>Lazy Fetch</h1>
<input v-model="name">
<button @click="() => refresh()">
Refresh
</button>
<div>{{ data }}</div>
</div>
</template>
6 changes: 6 additions & 0 deletions test/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ describe('nuxt composables', () => {
expect(html).toContain('error2: Error: fetch-2 error')
expect(html).toContain('error3: Error: fetch-3 error')
})

it('should render api response', async () => {
const html = await $fetch('/lazy-fetch')

expect(html).toContain('<div>Hello API</div>')
})
})

describe('head tags', () => {
Expand Down

0 comments on commit 2c2e693

Please sign in to comment.