From 81e2115ad9b5c301146f14f90895d1d4500beedd Mon Sep 17 00:00:00 2001 From: peter Date: Wed, 18 Mar 2020 03:17:31 +0100 Subject: [PATCH] new readme and some small fixes --- README.md | 382 ++++++++++++++++++++++++++++++++-------------- demo.ts | 239 +++++++++++++++++++++++++++++ src/Backend.ts | 4 - src/Frontend.ts | 8 +- src/Interfaces.ts | 2 +- src/Types.ts | 11 +- test/Test.ts | 10 +- tsconfig.json | 2 +- 8 files changed, 526 insertions(+), 132 deletions(-) create mode 100644 demo.ts diff --git a/README.md b/README.md index d274086..b2b009d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Weekly Downloads](https://img.shields.io/npm/dw/rpclibrary?color=important)](https://www.npmjs.com/package/rpclibrary) [![License Type](https://img.shields.io/npm/l/rpclibrary?color=blueviolet)](https://gitea.nitowa.xyz/docs/rpclibrary/src/branch/master/LICENSE.md) -rpclibrary is a websocket on steroids! +rpclibrary is a simple to use websocket RPC library. # How to install ``` @@ -16,147 +16,303 @@ npm i rpclibrary ```typescript import {RPCServer, RPCSocket} from 'rpclibrary' -const port = 1234 -const host = 'locahost' +// TL;DR +const echo = (text) => text +const add = (a, b) => a + b -const echo = (x) => x +new RPCServer(20000, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + echo, + add, + ] +}]) -const server = new RPCServer(port, [{ - name: 'HelloWorldRPCGroup', - exportRPCs: () => [ - echo, //named function variable - function echof(x){ return x }, //named function +new RPCSocket(20000, 'localhost').connect().then(async sock => { + try{ + const RPCs = sock['MyRPCGroup1'] + await RPCs.echo("hello!").then(console.log) + await RPCs.add(1, Math.PI).then(console.log) + }catch(e){ + console.log(String(e)) + } +}) +``` + +# Async and Callbacks? + +rpclibrary offers full support for callbacks and Promises. +Please note that **there may only be one callback per RPC and it has to be the last parameter** + +```typescript + +const getAsync = async () => await new Promise((res, _) => { + setTimeout(() => { + res({ + topic: "Hey!!", + message: "Hello World Async!" + }) + }, 250) +}) + +const getCallback = (callback) => { + setTimeout(() => { + try{ + callback({ + topic: "Hey!!", + message: "Hello World Callback!" + }) + }catch(e){ + console.log(String(e)) + } + }, 250) + return "Please wait for a callback :)" +} + +new RPCServer(20000, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + getAsync, { - name: 'echoExplicit', //describing object - call: async (x) => x + name: 'getCallback', + hook: getCallback, } ] }]) -const client = new RPCSocket(port, host) - -client.connect().then(async () => { - const r0 = await client['HelloWorldRPCGroup'].echo('Hello') - const r1 = await client['HelloWorldRPCGroup'].echof('World') - const r2 = await client['HelloWorldRPCGroup'].echoExplicit('RPC!') - - console.log(r0,r1,r2) //Hello World RPC! +new RPCSocket(20000, 'localhost').connect().then(async sock => { + try{ + const RPCs = sock['MyRPCGroup1'] + await RPCs.getAsync().then(console.log) + await RPCs.getCallback(console.log).then(console.log) + }catch(e){ + console.log(String(e)) + } }) ``` -# Using callbacks +# Hooks and Events -rpclibrary offers a special type of call that can be used with callbacks. The callback **has to be the last argument** and **may be the only passed function**. - -In order to function, some metadata has to be included in the return value of hooks. On success, the function is expected to return a `{ result: 'Success', uuid: string }` (Types.SubscriptionResponse) or in case of errors a `{ result: 'Error' }`(Types.ErrorResponse). - -The uuid, as the name implies, is used to uniquely identify the callback for a given invocation and also dictates the name given to the client-side RPC. Unless you got a preferred way of generating these (e.g. using some kind of unique information important to your task) we recommend [uuid](https://www.npmjs.com/package/uuid) for this purpose. - -You should unhook the client socket once you're done with it as not to cause security or control flow issues. +There are a many things you can hook into to manage your connections ```typescript -import {RPCServer, RPCSocket} from 'rpclibrary' - -const port = 1234 -const host = 'locahost' - -const callbacks:Map = new Map() - -new RPCServer(port, [{ - name: 'HelloWorldRPCGroup', - exportRPCs: () => [ - function triggerCallbacks(...messages){ callbacks.forEach(cb => cb.apply({}, messages)) }, +new RPCServer(20001, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + echo, + add, + getAsync, { - name: 'subscribe', - hook: async (callback) => { - const randStr = 'generate_a_random_string_here' - callbacks.set(randStr, callback); - return { result: 'Success', uuid: randStr} - } - },{ - name: 'unsubscribe', - call: async (uuid) => { callbacks.delete(uuid) } + name: 'getCallback', + hook: getCallback, + onClose: (response, rpc) => { /* client disconnected */ }, + onCallback: (...callbackArgs) => { /* callback triggered */ } } - ] + ], +}], { + visibility: '127.0.0.1', //0.0.0.0 + closeHandler: (socket) => { /* global close handler */ }, + connectionHandler: (socket) => { /* new connection made */ }, + errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ }, +}) + +const sock = new RPCSocket(20001, 'localhost') +sock.on('error', (e) => { /* handle error */ }) +sock.on('close', () => { /* handle close event */ }) + +sock.hook('RPCName', (/* arg0, arg1, ..., argN */) => { /* bind client-side RPCs */ }) +//Retrieve the socket from connectionHandler (Server-side) and trigger with +//socket.call('RPCName', arg0, arg1, ..., argN) + +sock.connect().then(_ => { /* ... */}) + +``` + + +# Restricting access + +rpclibrary offers some minimalistic permission management + +```typescript + +//Restricting access +new RPCServer(20002, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + echo, + add, + getAsync, + { + name: 'getCallback', + hook: getCallback, + } + ], +}], { + sesame: "sesame open", + /* + OR check sesame dynamically + and refine permissioning with accessfilter (optional) + */ + + //sesame: (sesame) => true + //accessFilter: (sesame, exporter) => { return exporter.name === "MyRPCGroup1" && sesame === "sesame open" }, +}) + +new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => { + try{ + const RPCs = sock['MyRPCGroup1'] + await RPCs.echo("hello!").then(console.log) + await RPCs.add(1, Math.PI).then(console.log) + await RPCs.getAsync().then(console.log) + await RPCs.getCallback(console.log).then(console.log) + }catch(e){ + console.log(String(e)) + } +}) + + +``` + + +# Typescript support + +rpclibrary is a typescript-first project and offers full support for typing your RPCs. +**NOTE** that your function implementations have to be currectly typed to make the compiler agree. +Explicit typing is recommended. + +Example: +```typescript +echo = (x) => x + /*becomes*/ +echo = (x:string) : string => x +``` + + +```typescript +type MyInterface = { + MyRPCGroup1: { + echo: (x: string) => string + add: (a: number, b: number) => number + getAsync: () => Promise<{ topic: string, message: string }> + getCallback: (callback:Function) => string + } +}; + + +/* +exportRPCs is now type safe. Try swapping echo for badEcho. +Sadly TSC's stack traces aren't the best, but try to scroll to the bottom of them to find useful info like + +Type '(x: boolean) => number' is not assignable to type '(x: string) => string' +*/ + +const badEcho = (x: boolean) : number => 3 + +new RPCServer(20003, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + //badEcho, + echo, + add, + getAsync, + { + name: 'getCallback', + hook: getCallback, + } + ], }]) -const client = new RPCSocket(port, host) -client.connect().then(async () => { - const res = await client['HelloWorldRPCGroup'].subscribe(async (...args:any) => { - console.log.apply(console, args) - - /* close the callbacks once you're done */ - await client['HelloWorldRPCGroup'].unsubscribe(res.uuid) - client.unhook(res.uuid) - }) - - await client['HelloWorldRPCGroup'].triggerCallbacks("Hello", "World", "Callbacks") +new RPCSocket(20003, 'localhost').connect().then(async sock => { + try{ + await sock.MyRPCGroup1.echo("hello!").then(console.log) + await sock.MyRPCGroup1.add(1, Math.PI).then(console.log) + await sock.MyRPCGroup1.getAsync().then(console.log) + await sock.MyRPCGroup1.getCallback(console.log).then(console.log) + }catch(e){ + console.log(String(e)) + } }) -``` - -If you need to include further response data into your `SubscriptionResponse` you can extend it using the server's first generic parameter `SubResType` - -```typescript -new RPCServer<{extension: string}>(port, [{ - name: 'MyRPCGroup', - exportRPCs: () => [{ - name: 'subscribe', - hook: async (callback) => { - return { - result: 'Success', - uuid: 'very_random_string', - extension: 'your_data_here' //tsc will demand this field - } - } - }]} -]) ``` -#Experimental typing support -It is possible to declare pseudo-interfaces for servers and clients by using server's second generic parameter. -This feature is currently still in development and considered **unstable and untested**. Use with caution. + +# A class-based scalable pattern for APIs + +because long lists of functions quickly become unwieldy, it is smart to break up the RPCs into chunks or features. +A pattern I found to be useful is as follows: ```typescript -type MyInterface = { - Group1: { - triggerCallbacks: (...args:any[]) => Promise, - subscribe: (param:string, callback:Function) => Promise>, - unsubscribe: (uuid:string) => Promise - }, - Group2: { - echo: (x:string) => Promise +interface IMyImplementation { + echo: (x: string) => string + add: (a: number, b: number) => number + getAsync: () => Promise<{ topic: string, message: string }> + getCallback: (callback:Function) => string +} + +type MyInterface = { + MyRPCGroup1: { + echo: IMyImplementation['echo'] + add: IMyImplementation['add'] + getAsync: IMyImplementation['getAsync'] + getCallback: IMyImplementation['getCallback'] } } -``` -Create a client using -```typescript -RPCSocket.makeSocket(port, host).then((async (client) => { - const r = await client.Group2.echo("hee") //tsc knows about available RPCs -})) -/* OR */ +class MyImplementation implements IMyImplementation, RPCExporter{ + //"X" as "X" syntax is required to satisfy the type system (as it assumes string to be the true type) + name = "MyRpcGroup11" as "MyRPCGroup1" + + //List the functions you declared in MyInterface + exportRPCs = () => [ + this.echo, + this.add, + this.getAsync, + this.getCallback + ] + + //Write your implementations as you normally would + echo = (text: string) => text + + add = (a: number, b: number) : number => a + b + + getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => { + setTimeout(() => { + res({ + topic: "Hey!!", + message: "Hello World Async!" + }) + }, 250) + }) + + getCallback = (callback: Function) : string => { + setTimeout(() => { + try{ + callback({ + topic: "Hey!!", + message: "Hello World Callback!" + }) + }catch(e){ + console.log(String(e)) + } + }, 250) + return "Please wait for a callback :)" + } +} + +type ProjectInterface = MyInterface + //& MyOtherInterface + //& MyOtherOtherInterface + // ... +; + +new RPCServer(20004, [new MyImplementation() /*, new MyOtherImplementation(), new MyOtherOtherImplementation() */]) + +new RPCSocket(20004, 'localhost').connect().then(async sock => { + // ... +}) -const client = new RPCSocket(port, host) -client.connect().then((async (client) => { - const r = await client.Group2.echo("hee") //tsc knows about available RPCs -})) ``` -Create a server using -```typescript -new RPCServer<{a:string}, MyInterface>(port, - [{ - //... - },{ - name: 'Group2', //Auto completion for names - exportRPCs: () => [{ - name: 'echo', //this name too! - call: async (x) => x+"llo World!" //the paramter and return types are known by tsc - }] - }] -) -``` # [Full documentation](https://gitea.nitowa.xyz/docs/rpclibrary) diff --git a/demo.ts b/demo.ts new file mode 100644 index 0000000..c648fc7 --- /dev/null +++ b/demo.ts @@ -0,0 +1,239 @@ +import { RPCServer, RPCSocket } from './Index' +import { RPCExporter } from './src/Interfaces' + +// TL;DR +const echo = (text: string) => text +const add = (a: number, b: number) : number => a + b +const getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => { + setTimeout(() => { + res({ + topic: "Hey!!", + message: "Hello World Async!" + }) + }, 250) +}) +const getCallback = (callback: Function) : string => { + setTimeout(() => { + try{ + callback({ + topic: "Hey!!", + message: "Hello World Callback!" + }) + }catch(e){ + console.log(String(e)) + } + }, 250) + return "Please wait for a callback :)" +} +new RPCServer(20000, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + echo, + add, + getAsync, + { + name: 'getCallback', + hook: getCallback, + onClose: (response, rpc) => { /* ... */ }, + onCallback: (...callbackArgs) => { /* ... */ } + } + ] +}]) + +new RPCSocket(20000, 'localhost').connect().then(async sock => { + try{ + const RPCs = sock['MyRPCGroup1'] + await RPCs.echo("hello!").then(console.log) + await RPCs.add(1, Math.PI).then(console.log) + await RPCs.getAsync().then(console.log) + await RPCs.getCallback(console.log).then(console.log) + }catch(e){ + console.log(String(e)) + } +}) + +//Hooks and events +new RPCServer(20001, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + echo, + add, + getAsync, + { + name: 'getCallback', + hook: getCallback, + onClose: (response, rpc) => { /* client disconnected */ }, + onCallback: (...callbackArgs) => { /* callback triggered */ } + } + ], +}], { + visibility: '127.0.0.1', //0.0.0.0 + closeHandler: (socket) => { /* global close handler */ }, + connectionHandler: (socket) => { /* new connection made */ }, + errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ }, +}) + +const sock = new RPCSocket(20001, 'localhost') +sock.on('error', (e) => { /* handle error */ }) +sock.on('close', () => { /* handle close event */ }) + +sock.hook('RPCName', (/* arg0, arg1, ..., argN */) => { /* bind client-side RPCs */ }) +//Retrieve the socket from connectionHandler (Server-side) and trigger with +//socket.call('RPCName', arg0, arg1, ..., argN) + +sock.connect().then(_ => { /* ... */}) + +//Restricting access +new RPCServer(20002, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + echo, + add, + getAsync, + { + name: 'getCallback', + hook: getCallback, + } + ], +}], { + sesame: "sesame open", + /* + OR check sesame dynamically + and refine permissioning with accessfilter (optional) + */ + + //sesame: (sesame) => true + //accessFilter: (sesame, exporter) => { return exporter.name === "MyRPCGroup1" && sesame === "sesame open" }, +}) + +new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => { + try{ + const RPCs = sock['MyRPCGroup1'] + await RPCs.echo("hello!").then(console.log) + await RPCs.add(1, Math.PI).then(console.log) + await RPCs.getAsync().then(console.log) + await RPCs.getCallback(console.log).then(console.log) + }catch(e){ + console.log(String(e)) + } +}) + + +//TypeScript and pseudo-interfaces + +type MyInterface = { + MyRPCGroup1: { + echo: (x: string) => string + add: (a: number, b: number) => number + getAsync: () => Promise<{ topic: string, message: string }> + getCallback: (callback:Function) => string + } +}; + + +/* +exportRPCs is now type safe. Try swapping echo for badEcho. +Sadly TSC's stack traces aren't the best, but try to scroll to the bottom of them to find useful info like + +Type '(x: boolean) => number' is not assignable to type '(x: string) => string' +*/ + +const badEcho = (x: boolean) : number => 3 + +new RPCServer(20003, [{ + name: 'MyRPCGroup1', + exportRPCs: () => [ + //badEcho, + echo, + add, + getAsync, + { + name: 'getCallback', + hook: getCallback, + } + ], +}]) + +new RPCSocket(20003, 'localhost').connect().then(async sock => { + try{ + await sock.MyRPCGroup1.echo("hello!").then(console.log) + await sock.MyRPCGroup1.add(1, Math.PI).then(console.log) + await sock.MyRPCGroup1.getAsync().then(console.log) + await sock.MyRPCGroup1.getCallback(console.log).then(console.log) + }catch(e){ + console.log(String(e)) + } +}) + + +//Class-based pattern + +interface IMyImplementation { + echo: (x: string) => string + add: (a: number, b: number) => number + getAsync: () => Promise<{ topic: string, message: string }> + getCallback: (callback:Function) => string +} + +type MyIfc = { + MyRPCGroup1: { + echo: IMyImplementation['echo'] + add: IMyImplementation['add'] + getAsync: IMyImplementation['getAsync'] + getCallback: IMyImplementation['getCallback'] + } +} + +class MyImplementation implements IMyImplementation, RPCExporter{ + //"X" as "X" syntax is required to satisfy the type system (as it assumed string) + name = "MyRpcGroup11" as "MyRPCGroup1" + + //List the functions you declared in MyIfc + exportRPCs = () => [ + this.echo, + this.add, + this.getAsync, + this.getCallback + ] + + //Write your implementations as you normally would + echo = (text: string) => text + + add = (a: number, b: number) : number => a + b + + getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => { + setTimeout(() => { + res({ + topic: "Hey!!", + message: "Hello World Async!" + }) + }, 250) + }) + + getCallback = (callback: Function) : string => { + setTimeout(() => { + try{ + callback({ + topic: "Hey!!", + message: "Hello World Callback!" + }) + }catch(e){ + console.log(String(e)) + } + }, 250) + return "Please wait for a callback :)" + } +} + +new RPCServer(20004, [new MyImplementation(), /* ... other RPCExporters */]) + +new RPCSocket(20004, 'localhost').connect().then(async sock => { + try{ + await sock.MyRPCGroup1.echo("hello!").then(console.log) + await sock.MyRPCGroup1.add(1, Math.PI).then(console.log) + await sock.MyRPCGroup1.getAsync().then(console.log) + await sock.MyRPCGroup1.getCallback(console.log).then(console.log) + }catch(e){ + console.log(String(e)) + } +}) \ No newline at end of file diff --git a/src/Backend.ts b/src/Backend.ts index c146e4f..f5961fe 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -6,10 +6,6 @@ import * as T from './Types'; import * as U from './Utils'; import * as I from './Interfaces'; - -/** - * A Websocket-server-on-steroids with built-in RPC capabilities - */ export class RPCServer< InterfaceT extends T.RPCInterface = T.RPCInterface, > implements I.Destroyable { diff --git a/src/Frontend.ts b/src/Frontend.ts index 49e7512..52f94a3 100644 --- a/src/Frontend.ts +++ b/src/Frontend.ts @@ -13,8 +13,8 @@ import { stripAfterEquals, appendComma } from './Utils'; export class RPCSocket implements I.Socket{ static async makeSocket(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise> { - const socket = new RPCSocket(port, server, conf) - return await socket.connect(sesame) + const socket = new RPCSocket(port, server, conf) + return await socket.connect(sesame) } private socket: I.Socket @@ -128,7 +128,7 @@ export class RPCSocket implements I /** * Connects to the server and attaches available RPCs to this object */ - public async connect( sesame?: string ) : Promise> { + public async connect( sesame?: string ) : Promise> { this.socket = await bsock.connect(this.port, this.server, this.conf.tls?this.conf.tls:false) this.errorHandlers.forEach(h => this.socket.on('error', h)) this.closeHandlers.forEach(h => this.socket.on('close', h)) @@ -153,7 +153,7 @@ export class RPCSocket implements I this[i.owner][i.name] = f this[i.owner][i.name].bind(this) }) - return > (this as any) + return > (this as any) } /** diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 02fc7e5..f642851 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -6,7 +6,7 @@ import * as I from "./Interfaces" */ export type RPCExporter< Ifc extends T.RPCInterface = T.RPCInterface, - Name extends keyof Ifc = string, + Name extends keyof Ifc = keyof Ifc, > = { name: Name exportRPCs() : T.RPCDefinitions[Name] diff --git a/src/Types.ts b/src/Types.ts index c549473..0b3b9b0 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,13 +3,12 @@ import { RPCSocket } from "./Frontend"; export type AnyFunction = (...args:any) => any export type HookFunction = AnyFunction -export type AccessFilter = (sesame:string|undefined, exporter: I.RPCExporter) => Promise +export type AccessFilter = (sesame:string|undefined, exporter: I.RPCExporter) => Promise | boolean export type Visibility = "127.0.0.1" | "0.0.0.0" export type ConnectionHandler = (socket:I.Socket) => void export type ErrorHandler = (socket:I.Socket, error:any, rpcName: string, args: any[]) => void export type CloseHandler = (socket:I.Socket) => void export type SesameFunction = (sesame : string) => boolean -export type ExceptionHandling = 'local' | 'remote' export type SesameConf = { sesame?: string | SesameFunction } @@ -20,7 +19,7 @@ export type FrontEndHandlerType = { export type ExporterArray = I.RPCExporter, keyof InterfaceT>[] -export type ConnectedSocket = RPCSocket & T +export type ConnectedSocket = RPCSocket & AsyncIfc export type ServerConf = { accessFilter?: AccessFilter @@ -28,7 +27,6 @@ export type ServerConf = { errorHandler?: ErrorHandler closeHandler?: CloseHandler visibility?: Visibility - exceptionHandling?: ExceptionHandling } & SesameConf export type SocketConf = { @@ -95,3 +93,8 @@ export type ExtendedRpcInfo = RpcInfo & { uniqueName: string } export type OnFunction = (type: T, f: FrontEndHandlerType[T]) => void export type HookCloseFunction = (res: T, rpc:HookRPC) => any + + +export type AsyncIfc = { [grp in keyof Ifc]: {[rpcname in keyof Ifc[grp]] : AsyncAnyFunction } } + +export type AsyncAnyFunction = F extends (...args: Parameters) => infer R ? ((...args: Parameters) => R extends Promise ? R : Promise ) : Promise \ No newline at end of file diff --git a/test/Test.ts b/test/Test.ts index 2ead2de..1427a9a 100644 --- a/test/Test.ts +++ b/test/Test.ts @@ -330,7 +330,7 @@ describe('Sesame should unlock the socket', () => { it('should not work without sesame', (done) => { const sock = new RPCSocket(21004, "localhost") - sock.connect( /* no sesame */).then(async (cli) => { + sock.connect( /* no sesame */).then(async (cli) => { if (!cli.test) done() else { @@ -343,7 +343,7 @@ describe('Sesame should unlock the socket', () => { it('should fail with wrong sesame', (done) => { const sock = new RPCSocket(21004, "localhost") - sock.connect('abasd').then(async (cli) => { + sock.connect('abasd').then(async (cli) => { if (!cli.test) done() else { @@ -586,8 +586,8 @@ describe("Class binding", () => { }) beforeEach((done) => { - const s = new RPCSocket(21004, 'localhost') - s.connect("xxx").then(conn => { + const s = new RPCSocket(21004, 'localhost') + s.connect("xxx").then(conn => { sock = conn done() }) @@ -645,7 +645,7 @@ describe("Class binding", () => { describe("attaching handlers before connecting", () => { it("fires error if server is unreachable", (done) => { - const sock = new RPCSocket(21004, 'localhost') + const sock = new RPCSocket(21004, 'localhost') let errorHandleCount = 0 sock.on('error', (err) => { diff --git a/tsconfig.json b/tsconfig.json index 4de165e..47547de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,6 @@ "strict": true, "experimentalDecorators": true }, - "include": ["src/**/*.ts", "test/**/*.ts", "Index.ts"], + "include": ["src/**/*.ts", "test/**/*.ts", "Index.ts", "demo.ts"], "exclude": ["node_modules"] } \ No newline at end of file