From 47c37811fd13bcdaf8654813259eb096d0c65dc4 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 24 Feb 2023 12:32:01 -0700 Subject: [PATCH] fix: refactor the universe --- packages/bling/src/babel.ts | 10 +- packages/bling/src/client.ts | 162 +++++--------- packages/bling/src/server.ts | 209 +++++++++--------- packages/bling/src/types.ts | 63 ++++-- .../src/utils/{responses.ts => utils.ts} | 87 +++++++- 5 files changed, 289 insertions(+), 242 deletions(-) rename packages/bling/src/utils/{responses.ts => utils.ts} (71%) diff --git a/packages/bling/src/babel.ts b/packages/bling/src/babel.ts index 98bdde9..1286bb6 100644 --- a/packages/bling/src/babel.ts +++ b/packages/bling/src/babel.ts @@ -116,6 +116,7 @@ function transformServer({ types: t, template }) { path.node.callee.name === 'server$' ) { const serverFn = path.get('arguments')[0] + const serverFnOpts = path.get('arguments')[1] let program = path.findParent((p) => t.isProgram(p)) let statement = path.findParent((p) => program.get('body').includes(p) @@ -126,7 +127,6 @@ function transformServer({ types: t, template }) { p.isFunctionDeclaration() || p.isObjectProperty() ) - const serverResource = path.getData('serverResource') ?? false let serverIndex = state.servers++ let hasher = state.opts.minify ? hashFn : (str) => str const fName = state.filename @@ -197,10 +197,11 @@ function transformServer({ types: t, template }) { if (state.opts.ssr) { statement.insertBefore( template(` - const $$server_module${serverIndex} = server$.createHandler(%%source%%, "${route}", ${serverResource}); + const $$server_module${serverIndex} = server$.createHandler(%%source%%, "${route}", %%options%%); server$.registerHandler("${route}", $$server_module${serverIndex}); `)({ source: serverFn.node, + options: serverFnOpts.node, }) ) } else { @@ -209,10 +210,10 @@ function transformServer({ types: t, template }) { ` ${ process.env.TEST_ENV === 'client' - ? `server$.registerHandler("${route}", server$.createHandler(%%source%%, "${route}", ${serverResource}));` + ? `server$.registerHandler("${route}", server$.createHandler(%%source%%, "${route}", %%options%%));` : `` } - const $$server_module${serverIndex} = server$.createFetcher("${route}", ${serverResource});`, + const $$server_module${serverIndex} = server$.createFetcher("${route}", %%options%%);`, { syntacticPlaceholders: true, } @@ -220,6 +221,7 @@ function transformServer({ types: t, template }) { process.env.TEST_ENV === 'client' ? { source: serverFn.node, + options: serverFnOpts.node, } : {} ) diff --git a/packages/bling/src/client.ts b/packages/bling/src/client.ts index 6be76cc..b886f02 100644 --- a/packages/bling/src/client.ts +++ b/packages/bling/src/client.ts @@ -1,124 +1,80 @@ import { - ContentTypeHeader, - JSONResponseType, - mergeHeaders, + createFetcher, + mergeRequestInits, parseResponse, - XBlingContentTypeHeader, + payloadRequestInit, XBlingOrigin, XBlingResponseTypeHeader, -} from './utils/responses' +} from './utils/utils' import type { - Deserializer, - FetchEvent, + AnyServerFn, Serializer, - ServerFunction, + ServerFnOpts, + ServerFn, + NonFnProps, } from './types' -export { json } from './utils/responses' +export { json } from './utils/utils' -export type CreateClientServerFunction = (< - E extends any[], - T extends (...args: [...E]) => any ->( - fn: T -) => ServerFunction) & { - addSerializer(serializer: Serializer): void - createFetcher( - route: string, - serverResource: boolean - ): ServerFunction - fetch(route: string, init?: RequestInit): Promise - createRequestInit: (path: string, args: any[], meta: any) => RequestInit - addDeserializer(deserializer: Deserializer): void -} & FetchEvent - -export const server$: CreateClientServerFunction = ((_fn: any) => { - throw new Error('Should be compiled away') -}) as unknown as CreateClientServerFunction +// let serializers: Serializer[] = [] -server$.addSerializer = ({ apply, serialize }: Serializer) => { +export function addSerializer({ apply, serialize }: Serializer) { serializers.push({ apply, serialize }) } -server$.createRequestInit = function (route, args: any[], meta): RequestInit { - let body, - headers: Record = { - [XBlingOrigin]: 'client', - } +export type ClientServerFn = (( + fn: T, + opts?: ServerFnOpts +) => ServerFn) & { + createFetcher(route: string, defualtOpts: ServerFnOpts): ServerFn +} - if (args[0] instanceof FormData) { - body = args[0] - } else { - body = JSON.stringify(args, (key, value) => { - let serializer = serializers.find(({ apply }) => apply(value)) - if (serializer) { - return serializer.serialize(value) +const _server$ = (() => { + throw new Error('Should be compiled away') +}) as any + +const _serverProps: NonFnProps = { + createFetcher: (route: string, defaultOpts?: ServerFnOpts) => { + return createFetcher(route, async (payload: any, opts?: ServerFnOpts) => { + let payloadInit = payloadRequestInit(payload, serializers) + + const request = new Request( + new URL(route, window.location.href).href, + mergeRequestInits( + { + method: 'POST', + headers: { + [XBlingOrigin]: 'client', + }, + }, + payloadInit, + defaultOpts?.request, + opts?.request + ) + ) + + const response = await fetch(request) + + // // throws response, error, form error, json object, string + if (response.headers.get(XBlingResponseTypeHeader) === 'throw') { + throw await parseResponse(response) + } else { + return await parseResponse(response) } - return value }) - headers[ContentTypeHeader] = JSONResponseType - } - - return { - method: 'POST', - body: body, - headers: new Headers({ - ...headers, - }), - } -} - -type ServerCall = (route: string, init: RequestInit) => Promise - -server$.createFetcher = (route, meta) => { - let fetcher: any = (...args: any[]) => { - const requestInit = server$.createRequestInit(route, args, meta) - // request body: json, formData, or string - return (server$.call as ServerCall)(route, requestInit) - } - - fetcher.url = route - - fetcher.fetch = (init: RequestInit) => - (server$.call as ServerCall)(route, init) - - fetcher.withRequest = - (partialInit: Partial) => - (...args: any) => { - let requestInit = server$.createRequestInit(route, args, meta) - // request body: json, formData, or string - return (server$.call as ServerCall)(route, { - ...requestInit, - ...partialInit, - headers: mergeHeaders(requestInit.headers, partialInit.headers), - }) - } - - return fetcher as ServerFunction + }, + // // used to fetch from an API route on the server or client, without falling into + // // fetch problems on the server + // fetch: async function (route: string | URL, init: RequestInit) { + // if (route instanceof URL || route.startsWith('http')) { + // return await fetch(route, init) + // } + // const request = new Request(new URL(route, window.location.href).href, init) + // return await fetch(request) + // }, } -server$.call = async function (route: string, init: RequestInit) { - const request = new Request(new URL(route, window.location.href).href, init) - - const response = await fetch(request) - - // // throws response, error, form error, json object, string - if (response.headers.get(XBlingResponseTypeHeader) === 'throw') { - throw await parseResponse(response) - } else { - return await parseResponse(response) - } -} as any - -// used to fetch from an API route on the server or client, without falling into -// fetch problems on the server -server$.fetch = async function (route: string | URL, init: RequestInit) { - if (route instanceof URL || route.startsWith('http')) { - return await fetch(route, init) - } - const request = new Request(new URL(route, window.location.href).href, init) - return await fetch(request) -} +export const server$: ClientServerFn = Object.assign(_server$, _serverProps) diff --git a/packages/bling/src/server.ts b/packages/bling/src/server.ts index a47c2c8..9142744 100644 --- a/packages/bling/src/server.ts +++ b/packages/bling/src/server.ts @@ -6,57 +6,109 @@ import { XBlingLocationHeader, XBlingOrigin, XBlingResponseTypeHeader, + createFetcher, isRedirectResponse, - mergeHeaders, + mergeRequestInits, + mergeServerOpts, parseResponse, -} from './utils/responses' + payloadRequestInit, +} from './utils/utils' import type { + AnyServerFn, Deserializer, - FetchEvent, - ServerFunction, - ServerFunctionEvent, + ServerFnOpts, + ServerFn, + ServerFnCtx, + NonFnProps, } from './types' -export { json } from './utils/responses' - -export type CreateServerFunction = (< - E extends any[], - T extends (...args: [...E]) => any ->( - fn: T -) => ServerFunction) & { - // SERVER - getHandler: (route: string) => any - createHandler: (fn: any, hash: string, serverResource: boolean) => any - registerHandler: (route: string, handler: any) => any - hasHandler: (route: string) => boolean - parseRequest: (event: ServerFunctionEvent) => Promise<[string, any[]]> - respondWith: ( - event: ServerFunctionEvent, - data: Response | Error | string | object, - responseType: 'throw' | 'return' - ) => void - normalizeArgs: ( - path: string, - that: ServerFunctionEvent | any, - args: any[], - meta: any - ) => [any, any[]] - fetch(route: string, init?: RequestInit): Promise - addDeserializer(deserializer: Deserializer): void -} & FetchEvent +export { json } from './utils/utils' const deserializers: Deserializer[] = [] -export const server$: CreateServerFunction = ((_fn: any) => { +export function addDeserializer(deserializer: Deserializer) { + deserializers.push(deserializer) +} + +export type ServerServerFn = (( + fn: T, + opts?: ServerFnOpts +) => ServerFn) & { + createHandler( + fn: AnyServerFn, + route: string, + opts: ServerFnOpts + ): ServerFn +} + +const _server$ = (() => { throw new Error('Should be compiled away') -}) as unknown as CreateServerFunction +}) as any -server$.addDeserializer = (deserializer: Deserializer) => { - deserializers.push(deserializer) +const _serverProps: NonFnProps = { + createHandler: ( + fn: AnyServerFn, + route: string, + defaultOpts?: ServerFnOpts + ): ServerFn => { + return createFetcher(route, async (payload: any, opts?: ServerFnOpts) => { + console.log(`Executing server function: ${route}`) + if (payload) console.log(` Fn Payload: ${payload}`) + + let payloadInit = payloadRequestInit(payload, false) + + // Even though we're not crossing the network, we still need to + // create a Request object to pass to the server function + const request = new Request( + route, + mergeRequestInits( + { + method: 'POST', + headers: { + [XBlingOrigin]: 'server', + }, + }, + payloadInit, + defaultOpts?.request, + opts?.request + ) + ) + + try { + // Do the same parsing of the result as we do on the client + return parseResponse( + await fn(payload, { + request: request, + }) + ) + } catch (e) { + if (e instanceof Error && /[A-Za-z]+ is not defined/.test(e.message)) { + const error = new Error( + e.message + + '\n' + + ' You probably are using a variable defined in a closure in your server function. Make sure you pass any variables needed to the server function as arguments. These arguments must be serializable.' + ) + error.stack = e.stack ?? '' + throw error + } + throw e + } + }) + }, + // used to fetch from an API route on the server or client, without falling into + // fetch problems on the server + // fetch: async function (route: string | URL, init: RequestInit) { + // if (route instanceof URL || route.startsWith('http')) { + // return await fetch(route, init) + // } + // const request = new Request(new URL(route, window.location.href).href, init) + // return await fetch(request) + // }, } -server$.parseRequest = async function (event: ServerFunctionEvent) { +export const server$: ServerServerFn = Object.assign(_server$, _serverProps) + +async function parseRequest(event: ServerFnCtx) { let request = event.request let contentType = request.headers.get(ContentTypeHeader) let name = new URL(request.url).pathname, @@ -88,8 +140,8 @@ server$.parseRequest = async function (event: ServerFunctionEvent) { return [name, args] } -server$.respondWith = function ( - { request }: ServerFunctionEvent, +function respondWith( + { request }: ServerFnCtx, data: Response | Error | string | object, responseType: 'throw' | 'return' ) { @@ -172,13 +224,13 @@ server$.respondWith = function ( }) } -export async function handleEvent(event: ServerFunctionEvent) { +export async function handleEvent(event: ServerFnCtx) { const url = new URL(event.request.url) - if (server$.hasHandler(url.pathname)) { + if (hasHandler(url.pathname)) { try { - let [name, args] = await server$.parseRequest(event) - let handler = server$.getHandler(name) + let [name, args] = await parseRequest(event) + let handler = getHandler(name) if (!handler) { throw { status: 404, @@ -189,87 +241,30 @@ export async function handleEvent(event: ServerFunctionEvent) { event, ...(Array.isArray(args) ? args : [args]) ) - return server$.respondWith(event, data, 'return') + return respondWith(event, data, 'return') } catch (error) { - return server$.respondWith(event, error as Error, 'throw') + return respondWith(event, error as Error, 'throw') } } return null } -server$.normalizeArgs = ( - path: string, - that: ServerFunctionEvent | any, - args: any[], - meta: any -) => { - let ctx: any | undefined - if (typeof that === 'object') { - ctx = that - } - - return [ctx, args] -} - const handlers = new Map() -// server$.requestContext = null; -server$.createHandler = (impl, route, meta) => { - let serverFunction: any = function ( - this: ServerFunctionEvent | any, - ...args: any[] - ) { - let [normalizedThis, normalizedArgs] = server$.normalizeArgs( - route, - this, - args, - meta - ) - - const execute = async () => { - console.log(`Executing server function: ${route}`) - if (normalizedArgs) console.log(` Fn Payload: ${normalizedArgs}`) - try { - // Do the same parsing of the result as we do on the client - return parseResponse(await impl.call(normalizedThis, ...normalizedArgs)) - } catch (e) { - if (e instanceof Error && /[A-Za-z]+ is not defined/.test(e.message)) { - const error = new Error( - e.message + - '\n' + - ' You probably are using a variable defined in a closure in your server function.' - ) - error.stack = e.stack ?? '' - throw error - } - throw e - } - } - - return execute() - } - - serverFunction.url = route - serverFunction.action = function (...args: any[]) { - return serverFunction.call(this, ...args) - } - - return serverFunction -} -server$.registerHandler = function (route, handler) { +export function registerHandler(route: string, handler: any): any { console.log('Registering handler', route) handlers.set(route, handler) } -server$.getHandler = function (route) { +export function getHandler(route: string) { return handlers.get(route) } -server$.hasHandler = function (route) { +export function hasHandler(route: string) { return handlers.has(route) } // used to fetch from an API route on the server or client, without falling into // fetch problems on the server -server$.fetch = fetch +// server$.fetch = fetch diff --git a/packages/bling/src/types.ts b/packages/bling/src/types.ts index 7594cb7..c07f47c 100644 --- a/packages/bling/src/types.ts +++ b/packages/bling/src/types.ts @@ -1,12 +1,6 @@ export const FormError = Error export const ServerError = Error -export interface FetchEvent { - request: Request - env: any - locals: Record -} - export type Serializer = { apply: (value: any) => boolean serialize: (value: any) => any @@ -14,26 +8,51 @@ export type Serializer = { export type Deserializer = { apply: (value: any) => any - deserialize: (value: any, ctx: ServerFunctionEvent) => any + deserialize: (value: any, ctx: ServerFnCtx) => any } -export interface ServerFunctionEvent extends FetchEvent { - // fetch(url: string, init: RequestInit): Promise - // $type: typeof FETCH_EVENT -} +export type AnyServerFn = (payload: any, ctx: ServerFnCtx) => any -export type ServerFunction< - E extends any[], - T extends (...args: [...E]) => any, - TReturn = Awaited> extends JsonResponse - ? R - : ReturnType -> = ((...p: Parameters) => Promise>) & { +export type ServerFnReturn = Awaited< + ReturnType +> extends JsonResponse + ? R + : ReturnType + +export type ServerFnImpl = ( + payload: Parameters['0'], + opts?: ServerFnOpts +) => Promise>> + +export type ServerFnMethods = { url: string - fetch: (init: RequestInit) => Promise> - withRequest: ( - init: Partial - ) => (...p: Parameters) => Promise> + query: ( + payload: Parameters['0'], + opts: ServerFnOpts + ) => Promise>> + mutate: ( + payload: Parameters['0'], + opts: ServerFnOpts + ) => Promise>> + fetch: ( + init: RequestInit, + opts: ServerFnOpts + ) => Promise>> } +export type ServerFn = ServerFnImpl & + ServerFnMethods + export interface JsonResponse extends Response {} + +export type ServerFnOpts = { + request?: RequestInit +} + +export type ServerFnCtx = { + request: Request +} + +export type NonFnProps = { + [TKey in keyof T]: TKey extends (...args: any[]) => any ? never : T[TKey] +} diff --git a/packages/bling/src/utils/responses.ts b/packages/bling/src/utils/utils.ts similarity index 71% rename from packages/bling/src/utils/responses.ts rename to packages/bling/src/utils/utils.ts index 21670f7..5a4b18a 100644 --- a/packages/bling/src/utils/responses.ts +++ b/packages/bling/src/utils/utils.ts @@ -1,4 +1,13 @@ -import { JsonResponse } from '../types' +import { + AnyServerFn, + JsonResponse, + NonFnProps, + Serializer, + ServerFn, + ServerFnImpl, + ServerFnMethods, + ServerFnOpts, +} from '../types' export const XBlingStatusCodeHeader = 'x-bling-status-code' export const XBlingLocationHeader = 'x-bling-location' @@ -144,6 +153,14 @@ export function mergeHeaders(...objs: (Headers | HeadersInit | undefined)[]) { return new Headers(allHeaders) } +export function mergeRequestInits(...objs: (RequestInit | undefined)[]) { + return Object.assign.call(null, [ + {}, + ...objs, + { headers: mergeHeaders(...objs.map((o) => o && o.headers)) }, + ]) +} + export async function parseResponse(response: Response) { if (response instanceof Response) { const contentType = @@ -184,8 +201,66 @@ export async function parseResponse(response: Response) { return response } -// export function json(data: TData, responseInit: ResponseInit = {}) { -// responseInit.headers = new Headers(responseInit.headers) -// responseInit.headers.set(XBlingContentTypeHeader, 'json') -// return new Response(JSON.stringify(data), responseInit) as JsonResponse -// } +export function mergeServerOpts(...objs: (ServerFnOpts | undefined)[]) { + return Object.assign.call(null, [ + {}, + ...objs, + { + request: mergeRequestInits(...objs.map((o) => o && o.request)), + }, + ]) +} + +export function payloadRequestInit( + payload: any, + serializers: false | Serializer[] +) { + let payloadInit: RequestInit = {} + + if (payload instanceof FormData) { + payloadInit.body = payload + } else { + payloadInit.body = JSON.stringify( + payload, + serializers + ? (key, value) => { + let serializer = serializers.find(({ apply }) => apply(value)) + if (serializer) { + return serializer.serialize(value) + } + return value + } + : undefined + ) + + payloadInit.headers = { + [ContentTypeHeader]: JSONResponseType, + } + } + + return payloadInit +} + +export function createFetcher( + route: string, + fetcherImpl: ServerFnImpl +): ServerFn { + return Object.assign(fetcherImpl, { + url: route, + fetch: (request: RequestInit, opts: ServerFnOpts) => { + return fetcherImpl(undefined, mergeServerOpts({ request }, opts)) + }, + query: (payload: any, opts: ServerFnOpts) => { + return fetcherImpl( + payload, + mergeServerOpts({ request: { method: 'GET' } }, opts) + ) + }, + mutate: (payload: any, opts: ServerFnOpts) => { + return fetcherImpl( + payload, + mergeServerOpts({ request: { method: 'POST' } }, opts) + ) + }, + } as ServerFnMethods) as ServerFn +}