[add] first templates

This commit is contained in:
建喵 2022-04-29 14:54:55 +08:00
commit d797a2a41a
24 changed files with 4909 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
.DS_STORE
*.meta

11
.mocharc.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
require: [
'ts-node/register',
],
timeout: 999999,
exit: true,
spec: [
'./test/**/*.test.ts'
],
'preserve-symlinks': true
}

30
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "mocha current file",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
"${file}"
],
"internalConsoleOptions": "openOnSessionStart",
"cwd": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "ts-node current file",
"protocol": "inspector",
"args": [
"${relativeFile}"
],
"cwd": "${workspaceRoot}",
"runtimeArgs": [
"-r",
"ts-node/register"
],
"internalConsoleOptions": "openOnSessionStart"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

30
Dockerfile Normal file
View File

@ -0,0 +1,30 @@
FROM node
# 使用淘宝 NPM 镜像(国内机器构建推荐启用)
# RUN npm config set registry https://registry.npm.taobao.org/
# npm install
ADD package*.json /src/
WORKDIR /src
RUN npm i
# build
ADD . /src
RUN npm run build
# clean
RUN npm prune --production
# move
RUN rm -rf /app \
&& mv dist /app \
&& mv node_modules /app/ \
&& rm -rf /src
# ENV
ENV NODE_ENV production
EXPOSE 3000
WORKDIR /app
CMD node index.js

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# TSRPC Server
## Usage
### Local dev server
Dev server would restart automatically when code changed.
```
npm run dev
```
### Run unit Test
Execute `npm run dev` first, then execute:
```
npm run test
```
### Build
```
npm run build
```
---
## Additional Scripts
### Generate API document
Generate API document in swagger/openapi and markdown format.
```shell
npm run doc
```
### Generate ServiceProto
```
npm run proto
```
### Generate API templates
```
npm run api
```
### Manually sync shared code
```
npm run sync
```

3672
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "backend-.",
"version": "0.1.0",
"main": "index.js",
"private": true,
"scripts": {
"proto": "tsrpc proto",
"sync": "tsrpc link",
"api": "tsrpc api",
"doc": "tsrpc doc",
"dev": "tsrpc dev",
"test": "mocha test/**/*.test.ts",
"build": "tsrpc build"
},
"devDependencies": {
"@types/mocha": "^8.2.3",
"@types/node": "^15.14.9",
"mocha": "^9.1.3",
"onchange": "^7.1.0",
"ts-node": "^10.4.0",
"tsrpc-cli": "^2.2.2",
"typescript": "^4.5.2"
},
"dependencies": {
"tsrpc": "^3.1.3"
}
}

12
src/api/ApiJoin.ts Normal file
View File

@ -0,0 +1,12 @@
import { ApiCallWs } from "tsrpc";
import { roomInstance } from "..";
import { ReqJoin, ResJoin } from "../shared/protocols/PtlJoin";
export async function ApiJoin(call: ApiCallWs<ReqJoin, ResJoin>) {
let playerId = roomInstance.join(call.req, call.conn);
call.succ({
playerId: playerId,
gameState: roomInstance.gameSystem.state
})
}

39
src/index.ts Normal file
View File

@ -0,0 +1,39 @@
import 'k8w-extend-native';
import * as path from "path";
import { WsConnection, WsServer } from "tsrpc";
import { Room } from './models/Room';
import { serviceProto, ServiceType } from './shared/protocols/serviceProto';
// 创建 TSRPC WebSocket Server
export const server = new WsServer(serviceProto, {
port: 3000,
json: true
});
// 断开连接后退出房间
server.flows.postDisconnectFlow.push(v => {
let conn = v.conn as WsConnection<ServiceType>;
if (conn.playerId) {
roomInstance.leave(conn.playerId, conn);
}
return v;
});
export const roomInstance = new Room(server);
// 初始化
async function init() {
// 挂载 API 接口
await server.autoImplementApi(path.resolve(__dirname, 'api'));
// TODO
// Prepare something... (e.g. connect the db)
};
// 启动入口点
async function main() {
await init();
await server.start();
}
main();

