diff --git a/libs/rpc/package.json b/libs/rpc/package.json index 678758f2..9112248e 100644 --- a/libs/rpc/package.json +++ b/libs/rpc/package.json @@ -32,8 +32,8 @@ "build": "tsup --config build.config.js", "check": "tsc --noEmit", "document": "typedoc", - "test": "vitest run", - "testing": "vitest watch", + "test": "vitest run --typecheck", + "testing": "vitest watch --typecheck", "lint": "eslint ." }, "dependencies": { diff --git a/libs/rpc/src/client/index.test.ts b/libs/rpc/src/client/index.test.ts index fe5d511d..3c1b66dd 100644 --- a/libs/rpc/src/client/index.test.ts +++ b/libs/rpc/src/client/index.test.ts @@ -1,7 +1,12 @@ import { expect, test } from "vitest" import { createPrimClient } from "./index" -type ExampleModule = { hello: (name?: string) => Promise; goodbye: () => string; anError: () => void } +type ExampleModule = { + hello: (name?: string) => Promise + goodbye: () => string + anError: () => void + // what(): { is(): { this(): void } } +} const exampleModule = { hello(name?: string) { return ["Hello", name].filter(given => given).join(" ") @@ -12,9 +17,11 @@ const exampleModule = { } test("static import", async () => { - const client = createPrimClient, typeof exampleModule>({ - module: exampleModule, - }) + const options = { + module: () => Promise.resolve(exampleModule), + handleForms: false, + } as const + const client = createPrimClient(options) // not async since module was provided locally expect(client.hello()).toBe("Hello") // FIXME: not implemented yet diff --git a/libs/rpc/src/client/index.ts b/libs/rpc/src/client/index.ts index 3433af02..b4b1a252 100644 --- a/libs/rpc/src/client/index.ts +++ b/libs/rpc/src/client/index.ts @@ -1,22 +1,35 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import type { PrimOptions, PromisifiedModule } from "../interfaces" +import { + FunctionAndForm, + type JsonHandler, + type PossibleModule, + type PrimOptions, + type PromisifiedModule, + type RemoveDynamicImport, + type RemoveFunctionWrapper, +} from "../interfaces" import { createPrimOptions } from "../options" import { isDefined } from "emery" import { createMethodCatcher } from "./proxy" import { handlePotentialPromise } from "./wrapper" import { getUnfulfilledModule, handleLocalModule } from "./local" +import { MergeModuleMethods } from "../types/merge" export function createPrimClient< - ModuleType extends GivenOptions["module"], - OverrideModule extends GivenOptions["module"] = never, - GivenOptions extends PrimOptions = PrimOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + ModuleType extends PossibleModule = never, + GivenOptions extends PrimOptions = PrimOptions, >(options?: GivenOptions) { options = createPrimOptions(options) const providedModule = getUnfulfilledModule(options.module) const providedMethodPlugin = isDefined(options.methodPlugin) const providedCallbackPlugin = isDefined(options.callbackPlugin) // the returned client will catch all method calls given on it recursively - return createMethodCatcher>({ + type FinalModule = MergeModuleMethods< + PromisifiedModule, + PromisifiedModule + > + return createMethodCatcher({ onMethod(rpc, next) { // if module method was provided (and is not dynamic import), intercept call and return synchronously if (providedModule instanceof Promise) return next diff --git a/libs/rpc/src/client/local.ts b/libs/rpc/src/client/local.ts index 6df80add..13b03388 100644 --- a/libs/rpc/src/client/local.ts +++ b/libs/rpc/src/client/local.ts @@ -45,7 +45,7 @@ export function handleLocalModule(rpc: RpcCall, options: Prim // eslint-disable-next-line @typescript-eslint/no-unsafe-return if (method) { const preprocessed = options.preRequest?.(rpc.args, rpc.method) ?? { args: rpc.args } - if (Array.isArray(preprocessed.args) && givenFormLike(preprocessed.args[0])) { + if (options.handleForms && Array.isArray(preprocessed.args) && givenFormLike(preprocessed.args[0])) { preprocessed.args[0] = handlePossibleForm(preprocessed.args[0]) } if ("result" in preprocessed) { diff --git a/libs/rpc/src/interfaces.ts b/libs/rpc/src/interfaces.ts index 9a1dcb6c..fe259f1d 100644 --- a/libs/rpc/src/interfaces.ts +++ b/libs/rpc/src/interfaces.ts @@ -119,28 +119,22 @@ interface PrimWebSocketFunctionEvents { // let d: VariableArgsFunction // await d("what") -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FunctionAndForm = { - (...args: Args): Result - (formLike: SubmitEvent | FormData | HTMLFormElement): Result -} +// type B = [T] extends [never] ? true : false +// let b: B -// type PromisifiedModuleDirectWithoutOverride< -// ModuleGiven extends object, -// Recursive extends true | false = true, -// Keys extends keyof ModuleGiven = Extract, -// > = ConditionalExcept< -// { -// [Key in Keys]: ModuleGiven[Key] extends ((...args: infer A) => infer R) & object -// ? FunctionAndForm>> & PromisifiedModuleDirect -// : ModuleGiven[Key] extends object -// ? Recursive extends true -// ? PromisifiedModuleDirect -// : never -// : never -// }, -// never -// > +// NOTE: ny default, assume that form arguments could be given unless explicitly disabled +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type FunctionAndForm = [F] extends [false] + ? { + (...args: Args): Result + } + : { + (...args: Args): Result + (formLike: SubmitEvent | FormData | HTMLFormElement): Result + } + +// TODO: to support nested modules in PromisifiedModuleDirect, I need to merge returned Promise with the functions +// returned from a given function // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyFunction = (...args: any[]) => any @@ -148,24 +142,18 @@ export type AnyFunction = (...args: any[]) => any type PromisifiedModuleDirect< ModuleGiven extends object, Recursive extends true | false = true, - ModuleOverride extends object = never, + HandleForm extends true | false = true, + Promised extends true | false = true, Keys extends keyof ModuleGiven = Extract, > = ConditionalExcept< { [Key in Keys]: ModuleGiven[Key] extends (...args: infer A) => infer R - ? Key extends keyof ModuleOverride - ? ModuleOverride[Key] extends (...args: infer A2) => infer R2 - ? FunctionAndForm & PromisifiedModuleDirect - : FunctionAndForm>> & PromisifiedModuleDirect - : FunctionAndForm>> & PromisifiedModuleDirect + ? FunctionAndForm> : R, HandleForm> & + PromisifiedModuleDirect : ModuleGiven[Key] extends object ? Recursive extends true - ? Key extends keyof ModuleOverride - ? ModuleOverride[Key] extends object - ? PromisifiedModuleDirect - : PromisifiedModuleDirect - : PromisifiedModuleDirect - : PromisifiedModuleDirect + ? PromisifiedModuleDirect + : never : never }, never @@ -207,7 +195,8 @@ type PromisifiedModuleDirect< // NOTE: this is a non-recursive version of default `Awaited` type that comes with TypeScript type PromisifiedModuleDynamicImport< ModuleGiven extends object, - ModuleOverride extends object = never, + HandleForm extends true | false, + Promised extends true | false, > = ModuleGiven extends object & { // eslint-disable-next-line @typescript-eslint/no-explicit-any then: (onfulfilled: infer F, ...args: infer _) => any @@ -215,20 +204,21 @@ type PromisifiedModuleDynamicImport< ? // eslint-disable-next-line @typescript-eslint/no-explicit-any F extends (value: infer V, ...args: infer _) => any ? V extends object - ? PromisifiedModuleDirect + ? PromisifiedModuleDirect : never : never - : PromisifiedModuleDirect + : PromisifiedModuleDirect // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyFunctionReturnsPromise = (...args: any[]) => PromiseLike // If given a function that returns a promise, get the returned promise (pattern used often with dynamic imports) export type PromisifiedModule< Given extends object, - ModuleOverride extends object = never, + HandleForm extends true | false, + Promised extends true | false = true, > = Given extends AnyFunctionReturnsPromise - ? PromisifiedModuleDynamicImport, ModuleOverride> - : PromisifiedModuleDynamicImport + ? PromisifiedModuleDynamicImport, HandleForm, Promised> + : PromisifiedModuleDynamicImport export type RemoveFunctionWrapper = Given extends AnyFunctionReturnsPromise ? ReturnType @@ -248,7 +238,7 @@ export type RemoveDynamicImport = ModuleGiven extends object & { // The following is intended to be used to export a module used with the client // (useful for JSDocs or usage outside of the Prim Client that provides this type) /** Module transformed as it is done by the Prim+RPC client */ -export type RpcModule = PromisifiedModule +export type RpcModule = PromisifiedModule export interface JsonHandler { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -296,7 +286,11 @@ export type PossibleModule = object // | Promise> // | (() => Promise>) -export interface PrimOptions { +export interface PrimOptions< + M extends PossibleModule = PossibleModule, + J extends JsonHandler = JsonHandler, + F extends boolean = boolean, +> { /** * This option is not yet implemented. * @@ -389,7 +383,7 @@ export interface PrimOptions, true | "idempotent">> + allowList?: PartialDeep, true | "idempotent">> /** * In JavaScript, functions are objects. Those objects can have methods. This means that functions can have methods. * @@ -428,6 +422,7 @@ export interface PrimOptions { args: unknown[]; result?: unknown } | undefined /** Transform given result prior to being returned to the RPC caller */ diff --git a/libs/rpc/src/options.ts b/libs/rpc/src/options.ts index ffd2256d..d9931eb1 100644 --- a/libs/rpc/src/options.ts +++ b/libs/rpc/src/options.ts @@ -97,7 +97,7 @@ const createBaseServerOptions = (): PrimServerOptions => ({ * @returns Options with defaults set */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createPrimOptions = PrimOptions>( +export function createPrimOptions = PrimOptions>( options?: OptionsType, server = false ) { diff --git a/libs/rpc/src/types/merge.test-d.ts b/libs/rpc/src/types/merge.test-d.ts new file mode 100644 index 00000000..3d526cc0 --- /dev/null +++ b/libs/rpc/src/types/merge.test-d.ts @@ -0,0 +1,114 @@ +import { expectTypeOf, test, describe } from "vitest" +import type { MergeModuleMethods } from "./merge" +import { PromisifiedModule } from "../interfaces" + +describe("MergeModule merges modules", () => { + test("override all without new methods", () => { + type A = { + lorem(): string + testing: { + ipsum(): Promise + } + } + type B = { + lorem(): number + testing: { + ipsum(): void + } + } + type Merged = MergeModuleMethods + expectTypeOf().toMatchTypeOf() + }) + + test("with new methods", () => { + type A = { + what(): void + testing: { + lorem(): string + } + } + type B = { + testing: { + ipsum(): void + } + cool(): boolean + } + type Merged = MergeModuleMethods + type Expected = { + what(): void + testing: { + lorem(): string + ipsum(): void + } + cool(): boolean + } + expectTypeOf().toMatchTypeOf() + }) + + test("without override", () => { + type A = { + what(): void + testing: { + lorem(): string + } + } + type B = never + type Merged = MergeModuleMethods + expectTypeOf().toMatchTypeOf() + }) + + test("works without base", () => { + type A = never + type B = { + what(): void + } + type Merged = MergeModuleMethods + expectTypeOf().toMatchTypeOf() + }) +}) + +describe("MergeModule works with PromisifiedModule", () => { + test("With form handling", () => { + type A = PromisifiedModule< + { + lorem(): Promise + ipsum(): void + }, + true + > + type B = { + lorem(): string + } + type Merged = MergeModuleMethods + type Expected = { + lorem(): string + ipsum(): Promise + ipsum(formlike: SubmitEvent | FormData | HTMLFormElement): Promise + } + + expectTypeOf().toMatchTypeOf() + }) + + test("without form handling", () => { + type A = PromisifiedModule< + { + lorem(): Promise + test: { + ipsum(): void + } + }, + false + > + type B = { + lorem(): string + } + type Merged = MergeModuleMethods + type Expected = { + lorem(): string + test: { + ipsum(): Promise + } + } + expectTypeOf().toMatchTypeOf() + }) +}) diff --git a/libs/rpc/src/types/merge.ts b/libs/rpc/src/types/merge.ts new file mode 100644 index 00000000..c8e4f729 --- /dev/null +++ b/libs/rpc/src/types/merge.ts @@ -0,0 +1,25 @@ +import type { AnyFunction } from "../interfaces" + +export type MergeModuleMethods< + ModuleGiven extends object, + Override extends object = never, + Keys extends keyof ModuleGiven = Extract, +> = [ModuleGiven] extends [never] + ? [Override] extends [never] + ? never + : Override + : { + [Key in Keys]: Key extends keyof Override + ? ModuleGiven[Key] extends AnyFunction + ? Override[Key] extends AnyFunction + ? Override[Key] + : ModuleGiven[Key] + : Override[Key] extends object + ? ModuleGiven[Key] extends object + ? MergeModuleMethods + : Override[Key] + : Override[Key] + : ModuleGiven[Key] + } & { + [Key in Exclude]: Override[Key] + }