@tanstack/start-client-core
Version:
Modern and scalable routing for React applications
935 lines (854 loc) • 26.4 kB
text/typescript
import { mergeHeaders } from '@tanstack/router-core/ssr/client'
import { isRedirect, parseRedirect } from '@tanstack/router-core'
import { isServer } from '@tanstack/router-core/isServer'
import { TSS_SERVER_FUNCTION_FACTORY } from './constants'
import { getStartOptions } from './getStartOptions'
import { getStartContextServerOnly } from './getStartContextServerOnly'
import { createNullProtoObject, safeObjectMerge } from './safeObjectMerge'
import type {
ClientFnMeta,
ServerFnMeta,
TSS_SERVER_FUNCTION,
} from './constants'
import type {
AnyValidator,
Constrain,
Expand,
Register,
RegisteredSerializableInput,
ResolveValidatorInput,
ValidateSerializable,
ValidateSerializableInput,
Validator,
} from '@tanstack/router-core'
import type {
AnyFunctionMiddleware,
AnyRequestMiddleware,
AssignAllServerFnContext,
IntersectAllValidatorInputs,
IntersectAllValidatorOutputs,
} from './createMiddleware'
export type ServerFnStrict = boolean | { input?: boolean; output?: boolean }
export interface ServerFnOptions<
TMethod extends Method = Method,
TStrict extends ServerFnStrict = true,
> {
method?: TMethod
strict?: TStrict
}
export type ServerFnStrictInput<TStrict extends ServerFnStrict> =
TStrict extends false
? false
: TStrict extends { input: infer TInput extends boolean }
? TInput
: true
export type ServerFnStrictOutput<TStrict extends ServerFnStrict> =
TStrict extends false
? false
: TStrict extends { output: infer TOutput extends boolean }
? TOutput
: true
export type CreateServerFn<TRegister> = <
TMethod extends Method,
TStrict extends ServerFnStrict = true,
TResponse = unknown,
TMiddlewares = undefined,
TInputValidator = undefined,
>(
options?: ServerFnOptions<TMethod, TStrict>,
__opts?: ServerFnBaseOptions<
TRegister,
TMethod,
TResponse,
TMiddlewares,
TInputValidator,
TStrict
>,
) => ServerFnBuilder<TRegister, TMethod, TStrict>
export const createServerFn: CreateServerFn<Register> = (options, __opts) => {
const resolvedOptions = (__opts || options || {}) as ServerFnBaseOptions<
any,
any,
any,
any,
any,
any
>
if (typeof resolvedOptions.method === 'undefined') {
resolvedOptions.method = 'GET' as Method
}
const res: ServerFnBuilder<Register, Method, ServerFnStrict> = {
options: resolvedOptions,
middleware: (middleware) => {
// multiple calls to `middleware()` merge the middlewares with the previously supplied ones
// this is primarily useful for letting users create their own abstractions on top of `createServerFn`
const newMiddleware = [...(resolvedOptions.middleware || [])]
// forEach skips holes and ignores items appended during iteration
middleware.forEach((item) => {
if (TSS_SERVER_FUNCTION_FACTORY in item) {
if (item.options.middleware) {
newMiddleware.push(...item.options.middleware)
}
} else {
newMiddleware.push(item)
}
})
const newOptions = {
...resolvedOptions,
middleware: newMiddleware,
}
const res = createServerFn(undefined, newOptions)
res[TSS_SERVER_FUNCTION_FACTORY] = true
return res
},
validator: (validator) => {
const newOptions = {
...resolvedOptions,
validator,
}
return createServerFn(undefined, newOptions) as any
},
handler: (...args) => {
// This function signature changes due to AST transformations
// in the babel plugin. We need to cast it to the correct
// function signature post-transformation
const [extractedFn, serverFn] = args as unknown as [
CompiledFetcherFn<Register, any>,
ServerFn<Register, Method, any, any, any>,
]
// Keep the original function around so we can use it
// in the server environment
const newOptions = { ...resolvedOptions, extractedFn, serverFn }
const resolvedMiddleware = [
...(newOptions.middleware || []),
serverFnBaseToMiddleware(newOptions),
]
// We want to make sure the new function has the same
// properties as the original function
// Propagate the declared HTTP method onto the extracted handler
// so the manifest-exported symbol (resolved by getServerFnById)
// carries `method`, enabling the server handler to reject
// mismatched HTTP methods before parsing request payloads.
;(extractedFn as any).method = resolvedOptions.method
return Object.assign(
async (opts?: CompiledFetcherFnOptions) => {
// Start by executing the client-side middleware chain
const result = await executeMiddleware(resolvedMiddleware, 'client', {
...extractedFn,
...newOptions,
data: opts?.data,
headers: opts?.headers,
signal: opts?.signal,
fetch: opts?.fetch,
context: createNullProtoObject(),
})
// Only a failed call has an `error` key. Its value is whatever was
// thrown, so a thrown undefined or 0 still rejects the call.
if ('error' in result) {
throw parseRedirect(result.error) ?? result.error
}
return result.result
},
{
// This copies over the URL, function ID
...extractedFn,
// Expose the declared HTTP method so the server handler
// can reject mismatched methods before parsing payloads
method: resolvedOptions.method,
// The extracted function on the server-side calls
// this function. Browsers never execute a server function.
__executeServer:
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- isServer is only known outside development
(isServer ?? typeof window === 'undefined')
? async (opts: any) => {
const startContext = getStartContextServerOnly()
const serverContextAfterGlobalMiddlewares =
startContext.contextAfterGlobalMiddlewares
const ctx = {
...extractedFn,
data: opts.data,
method: opts.method ?? resolvedOptions.method,
// Ensure we use the full serverFnMeta from the provider file's extractedFn
// (which has id, name, filename) rather than the partial one from SSR/client
// callers (which only has id)
serverFnMeta: extractedFn.serverFnMeta,
// Merge client context first so trusted server middleware context wins.
context: safeObjectMerge(
opts.context,
serverContextAfterGlobalMiddlewares,
),
request: startContext.request,
}
const result = await executeMiddleware(
resolvedMiddleware,
'server',
ctx,
)
// Only send the result and sendContext back to the client. A
// failed call sends only its thrown value, under an `error` key.
if ('error' in result) {
return { error: result.error }
}
return result.sendContext === undefined
? { result: result.result }
: { result: result.result, context: result.sendContext }
}
: undefined,
},
) as any
},
} as ServerFnBuilder<Register, Method, ServerFnStrict>
const fun = (options?: ServerFnOptions<Method, ServerFnStrict>) => {
const newOptions = {
...resolvedOptions,
...options,
}
return createServerFn(undefined, newOptions)
}
return Object.assign(fun, res) as any
}
function mergeOptionalSendContext(
target: Record<string, unknown> | undefined,
source: Record<string, unknown> | null | undefined,
) {
return target === undefined && source === undefined
? undefined
: safeObjectMerge(target, source)
}
function mergeOptionalHeaders(
target: HeadersInit | undefined,
source: HeadersInit | undefined,
) {
return target || source ? mergeHeaders(target, source) : undefined
}
export async function executeMiddleware(
middlewares: Array<AnyFunctionMiddleware | AnyRequestMiddleware>,
env: 'client' | 'server',
opts: ServerFnMiddlewareOptions,
): Promise<ServerFnMiddlewareResult> {
const globalMiddlewares: Array<AnyFunctionMiddleware | AnyRequestMiddleware> =
getStartOptions()?.functionMiddleware || []
// On the server, flattening skips middlewares that already executed in the
// request phase to prevent duplicate execution (issue #5239)
const flattenedMiddlewares = flattenMiddlewares(
globalMiddlewares.concat(middlewares),
undefined,
env === 'server'
? new Set(
getStartContextServerOnly({ throwIfNotFound: false })
?.executedRequestMiddlewares,
)
: undefined,
)
let index = 0
// Runs the remaining middlewares. A middleware's next() continues with its
// context merged with what it passed. Failures reject every pending next()
// up to the top, where they become `{ error }`.
const callNextMiddleware = async (
callerCtx: ServerFnMiddlewareResult,
userCtx?: ServerFnMiddlewareResult,
): Promise<ServerFnMiddlewareResult> => {
let ctx = callerCtx
if (userCtx !== undefined) {
// Assign after the spreads: an object literal with properties after a
// second spread defines each of them through a slow runtime call.
ctx = { ...callerCtx, ...userCtx }
// Use safeObjectMerge for context objects to prevent prototype pollution
ctx.context = safeObjectMerge(callerCtx.context, userCtx.context)
ctx.sendContext = mergeOptionalSendContext(
callerCtx.sendContext,
userCtx.sendContext,
)
ctx.headers = mergeOptionalHeaders(callerCtx.headers, userCtx.headers)
ctx._callSiteFetch = callerCtx._callSiteFetch
ctx.fetch = callerCtx._callSiteFetch ?? userCtx.fetch ?? callerCtx.fetch
ctx.result =
userCtx.result !== undefined
? userCtx.result
: userCtx instanceof Response
? userCtx
: callerCtx.result
}
let result = ctx
// Middleware without a function for this environment only validates.
while (index < flattenedMiddlewares.length) {
const options = flattenedMiddlewares[index++]!.options as {
validator?: AnyValidator
client?: MiddlewareFn
server?: MiddlewareFn
}
const validator = options.validator
if (validator && env === 'server') {
// Execute the middleware's input function
ctx.data = await execValidator(validator, ctx.data)
}
const middlewareFn = env === 'client' ? options.client : options.server
if (middlewareFn) {
result = await middlewareFn({
...ctx,
next: (nextCtx = {} as ServerFnMiddlewareResult) =>
callNextMiddleware(ctx, nextCtx),
})
if (result instanceof Response) {
// A returned redirect fails the call; any other Response is its
// result.
if (isRedirect(result)) {
throw result
}
result = { ...ctx, result }
} else if (!(result as any)) {
throw new Error(
'User middleware returned undefined. You must call next() or return a result in your middlewares.',
)
}
break
}
}
if ('error' in result) {
throw result.error
}
return result
}
// Start the middleware chain
const initialCtx = {
...opts,
// Client next() results always expose HeadersInit. Server middleware does
// not require a headers value unless one was explicitly supplied.
headers: opts.headers || (env === 'client' ? {} : undefined),
sendContext: opts.sendContext,
context: opts.context || createNullProtoObject(),
_callSiteFetch: opts.fetch,
}
if (initialCtx.sendContext !== undefined) {
initialCtx.sendContext ||= {}
}
try {
return await callNextMiddleware(initialCtx)
} catch (error) {
return { ...initialCtx, error }
}
}
export type CompiledFetcherFnOptions = {
method: Method
data: unknown
headers?: HeadersInit
signal?: AbortSignal
fetch?: CustomFetch
context?: any
}
export type Fetcher<TMiddlewares, TInputValidator, TResponse> =
undefined extends IntersectAllValidatorInputs<TMiddlewares, TInputValidator>
? OptionalFetcher<TMiddlewares, TInputValidator, TResponse>
: RequiredFetcher<TMiddlewares, TInputValidator, TResponse>
export interface FetcherBase {
[TSS_SERVER_FUNCTION]: true
url: string
method: Method
__executeServer: (opts: {
method: Method
data: unknown
headers?: HeadersInit
context?: any
}) => Promise<unknown>
}
export interface OptionalFetcher<
TMiddlewares,
TInputValidator,
TResponse,
> extends FetcherBase {
(
options?: OptionalFetcherDataOptions<TMiddlewares, TInputValidator>,
): Promise<Awaited<TResponse>>
}
export interface RequiredFetcher<
TMiddlewares,
TInputValidator,
TResponse,
> extends FetcherBase {
(
opts: RequiredFetcherDataOptions<TMiddlewares, TInputValidator>,
): Promise<Awaited<TResponse>>
}
// Ideally, this type should just be `export type CustomFetch = typeof globalThis.fetch`, but that conflicts with the type overrides the `bun-types` package - a dependency of unplugin.
// Relevant bun issues:
// - https://github.com/oven-sh/bun/issues/23500
// - https://github.com/oven-sh/bun/issues/23741
export type CustomFetch = typeof fetch extends (...args: infer A) => infer R
? (...args: A) => R
: never
export type FetcherBaseOptions = {
headers?: HeadersInit
signal?: AbortSignal
fetch?: CustomFetch
}
export interface OptionalFetcherDataOptions<
TMiddlewares,
TInputValidator,
> extends FetcherBaseOptions {
data?: Expand<IntersectAllValidatorInputs<TMiddlewares, TInputValidator>>
}
export interface RequiredFetcherDataOptions<
TMiddlewares,
TInputValidator,
> extends FetcherBaseOptions {
data: Expand<IntersectAllValidatorInputs<TMiddlewares, TInputValidator>>
}
export type RscStream<T> = {
__cacheState: T
}
export type Method = 'GET' | 'POST'
export type ServerFnReturnType<
TRegister,
TResponse,
TStrict extends ServerFnStrict = true,
> =
ServerFnStrictOutput<TStrict> extends false
? TResponse
: TResponse extends PromiseLike<infer U>
? Promise<ServerFnReturnType<TRegister, U, TStrict>>
: TResponse extends Response
? TResponse
: ValidateSerializableInput<TRegister, TResponse>
export type ServerFn<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TResponse,
TStrict extends ServerFnStrict = true,
> = (
ctx: ServerFnCtx<TRegister, TMethod, TMiddlewares, TInputValidator>,
) => ServerFnReturnType<TRegister, TResponse, TStrict>
export interface ServerFnCtx<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
> {
data: Expand<IntersectAllValidatorOutputs<TMiddlewares, TInputValidator>>
serverFnMeta: ServerFnMeta
context: Expand<AssignAllServerFnContext<TRegister, TMiddlewares, {}>>
method: TMethod
}
export type CompiledFetcherFn<TRegister, TResponse> = {
(
opts: CompiledFetcherFnOptions & ServerFnBaseOptions<TRegister, Method>,
): Promise<TResponse>
url: string
serverFnMeta: ServerFnMeta
}
export type ServerFnBaseOptions<
TRegister,
TMethod extends Method = 'GET',
TResponse = unknown,
TMiddlewares = unknown,
TInputValidator = unknown,
TStrict extends ServerFnStrict = true,
> = {
method: TMethod
strict?: TStrict
middleware?: Constrain<
TMiddlewares,
ReadonlyArray<AnyFunctionMiddleware | AnyRequestMiddleware>
>
validator?: ConstrainValidator<TRegister, TMethod, TInputValidator, TStrict>
extractedFn?: CompiledFetcherFn<TRegister, TResponse>
serverFn?: ServerFn<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TResponse,
TStrict
>
}
export type ValidateValidatorInput<
TRegister,
TMethod extends Method,
TInputValidator,
TStrict extends ServerFnStrict = true,
> =
ServerFnStrictInput<TStrict> extends false
? ResolveValidatorInput<TInputValidator>
: TMethod extends 'POST'
? ResolveValidatorInput<TInputValidator> extends FormData
? ResolveValidatorInput<TInputValidator>
: ValidateSerializable<
ResolveValidatorInput<TInputValidator>,
RegisteredSerializableInput<TRegister>
>
: ValidateSerializable<
ResolveValidatorInput<TInputValidator>,
RegisteredSerializableInput<TRegister>
>
export type ValidateValidator<
TRegister,
TMethod extends Method,
TInputValidator,
TStrict extends ServerFnStrict = true,
> =
ValidateValidatorInput<
TRegister,
TMethod,
TInputValidator,
TStrict
> extends infer TInput
? Validator<TInput, any>
: never
export type ConstrainValidator<
TRegister,
TMethod extends Method,
TInputValidator,
TStrict extends ServerFnStrict = true,
> =
| (unknown extends TInputValidator
? TInputValidator
: ResolveValidatorInput<TInputValidator> extends ValidateValidator<
TRegister,
TMethod,
TInputValidator,
TStrict
>
? TInputValidator
: never)
| ValidateValidator<TRegister, TMethod, TInputValidator, TStrict>
export type AppendMiddlewares<TMiddlewares, TNewMiddlewares> =
TMiddlewares extends ReadonlyArray<any>
? TNewMiddlewares extends ReadonlyArray<any>
? readonly [...TMiddlewares, ...TNewMiddlewares]
: TMiddlewares
: TNewMiddlewares
export interface ServerFnMiddleware<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
> {
middleware: <const TNewMiddlewares>(
middlewares: Constrain<
TNewMiddlewares,
ReadonlyArray<AnyFunctionMiddleware | AnyRequestMiddleware | AnyServerFn>
>,
) => ServerFnAfterMiddleware<
TRegister,
TMethod,
AppendMiddlewares<TMiddlewares, TNewMiddlewares>,
TInputValidator,
TStrict
>
}
export interface ServerFnAfterMiddleware<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
>
extends
ServerFnWithTypes<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
undefined,
TStrict
>,
ServerFnMiddleware<TRegister, TMethod, TMiddlewares, undefined, TStrict>,
ServerFnValidator<TRegister, TMethod, TMiddlewares, TStrict>,
ServerFnHandler<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
> {
<
TNewMethod extends Method = TMethod,
TNewStrict extends ServerFnStrict = TStrict,
>(
options?: ServerFnOptions<TNewMethod, TNewStrict>,
): ServerFnAfterMiddleware<
TRegister,
TNewMethod,
TMiddlewares,
TInputValidator,
TNewStrict
>
}
export type ValidatorFn<
TRegister,
TMethod extends Method,
TMiddlewares,
TStrict extends ServerFnStrict,
> = <TInputValidator>(
validator: ConstrainValidator<TRegister, TMethod, TInputValidator, TStrict>,
) => ServerFnAfterValidator<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
>
export interface ServerFnValidator<
TRegister,
TMethod extends Method,
TMiddlewares,
TStrict extends ServerFnStrict,
> {
validator: ValidatorFn<TRegister, TMethod, TMiddlewares, TStrict>
}
export interface ServerFnAfterValidator<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
>
extends
ServerFnWithTypes<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
undefined,
TStrict
>,
ServerFnMiddleware<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
>,
ServerFnHandler<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
> {}
export interface ServerFnAfterTyper<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
>
extends
ServerFnWithTypes<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
undefined,
TStrict
>,
ServerFnHandler<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
> {}
// Handler
export interface ServerFnHandler<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
> {
handler: <TNewResponse>(
fn?: ServerFn<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TNewResponse,
TStrict
>,
) => Fetcher<TMiddlewares, TInputValidator, TNewResponse>
}
export interface ServerFnBuilder<
TRegister,
TMethod extends Method = 'GET',
TStrict extends ServerFnStrict = true,
>
extends
ServerFnWithTypes<
TRegister,
TMethod,
undefined,
undefined,
undefined,
TStrict
>,
ServerFnMiddleware<TRegister, TMethod, undefined, undefined, TStrict>,
ServerFnValidator<TRegister, TMethod, undefined, TStrict>,
ServerFnHandler<TRegister, TMethod, undefined, undefined, TStrict> {
<
TNewMethod extends Method = TMethod,
TNewStrict extends ServerFnStrict = TStrict,
>(
options?: ServerFnOptions<TNewMethod, TNewStrict>,
): ServerFnBuilder<TRegister, TNewMethod, TNewStrict>
options: ServerFnBaseOptions<
TRegister,
TMethod,
unknown,
undefined,
undefined,
TStrict
>
}
export interface ServerFnWithTypes<
in out TRegister,
in out TMethod extends Method,
in out TMiddlewares,
in out TInputValidator,
in out TResponse,
in out TStrict extends ServerFnStrict,
> {
'~types': ServerFnTypes<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TResponse,
TStrict
>
options: ServerFnBaseOptions<
TRegister,
TMethod,
unknown,
undefined,
undefined,
TStrict
>
[TSS_SERVER_FUNCTION_FACTORY]: true
}
export type AnyServerFn = ServerFnWithTypes<any, any, any, any, any, any>
export interface ServerFnTypes<
in out TRegister,
in out TMethod extends Method,
in out TMiddlewares,
in out TInputValidator,
in out TResponse,
in out TStrict extends ServerFnStrict,
> {
method: TMethod
strict: TStrict
middlewares: TMiddlewares
validator: TInputValidator
response: TResponse
allServerContext: AssignAllServerFnContext<TRegister, TMiddlewares>
allInput: IntersectAllValidatorInputs<TMiddlewares, TInputValidator>
allOutput: IntersectAllValidatorOutputs<TMiddlewares, TInputValidator>
}
export function flattenMiddlewares<
T extends AnyFunctionMiddleware | AnyRequestMiddleware,
>(
middlewares: Array<T>,
maxDepth: number = 100,
seen: Set<T> = new Set<T>(),
): Array<T> {
const flattened: Array<T> = []
// forEach skips holes, and a list that is not an array throws instead of
// silently dropping its middleware.
const recurse = (middleware: Array<T>, depth: number) => {
if (depth > maxDepth) {
throw new Error(
`Middleware nesting depth exceeded maximum of ${maxDepth}. Check for circular references.`,
)
}
middleware.forEach((m) => {
if (m.options.middleware) {
recurse(m.options.middleware as Array<T>, depth + 1)
}
if (!seen.has(m)) {
seen.add(m)
flattened.push(m)
}
})
}
recurse(middlewares, 0)
return flattened
}
export type ServerFnMiddlewareOptions = {
method: Method
data: any
headers?: HeadersInit
signal?: AbortSignal
sendContext?: any
context?: any
serverFnMeta: ClientFnMeta
fetch?: CustomFetch
/** @internal - Preserves the call-site fetch to ensure it has highest priority over middleware */
_callSiteFetch?: CustomFetch
}
export type ServerFnMiddlewareResult = ServerFnMiddlewareOptions & {
result?: unknown
/** Present only when the call failed. Holds the thrown value, even `undefined`. */
error?: unknown
}
export type NextFn = (
ctx: ServerFnMiddlewareResult,
) => Promise<ServerFnMiddlewareResult>
export type MiddlewareFn = (
ctx: ServerFnMiddlewareOptions & {
next: NextFn
},
) => Promise<ServerFnMiddlewareResult>
export async function execValidator(
validator: AnyValidator,
input: unknown,
): Promise<unknown> {
if (validator == null) return {}
if ('~standard' in validator) {
const result = await validator['~standard'].validate(input)
if (result.issues)
throw new Error(JSON.stringify(result.issues, undefined, 2))
return result.value
}
if ('parse' in validator) {
return validator.parse(input)
}
if (typeof validator === 'function') {
return validator(input)
}
throw new Error('Invalid validator type!')
}
function serverFnBaseToMiddleware(
options: ServerFnBaseOptions<any, any, any, any, any>,
): AnyFunctionMiddleware {
return {
'~types': undefined!,
options: {
validator: options.validator,
client: async ({ next, sendContext, fetch, ...ctx }) => {
const payload = {
...ctx,
// switch the sendContext over to context
context: sendContext,
fetch,
}
// Execute the extracted function
// but not before serializing the context
const res = await options.extractedFn?.(payload)
return next(res)
},
// Always the last middleware, so it returns the result instead of
// passing it through next(). Browsers never execute a server function.
server:
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- isServer is only known outside development
(isServer ?? typeof window === 'undefined')
? async ({ next: _next, ...ctx }) => {
// Execute the server function
const result = await options.serverFn?.(ctx)
return { ...ctx, result } as any
}
: undefined,
},
}
}