101
src/models/Room.ts Normal file
View File

@ -0,0 +1,101 @@
import { WsConnection, WsServer } from "tsrpc";
import { gameConfig } from "../shared/game/gameConfig";
import { GameSystem, GameSystemInput, PlayerJoin } from "../shared/game/GameSystem";
import { ReqJoin } from "../shared/protocols/PtlJoin";
import { ServiceType } from "../shared/protocols/serviceProto";
/**
* - -
*/
export class Room {
// 帧同步频率,次数/秒
syncRate = gameConfig.syncRate;
nextPlayerId = 1;
gameSystem = new GameSystem();
server: WsServer<ServiceType>;
conns: WsConnection<ServiceType>[] = [];
pendingInputs: GameSystemInput[] = [];
playerLastSn: { [playerId: number]: number | undefined } = {};
lastSyncTime?: number;
constructor(server: WsServer<ServiceType>) {
this.server = server;
setInterval(() => { this.sync() }, 1000 / this.syncRate);
}
/** 加入房间 */
join(req: ReqJoin, conn: WsConnection<ServiceType>) {
let input: PlayerJoin = {
type: 'PlayerJoin',
playerId: this.nextPlayerId++,
// 初始位置随机
pos: {
x: Math.random() * 10 - 5,
y: Math.random() * 10 - 5
}
}
this.applyInput(input);
this.conns.push(conn);
conn.playerId = input.playerId;
conn.listenMsg('client/ClientInput', call => {
this.playerLastSn[input.playerId] = call.msg.sn;
call.msg.inputs.forEach(v => {
this.applyInput({
...v,
playerId: input.playerId
});
})
});
return input.playerId;
}
applyInput(input: GameSystemInput) {
this.pendingInputs.push(input);
}
sync() {
let inputs = this.pendingInputs;
this.pendingInputs = [];
// Apply inputs
inputs.forEach(v => {
this.gameSystem.applyInput(v)
});
// Apply TimePast
let now = process.uptime() * 1000;
this.applyInput({
type: 'TimePast',
dt: now - (this.lastSyncTime ?? now)
});
this.lastSyncTime = now;
// 发送同步帧
this.conns.forEach(v => {
v.sendMsg('server/Frame', {
inputs: inputs,
lastSn: this.playerLastSn[v.playerId!]
})
});
}
/** 离开房间 */
leave(playerId: number, conn: WsConnection<ServiceType>) {
this.conns.removeOne(v => v.playerId === playerId);
this.applyInput({
type: 'PlayerLeave',
playerId: playerId
});
}
}
declare module 'tsrpc' {
export interface WsConnection {
playerId?: number;
}
}

View File

