Feat : create games

This commit is contained in:
2024-06-06 19:50:29 -04:00
parent 30e9a3efcb
commit e870a254ed
18 changed files with 379 additions and 82 deletions
+12
View File
@@ -0,0 +1,12 @@
version: '3.1'
services:
mongo:
image: docker.io/mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- 27017:27017
+54
View File
@@ -16,6 +16,7 @@
"@nestjs/mongoose": "^10.0.6", "@nestjs/mongoose": "^10.0.6",
"@nestjs/platform-express": "^10.3.2", "@nestjs/platform-express": "^10.3.2",
"@nestjs/swagger": "^7.3.1", "@nestjs/swagger": "^7.3.1",
"connect-mongo": "^5.1.0",
"express-session": "^1.18.0", "express-session": "^1.18.0",
"mongoose": "^8.4.1", "mongoose": "^8.4.1",
"reflect-metadata": "^0.2.1", "reflect-metadata": "^0.2.1",
@@ -3428,6 +3429,18 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true "dev": true
}, },
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3715,6 +3728,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@@ -4262,6 +4281,23 @@
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
"node_modules/connect-mongo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz",
"integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.1",
"kruptein": "^3.0.0"
},
"engines": {
"node": ">=12.9.0"
},
"peerDependencies": {
"express-session": "^1.17.1",
"mongodb": ">= 5.1.0 < 7"
}
},
"node_modules/consola": { "node_modules/consola": {
"version": "2.15.3", "version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@@ -7121,6 +7157,18 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/kruptein": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.6.tgz",
"integrity": "sha512-EQJjTwAJfQkC4NfdQdo3HXM2a9pmBm8oidzH270cYu1MbgXPNPMJuldN7OPX+qdhPO5rw4X3/iKz0BFBfkXGKA==",
"license": "MIT",
"dependencies": {
"asn1.js": "^5.4.1"
},
"engines": {
"node": ">8"
}
},
"node_modules/leven": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -7381,6 +7429,12 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+1
View File
@@ -26,6 +26,7 @@
"@nestjs/mongoose": "^10.0.6", "@nestjs/mongoose": "^10.0.6",
"@nestjs/platform-express": "^10.3.2", "@nestjs/platform-express": "^10.3.2",
"@nestjs/swagger": "^7.3.1", "@nestjs/swagger": "^7.3.1",
"connect-mongo": "^5.1.0",
"express-session": "^1.18.0", "express-session": "^1.18.0",
"mongoose": "^8.4.1", "mongoose": "^8.4.1",
"reflect-metadata": "^0.2.1", "reflect-metadata": "^0.2.1",
+5 -2
View File
@@ -10,8 +10,11 @@ import { CardsService } from './cards/cards.service';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot(), ConfigModule.forRoot(),
MongooseModule.forRoot('mongodb://localhost:27017', { MongooseModule.forRoot(process.env.MONGO_ENDPOINT, {
auth: { password: 'example', username: 'root' }, auth: {
password: process.env.MONGO_PASSWORD,
username: process.env.MONGO_USER,
},
dbName: 'heron', dbName: 'heron',
}), }),
GamesModule, GamesModule,
-1
View File
@@ -1 +0,0 @@
export class CreateGameDto {}
+11
View File
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { Types } from 'mongoose';
export class DatabaseObjectDto {
constructor(data: { _id: Types.ObjectId }) {
this._id = data._id.toString();
}
@ApiProperty()
_id: string;
}
+86
View File
@@ -0,0 +1,86 @@
import { ApiProperty } from '@nestjs/swagger';
import { DatabaseObjectDto } from './database-object.dto';
import { Container, Team } from '../schemas/game.schema';
import { User } from 'src/users/schemas/user.entity';
export class ReadTeamDto extends DatabaseObjectDto {
constructor(data: ConstructorParameters<typeof DatabaseObjectDto>[0]) {
super(data);
}
}
export class ReadContainerDto extends DatabaseObjectDto {
constructor(data: ConstructorParameters<typeof DatabaseObjectDto>[0]) {
super(data);
}
}
export class ReadUserDto extends DatabaseObjectDto {
constructor(
data: { username: string; avatar: string } & ConstructorParameters<
typeof DatabaseObjectDto
>[0],
) {
super(data);
this.username = data.username;
this.avatar = data.avatar;
}
@ApiProperty()
username: string;
@ApiProperty()
avatar: string;
}
export class ReadGameDto extends DatabaseObjectDto {
constructor(
data: {
teams: Team[];
market: Container;
marketStack: Container;
fireGems: Container;
currentTurn?: Team;
started: boolean;
ended: boolean;
owner: User;
} & ConstructorParameters<typeof DatabaseObjectDto>[0],
) {
super(data);
this.teams = data.teams.map((e) => new ReadTeamDto(e));
this.market = new ReadContainerDto(data.market);
this.marketStack = new ReadContainerDto(data.marketStack);
this.fireGems = new ReadContainerDto(data.fireGems);
this.started = data.started;
this.ended = data.ended;
this.owner = new ReadUserDto(data.owner);
if (data.currentTurn) {
this.currentTurn = new ReadTeamDto(data.currentTurn);
}
}
@ApiProperty()
teams: ReadTeamDto[];
@ApiProperty()
market: ReadContainerDto;
@ApiProperty()
marketStack: ReadContainerDto;
@ApiProperty()
fireGems: ReadContainerDto;
@ApiProperty()
currentTurn?: ReadTeamDto;
@ApiProperty()
started: boolean;
@ApiProperty()
ended: boolean;
@ApiProperty()
owner: ReadUserDto;
}
+1 -4
View File
@@ -1,4 +1 @@
import { PartialType } from '@nestjs/mapped-types'; export class UpdateGameDto {}
import { CreateGameDto } from './create-game.dto';
export class UpdateGameDto extends PartialType(CreateGameDto) {}
+32 -8
View File
@@ -2,32 +2,56 @@ import {
Controller, Controller,
Get, Get,
Post, Post,
Body,
Patch,
Param, Param,
Delete, Delete,
UseGuards,
Session,
HttpException,
HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { GamesService } from './games.service'; import { GamesService } from './games.service';
import { CreateGameDto } from './dto/create-game.dto'; import { AuthGuard } from './guards/auth.guard';
import { UpdateGameDto } from './dto/update-game.dto'; import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { DatabaseObjectDto } from './dto/database-object.dto';
@Controller('games') @Controller('games')
export class GamesController { export class GamesController {
constructor(private readonly gamesService: GamesService) {} constructor(private readonly gamesService: GamesService) {}
@Post() @Post()
create() { @ApiOperation({ summary: 'Create game' })
return this.gamesService.create(); @ApiResponse({
status: 201,
description: 'Game successfully created.',
type: DatabaseObjectDto,
})
@ApiResponse({
status: 400,
description: 'You can only create one game at a time',
})
@ApiResponse({ status: 403, description: 'Forbidden.' })
@UseGuards(AuthGuard)
async create(@Session() session: Record<string, any>) {
const games = await this.gamesService.findByOwner(session.user._id);
if (games.length > 0) {
throw new HttpException(
'You can only create one game at a time',
HttpStatus.BAD_REQUEST,
);
}
return this.gamesService.create(session.user);
} }
@Get() @Get()
@ApiOperation({ summary: 'List all games' })
findAll() { findAll() {
return this.gamesService.findAll(); const games = this.gamesService.findAll();
return games;
} }
@Get(':id') @Get(':id')
findOne(@Param('id') id: string) { findOne(@Param('id') id: string) {
return this.gamesService.findOne(id); return this.gamesService.findById(id);
} }
// @Patch(':id') // @Patch(':id')
+17 -8
View File
@@ -1,24 +1,33 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { CreateGameDto } from './dto/create-game.dto'; import { DatabaseObjectDto } from './dto/database-object.dto';
import { UpdateGameDto } from './dto/update-game.dto';
import { InjectModel } from '@nestjs/mongoose'; import { InjectModel } from '@nestjs/mongoose';
import { Game } from './schemas/game.schema'; import { Game } from './schemas/game.schema';
import { Model } from 'mongoose'; import { Model } from 'mongoose';
import { ReadGameDto } from './dto/read-games.dto';
import { User } from 'src/users/schemas/user.entity';
@Injectable() @Injectable()
export class GamesService { export class GamesService {
constructor(@InjectModel(Game.name) private gameModel: Model<Game>) {} constructor(@InjectModel(Game.name) private gameModel: Model<Game>) {}
async create(): Promise<Game> {
async create(owner: User): Promise<DatabaseObjectDto> {
const createdGame = new this.gameModel(); const createdGame = new this.gameModel();
return createdGame.save(); createdGame.owner = owner;
const gamedata = await createdGame.save();
return new DatabaseObjectDto(gamedata);
} }
findAll() { async findAll(): Promise<ReadGameDto[]> {
return this.gameModel.find(); const games = await this.gameModel.find().exec();
return games.map((e) => new ReadGameDto(e));
} }
findOne(id: string) { findById(id: string) {
return this.gameModel.find({ _id: id }).exec(); return this.gameModel.findById(id).exec();
}
findByOwner(_id: string) {
return this.gameModel.find({ owner: _id }).exec();
} }
// update(id: number, updateGameDto: UpdateGameDto) { // update(id: number, updateGameDto: UpdateGameDto) {
+23
View File
@@ -0,0 +1,23 @@
import {
Injectable,
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const http = context.switchToHttp();
const request = http.getRequest<Request>();
if (!(request.session as Record<string, any>).user) {
throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED);
}
return true;
}
}
+5 -8
View File
@@ -1,20 +1,17 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { HydratedDocument } from 'mongoose'; import mongoose, { HydratedDocument, Types } from 'mongoose';
import { Card } from 'src/cards/schemas/cards.schema'; import { Card } from 'src/cards/schemas/cards.schema';
import { User } from 'src/users/schemas/user.entity'; import { User } from 'src/users/schemas/user.entity';
export type GameDocument = HydratedDocument<Game>; export type GameDocument = HydratedDocument<Game>;
@Schema() @Schema()
class GameObject {} export class Team extends Types.ObjectId {}
@Schema()
export class Team extends GameObject {}
export const TeamSchema = SchemaFactory.createForClass(Team); export const TeamSchema = SchemaFactory.createForClass(Team);
@Schema() @Schema()
export class Container extends GameObject { export class Container extends Types.ObjectId {
@Prop([{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]) @Prop([{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }])
cards: Card[]; cards: Card[];
} }
@@ -22,7 +19,7 @@ export class Container extends GameObject {
export const ContainerSchema = SchemaFactory.createForClass(Container); export const ContainerSchema = SchemaFactory.createForClass(Container);
@Schema() @Schema()
export class Game extends GameObject { export class Game extends Types.ObjectId {
@Prop([Team]) @Prop([Team])
teams: Team[]; teams: Team[];
@@ -42,7 +39,7 @@ export class Game extends GameObject {
started: boolean; started: boolean;
@Prop({ default: false }) @Prop({ default: false })
private ended: boolean; ended: boolean;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' }) @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
owner: User; owner: User;
+14 -2
View File
@@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
import * as session from 'express-session'; import * as session from 'express-session';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import MongoStore from 'connect-mongo';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@@ -15,11 +16,22 @@ async function bootstrap() {
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);
app.use( app.use(
session({ session.default({
secret: 'my-secret', secret: process.env.SESSION_SECRET,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
name: 'heron.session', name: 'heron.session',
store: MongoStore.create({
collectionName: 'sessions',
dbName: 'heron',
mongoUrl: process.env.MONGO_ENDPOINT,
mongoOptions: {
auth: {
password: process.env.MONGO_PASSWORD,
username: process.env.MONGO_USER,
},
},
}),
}), }),
); );
+6 -9
View File
@@ -1,19 +1,16 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class LoginDto extends PartialType(CreateUserDto) { export class LoginDto {
constructor(id: string, global_name: string, avatar: string) { constructor(userId: string, username: string, avatar: string) {
super(); this.userId = userId;
this.id = id; this.username = username;
this.global_name = global_name;
this.avatar = avatar; this.avatar = avatar;
} }
@ApiProperty() @ApiProperty()
id: string; userId: string;
@ApiProperty() @ApiProperty()
global_name: string; username: string;
@ApiProperty() @ApiProperty()
avatar: string; avatar: string;
+12 -3
View File
@@ -1,9 +1,18 @@
import { Schema, SchemaFactory } from '@nestjs/mongoose'; import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose'; import { HydratedDocument, Types } from 'mongoose';
export type UserDocument = HydratedDocument<User>; export type UserDocument = HydratedDocument<User>;
@Schema() @Schema()
export class User {} export class User extends Types.ObjectId {
@Prop()
userId: string;
@Prop()
avatar: string;
@Prop()
username: string;
}
export const UserSchema = SchemaFactory.createForClass(User); export const UserSchema = SchemaFactory.createForClass(User);
+4 -11
View File
@@ -19,13 +19,9 @@ export class UsersController {
async login( async login(
@Query('code') code: string, @Query('code') code: string,
@Session() session: Record<string, any>, @Session() session: Record<string, any>,
) { ): Promise<LoginDto> {
const tokenData = await this.usersService.saveDiscordToken(code); await this.usersService.createDiscordToken({ code }, session);
session.tokenData = tokenData; const user = await this.usersService.discordLogin(session);
const user = await this.usersService.useDiscordToken(
tokenData.token_type,
tokenData.token,
);
return user; return user;
} }
@@ -37,10 +33,7 @@ export class UsersController {
HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED,
); );
} }
const user = await this.usersService.useDiscordToken( const user = await this.usersService.discordLogin(session);
session.tokenData.token_type,
session.tokenData.token,
);
return user; return user;
} }
} }
+94 -25
View File
@@ -1,30 +1,67 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import { User } from './schemas/user.entity';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
async saveDiscordToken(code: string) { constructor(@InjectModel(User.name) private userModel: Model<User>) {}
async createDiscordToken(
{
code,
refresh_token,
}: {
code?: string;
refresh_token?: string;
},
session: Record<string, any>,
): Promise<void> {
let body;
if (code) {
body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENTID,
client_secret: process.env.DISCORD_CLIENTSECRET,
code,
grant_type: 'authorization_code',
redirect_uri: process.env.API_ENDPOINT + '/login',
});
} else if (refresh_token) {
body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENTID,
client_secret: process.env.DISCORD_CLIENTSECRET,
refresh_token,
grant_type: 'refresh_token',
});
} else {
throw new HttpException(
'Invalid internal call',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const req = await fetch('https://discord.com/api/oauth2/token', { const req = await fetch('https://discord.com/api/oauth2/token', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: new URLSearchParams({ body,
client_id: process.env.DISCORD_CLIENTID,
client_secret: process.env.DISCORD_CLIENTSECRET,
code: code,
grant_type: 'authorization_code',
redirect_uri: process.env.API_ENDPOINT + '/login',
scope: 'identify',
}),
}); });
if (!req.ok) { if (!req.ok) {
console.error(await req.json()); const data = await req.json();
throw new HttpException( if (data.error == 'invalid_grant') {
"Couldn't fetch Discord api", throw new HttpException(
HttpStatus.INTERNAL_SERVER_ERROR, 'Invalid code in request',
); HttpStatus.UNAUTHORIZED,
);
} else {
console.error(data);
throw new HttpException(
"Couldn't fetch Discord api",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} }
const data = await req.json(); const data = await req.json();
if (!data.token_type || !data.access_token) { if (!data.token_type || !data.access_token) {
@@ -34,27 +71,59 @@ export class UsersService {
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
const token_type = data.token_type; const expires = new Date(Date.now() + data.expires_in);
const token = data.access_token; session.tokenData = {
access_token: data.access_token,
return { token, token_type }; token_type: data.token_type,
refresh_token: data.refresh_token,
expires,
};
} }
async useDiscordToken(token_type: string, token: string) { async discordLogin(
session: Record<string, any>,
recursive: boolean = false,
): Promise<LoginDto> {
const req2 = await fetch('https://discord.com/api/users/@me', { const req2 = await fetch('https://discord.com/api/users/@me', {
headers: { headers: {
authorization: token_type + ' ' + token, authorization:
session.tokenData.token_type + ' ' + session.tokenData.access_token,
}, },
}); });
if (!req2.ok) { if (!req2.ok) {
throw new HttpException( const error = await req2.json();
"Couldn't fetch Discord api", if (error.error == 'invalid_grant' && !recursive) {
HttpStatus.INTERNAL_SERVER_ERROR, await this.createDiscordToken(
); {
refresh_token: session.tokenData.refresh_token,
},
session,
);
return this.discordLogin(session);
} else {
console.error(error);
throw new HttpException(
"Couldn't fetch Discord api",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} }
const data2 = await req2.json(); const data2 = await req2.json();
return new LoginDto(data2.id, data2.global_name, data2.avatar); console.log(data2);
const dto = new LoginDto(data2.id, data2.global_name, data2.avatar);
const user = await this.getOrCreateUser(dto.userId, dto);
session.user = user;
return dto;
}
async getOrCreateUser(userId: string, data?: LoginDto) {
let user = await this.userModel.findOne({ userId }).exec();
if (!user) {
const createdUser = new this.userModel({ ...data, userId });
user = await createdUser.save();
}
return user;
} }
remove(id: number) { remove(id: number) {
+2 -1
View File
@@ -11,6 +11,7 @@
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./",
"incremental": true, "incremental": true,
"skipLibCheck": true "skipLibCheck": true,
"esModuleInterop": true
} }
} }