@ -0,0 +1,143 @@
import { gameConfig } from "./gameConfig";
import { ArrowState } from "./state/ArrowState";
import { PlayerState } from "./state/PlayerState";
// 状态定义
export interface GameSystemState {
// 当前的时间(游戏时间)
now: number,
// 玩家
players: PlayerState[],
// 飞行中的箭矢
arrows: ArrowState[],
// 箭矢的 ID 生成
nextArrowId: number
}
/**
*
*/
export class GameSystem {
// 当前状态
private _state: GameSystemState = {
now: 0,
players: [],
arrows: [],
nextArrowId: 1
}
get state(): Readonly<GameSystemState> {
return this._state
}
// 重设状态
reset(state: GameSystemState) {
this._state = Object.merge({}, state);
}
// 应用输入,计算状态变更
applyInput(input: GameSystemInput) {
if (input.type === 'PlayerMove') {
let player = this._state.players.find(v => v.id === input.playerId);
if (!player) {
return;
}
if (player.dizzyEndTime && player.dizzyEndTime > this._state.now) {
return;
}
player.pos.x += input.speed.x * input.dt;
player.pos.y += input.speed.y * input.dt;
}
else if (input.type === 'PlayerAttack') {
let player = this._state.players.find(v => v.id === input.playerId);
if (player) {
let newArrow: ArrowState = {
id: this._state.nextArrowId++,
fromPlayerId: input.playerId,
targetPos: { ...input.targetPos },
targetTime: input.targetTime
};
this._state.arrows.push(newArrow);
this.onNewArrow.forEach(v => v(newArrow));
}
}
else if (input.type === 'PlayerJoin') {
this.state.players.push({
id: input.playerId,
pos: { ...input.pos }
})
}
else if (input.type === 'PlayerLeave') {
this.state.players.remove(v => v.id === input.playerId);
}
else if (input.type === 'TimePast') {
this._state.now += input.dt;
// 落地的 Arrow
for (let i = this._state.arrows.length - 1; i > -1; --i) {
let arrow = this._state.arrows[i];
if (arrow.targetTime <= this._state.now) {
// 伤害判定
let damagedPlayers = this._state.players.filter(v => {
return (v.pos.x - arrow.targetPos.x) * (v.pos.x - arrow.targetPos.x) + (v.pos.y - arrow.targetPos.y) * (v.pos.y - arrow.targetPos.y) <= gameConfig.arrowAttackRadius * gameConfig.arrowAttackRadius
});
damagedPlayers.forEach(p => {
// 设置击晕状态
p.dizzyEndTime = this._state.now + gameConfig.arrowDizzyTime;
// Event
})
this._state.arrows.splice(i, 1);
}
}
}
}
/*
*
*
*
*
*/
// 发射箭矢
onNewArrow: ((arrow: ArrowState) => void)[] = [];
}
export interface PlayerMove {
type: 'PlayerMove',
playerId: number,
speed: { x: number, y: number },
// 移动的时间 (秒)
dt: number,
}
export interface PlayerAttack {
type: 'PlayerAttack',
playerId: number,
// 落点坐标
targetPos: { x: number, y: number },
// 落点时间(游戏时间)
targetTime: number
}
export interface PlayerJoin {
type: 'PlayerJoin',
playerId: number,
pos: { x: number, y: number }
}
export interface PlayerLeave {
type: 'PlayerLeave',
playerId: number
}
// 时间流逝
export interface TimePast {
type: 'TimePast',
dt: number
}
// 输入定义
export type GameSystemInput = PlayerMove
| PlayerAttack
| PlayerJoin
| PlayerLeave
| TimePast;

View File

@ -0,0 +1,14 @@
export const gameConfig = {
syncRate: 10,
moveSpeed: 10,
// 箭矢飞行时间(毫秒)
arrowFlyTime: 500,
// 箭矢投掷距离
arrowDistance: 8,
// 箭矢落地命中判定半径
arrowAttackRadius: 2,
// 被箭矢几种后的晕眩时间(毫秒)
arrowDizzyTime: 1000
}

View File

@ -0,0 +1,9 @@
export type ArrowState = {
id: number,
// 谁发出的箭
fromPlayerId: number,
// 落地时间(游戏时间)
targetTime: number,
// 落点位置(游戏位置)
targetPos: { x: number, y: number }
}

View File

@ -0,0 +1,7 @@
export interface PlayerState {
id: number,
// 位置
pos: { x: number, y: number },
// 晕眩结束时间
dizzyEndTime?: number,
}

View File

@ -0,0 +1,15 @@
import { GameSystemState } from "../game/GameSystem";
/** 加入房间 */
export interface ReqJoin {
}
export interface ResJoin {
/** 加入房间后,自己的 ID */
playerId: number,
/** 状态同步:一次性同步当前状态 */
gameState: GameSystemState
}
// export const conf = {}

View File

@ -0,0 +1,9 @@
import { PlayerAttack, PlayerMove } from "../../game/GameSystem";
/** 发送自己的输入 */
export interface MsgClientInput {
sn: number,
inputs: ClientInput[]
};
export type ClientInput = Omit<PlayerMove, 'playerId'> | Omit<PlayerAttack, 'playerId'>;

View File

@ -0,0 +1,11 @@
import { GameSystemInput } from "../../game/GameSystem";
/**
* 广
*
*/
export interface MsgFrame {
inputs: GameSystemInput[],
/** 当前用户提交的,经服务端确认的最后一条输入的 SN */
lastSn?: number
}

View File

@ -0,0 +1,496 @@
import { ServiceProto } from 'tsrpc-proto';
import { MsgClientInput } from './client/MsgClientInput';
import { ReqJoin, ResJoin } from './PtlJoin';
import { MsgFrame } from './server/MsgFrame';
export interface ServiceType {
api: {
"Join": {
req: ReqJoin,
res: ResJoin
}
},
msg: {
"client/ClientInput": MsgClientInput,
"server/Frame": MsgFrame
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"services": [
{
"id": 0,
"name": "client/ClientInput",
"type": "msg"
},
{
"id": 1,
"name": "Join",
"type": "api"
},
{
"id": 2,
"name": "server/Frame",
"type": "msg"
}
],
"types": {
"client/MsgClientInput/MsgClientInput": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "sn",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "inputs",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "client/MsgClientInput/ClientInput"
}
}
}
]
},
"client/MsgClientInput/ClientInput": {
"type": "Union",
"members": [
{
"id": 0,
"type": {
"target": {
"type": "Reference",
"target": "../game/GameSystem/PlayerMove"
},
"keys": [
"playerId"
],
"type": "Omit"
}
},
{
"id": 1,
"type": {
"target": {
"type": "Reference",
"target": "../game/GameSystem/PlayerAttack"
},
"keys": [
"playerId"
],
"type": "Omit"
}
}
]
},
"../game/GameSystem/PlayerMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerMove"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "speed",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
},
{
"id": 3,
"name": "dt",
"type": {
"type": "Number"
}
}
]
},
"../game/GameSystem/PlayerAttack": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerAttack"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "targetPos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
},
{
"id": 3,
"name": "targetTime",
"type": {
"type": "Number"
}
}
]
},
"PtlJoin/ReqJoin": {
"type": "Interface"
},
"PtlJoin/ResJoin": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "gameState",
"type": {
"type": "Reference",
"target": "../game/GameSystem/GameSystemState"
}
}
]
},
"../game/GameSystem/GameSystemState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "now",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "players",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "../game/state/PlayerState/PlayerState"
}
}
},
{
"id": 2,
"name": "arrows",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "../game/state/ArrowState/ArrowState"
}
}
},
{
"id": 3,
"name": "nextArrowId",
"type": {
"type": "Number"
}
}
]
},
"../game/state/PlayerState/PlayerState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "pos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
},
{
"id": 2,
"name": "dizzyEndTime",
"type": {
"type": "Number"
},
"optional": true
}
]
},
"../game/state/ArrowState/ArrowState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "fromPlayerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "targetTime",
"type": {
"type": "Number"
}
},
{
"id": 3,
"name": "targetPos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
}
]
},
"server/MsgFrame/MsgFrame": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "inputs",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "../game/GameSystem/GameSystemInput"
}
}
},
{
"id": 1,
"name": "lastSn",
"type": {
"type": "Number"
},
"optional": true
}
]
},
"../game/GameSystem/GameSystemInput": {
"type": "Union",
"members": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerMove"
}
},
{
"id": 1,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerAttack"
}
},
{
"id": 2,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerJoin"
}
},
{
"id": 3,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerLeave"
}
},
{
"id": 4,
"type": {
"type": "Reference",
"target": "../game/GameSystem/TimePast"
}
}
]
},
"../game/GameSystem/PlayerJoin": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerJoin"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "pos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
}
]
},
"../game/GameSystem/PlayerLeave": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerLeave"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
}
]
},
"../game/GameSystem/TimePast": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "TimePast"
}
},
{
"id": 1,
"name": "dt",
"type": {
"type": "Number"
}
}
]
}
}
};

39
test/api/ApiSend.test.ts Normal file
View File

@ -0,0 +1,39 @@
import assert from "assert";
import { TsrpcError, WsClient } from "tsrpc";
import { serviceProto, ServiceType } from "../../src/shared/protocols/serviceProto";
// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
// 2. EXECUTE `npm test` TO START UNIT TEST
describe("ApiSend", function (): void {
let client: WsClient<ServiceType> = new WsClient(serviceProto, {
server: "ws://127.0.0.1:3000",
logger: console
});
before(async function (): Promise<void> {
let res: any = await client.connect();
assert.strictEqual(res.isSucc, true, "Failed to connect to server, have you executed `npm run dev` already?");
});
it("Success", async function (): Promise<void> {
let ret: any = await client.callApi("Send", {
content: "Test"
});
assert.ok(ret.isSucc)
});
it("Check content is empty", async function (): Promise<void> {
let ret: any = await client.callApi("Send", {
content: ""
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError("Content is empty")
});
});
after(async function () {
await client.disconnect();
});
});

15
test/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
}
}

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": [
"src"
]
}

116
tslint.json Normal file
View File

@ -0,0 +1,116 @@
{
"defaultSeverity": "warning",
"rules": {
"ban": [
true,
[
"_",
"extend"
],
[
"_",
"isNull"
],
[
"_",
"isDefined"
]
],
"class-name": false,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": false,
"forin": false,
"indent": [
true,
4
],
"interface-name": [
true,
"never-prefix"
],
"jsdoc-format": true,
"label-position": true,
"label-undefined": true,
"max-line-length": [
false,
140
],
"no-arg": true,
"no-bitwise": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": true,
// "no-eval": true,
"no-string-literal": false,
"no-trailing-comma": true,
"no-trailing-whitespace": true,
"no-unused-expression": false,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": [
true,
"double"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef": [
true,
"call-signature",
"parameter",
"property-declaration",
"variable-declaration"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace"
},
{
"index-signature": "space"
}
],
"use-strict": [
true,
"check-module",
"check-function"
],
"variable-name": false,
"whitespace": [
false,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

39
tsrpc.config.ts Normal file
View File

@ -0,0 +1,39 @@
import { TsrpcConfig } from 'tsrpc-cli';
const tsrpcConf: TsrpcConfig = {
// Generate ServiceProto
proto: [
{
ptlDir: 'src/shared/protocols', // Protocol dir
output: 'src/shared/protocols/serviceProto.ts', // Path for generated ServiceProto
apiDir: 'src/api', // API dir
docDir: 'docs', // API documents dir
// ptlTemplate: CodeTemplate.getExtendedPtl(),
// msgTemplate: CodeTemplate.getExtendedMsg(),
}
],
// Sync shared code
sync: [
{
from: 'src/shared',
to: '../frontend/assets/scripts/shared',
type: 'symlink' // Change this to 'copy' if your environment not support symlink
}
],
// Dev server
dev: {
autoProto: true, // Auto regenerate proto
autoSync: true, // Auto sync when file changed
autoApi: true, // Auto create API when ServiceProto updated
watch: 'src', // Restart dev server when these files changed
entry: 'src/index.ts', // Dev server command: node -r ts-node/register {entry}
},
// Build config
build: {
autoProto: true, // Auto generate proto before build
autoSync: true, // Auto sync before build
autoApi: true, // Auto generate API before build
outDir: 'dist', // Clean this dir before build
}
}
export default tsrpcConf;