Migrate to @atums/echo logger and improve project structure
Some checks failed
Code quality checks / biome (push) Failing after 12s
Some checks failed
Code quality checks / biome (push) Failing after 12s
This commit is contained in:
parent
490cd9d0e0
commit
fdce8810cf
11 changed files with 276 additions and 100 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
/node_modules
|
||||
bun.lock
|
||||
robots.txt
|
||||
logs
|
||||
public/custom
|
||||
|
|
28
LICENSE
Normal file
28
LICENSE
Normal file
|
@ -0,0 +1,28 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2025, creations.works
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
68
README.md
68
README.md
|
@ -1,3 +1,67 @@
|
|||
# bun frontend template
|
||||
# Bun Frontend Template
|
||||
|
||||
a simple bun frontend starting point i made and use
|
||||
A minimal, fast, and type-safe web server template built with [Bun](https://bun.sh) and TypeScript. Features file-system based routing, static file serving, WebSocket support, and structured logging.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `HOST` | Server host address | `0.0.0.0` | ✅ |
|
||||
| `PORT` | Server port | `8080` | ✅ |
|
||||
| `NODE_ENV` | Environment mode | `production` | ❌ |
|
||||
|
||||
### Creating Routes
|
||||
|
||||
Routes are automatically generated from files in `src/routes/`. Each route file exports:
|
||||
|
||||
```typescript
|
||||
// src/routes/example.ts
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET", // HTTP method(s)
|
||||
accepts: "application/json", // Content-Type validation
|
||||
returns: "application/json", // Response Content-Type
|
||||
needsBody?: "json" | "multipart" // Optional body parsing, dont include if neither are required
|
||||
};
|
||||
|
||||
async function handler(
|
||||
request: ExtendedRequest,
|
||||
requestBody: unknown,
|
||||
server: BunServer
|
||||
): Promise<Response> {
|
||||
return Response.json({ message: "Hello World" });
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
||||
```
|
||||
|
||||
### Route Features
|
||||
|
||||
- **Method Validation** - Automatic HTTP method checking
|
||||
- **Content-Type Validation** - Request/response content type enforcement
|
||||
- **Body Parsing** - Automatic JSON/FormData parsing
|
||||
- **Query Parameters** - Automatic query string parsing
|
||||
- **URL Parameters** - Next.js-style dynamic routes (`[id].ts`)
|
||||
|
||||
## Static Files
|
||||
|
||||
Place files in `public/` directory
|
||||
|
||||
### Custom Public Files
|
||||
|
||||
Files in `public/custom/` are served with security checks:
|
||||
- Path traversal protection
|
||||
- Content-type detection
|
||||
- Direct file serving
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the BSD-3-Clause - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **[@atums/echo](https://www.npmjs.com/package/@atums/echo)** - Structured logging with daily rotation
|
||||
- **[Bun](https://bun.sh)** - Fast JavaScript runtime and bundler
|
||||
- **[TypeScript](https://www.typescriptlang.org/)** - Type-safe JavaScript
|
||||
- **[Biome](https://biomejs.dev/)** - Fast formatter and linter
|
||||
|
|
24
biome.json
24
biome.json
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"ignore": []
|
||||
"ignore": ["dist"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
@ -17,11 +17,29 @@
|
|||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"css": {
|
||||
"formatter": {
|
||||
"indentStyle": "tab",
|
||||
"lineEnding": "lf"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedImports": "error",
|
||||
"noUnusedVariables": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": "error"
|
||||
},
|
||||
"style": {
|
||||
"useConst": "error",
|
||||
"noVar": "error"
|
||||
}
|
||||
},
|
||||
"ignore": ["types"]
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { resolve } from "node:path";
|
||||
import { logger } from "@creations.works/logger";
|
||||
import { echo } from "@atums/echo";
|
||||
|
||||
const environment: Environment = {
|
||||
port: Number.parseInt(process.env.PORT || "8080", 10),
|
||||
|
@ -8,10 +7,6 @@ const environment: Environment = {
|
|||
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
||||
};
|
||||
|
||||
const robotstxtPath: string | null = process.env.ROBOTS_FILE
|
||||
? resolve(process.env.ROBOTS_FILE)
|
||||
: null;
|
||||
|
||||
function verifyRequiredVariables(): void {
|
||||
const requiredVariables = ["HOST", "PORT"];
|
||||
|
||||
|
@ -20,7 +15,7 @@ function verifyRequiredVariables(): void {
|
|||
for (const key of requiredVariables) {
|
||||
const value = process.env[key];
|
||||
if (value === undefined || value.trim() === "") {
|
||||
logger.error(`Missing or empty environment variable: ${key}`);
|
||||
echo.error(`Missing or empty environment variable: ${key}`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
@ -30,4 +25,4 @@ function verifyRequiredVariables(): void {
|
|||
}
|
||||
}
|
||||
|
||||
export { environment, robotstxtPath, verifyRequiredVariables };
|
||||
export { environment, verifyRequiredVariables };
|
||||
|
|
39
logger.json
Normal file
39
logger.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"directory": "logs",
|
||||
"level": "debug",
|
||||
"disableFile": false,
|
||||
|
||||
"rotate": true,
|
||||
"maxFiles": 3,
|
||||
|
||||
"console": true,
|
||||
"consoleColor": true,
|
||||
|
||||
"dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
|
||||
"timezone": "local",
|
||||
|
||||
"silent": false,
|
||||
|
||||
"pattern": "{color:gray}{pretty-timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{color:blue}{line}{reset}:{color:blue}{column}{color:gray}){reset} {data}",
|
||||
"levelColor": {
|
||||
"debug": "blue",
|
||||
"info": "green",
|
||||
"warn": "yellow",
|
||||
"error": "red",
|
||||
"fatal": "red"
|
||||
},
|
||||
|
||||
"customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
|
||||
"customColors": {
|
||||
"GET": "green",
|
||||
"POST": "blue",
|
||||
"PUT": "yellow",
|
||||
"DELETE": "red",
|
||||
"PATCH": "cyan",
|
||||
"HEAD": "magenta",
|
||||
"OPTIONS": "white",
|
||||
"TRACE": "gray"
|
||||
},
|
||||
|
||||
"prettyPrint": true
|
||||
}
|
11
package.json
11
package.json
|
@ -10,16 +10,13 @@
|
|||
"cleanup": "rm -rf logs node_modules bun.lockdb"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.10",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"globals": "^16.0.0",
|
||||
"@biomejs/biome": "^1.9.4"
|
||||
"@types/bun": "latest",
|
||||
"@biomejs/biome": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@creations.works/logger": "^1.0.3",
|
||||
"ejs": "^3.1.10"
|
||||
"@atums/echo": "latest"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { logger } from "@creations.works/logger";
|
||||
import { echo } from "@atums/echo";
|
||||
|
||||
import { serverHandler } from "@/server";
|
||||
import { verifyRequiredVariables } from "@config/environment";
|
||||
import { serverHandler } from "@server";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
verifyRequiredVariables();
|
||||
|
@ -10,6 +10,6 @@ async function main(): Promise<void> {
|
|||
}
|
||||
|
||||
main().catch((error: Error) => {
|
||||
logger.error(["Error initializing the server:", error]);
|
||||
echo.error({ message: "Error initializing the server:", error });
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
138
src/server.ts
138
src/server.ts
|
@ -1,14 +1,14 @@
|
|||
import { resolve } from "node:path";
|
||||
import { environment, robotstxtPath } from "@config/environment";
|
||||
import { logger } from "@creations.works/logger";
|
||||
import { echo } from "@atums/echo";
|
||||
import { environment } from "@config/environment";
|
||||
import {
|
||||
type BunFile,
|
||||
FileSystemRouter,
|
||||
type MatchedRoute,
|
||||
type Serve,
|
||||
type Server,
|
||||
} from "bun";
|
||||
|
||||
import { webSocketHandler } from "@/websocket";
|
||||
import { webSocketHandler } from "@websocket";
|
||||
|
||||
class ServerHandler {
|
||||
private router: FileSystemRouter;
|
||||
|
@ -19,14 +19,14 @@ class ServerHandler {
|
|||
) {
|
||||
this.router = new FileSystemRouter({
|
||||
style: "nextjs",
|
||||
dir: "./src/routes",
|
||||
dir: resolve("src", "routes"),
|
||||
fileExtensions: [".ts"],
|
||||
origin: `http://${this.host}:${this.port}`,
|
||||
});
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
const server: Serve = Bun.serve({
|
||||
const server: Server = Bun.serve({
|
||||
port: this.port,
|
||||
hostname: this.host,
|
||||
fetch: this.handleRequest.bind(this),
|
||||
|
@ -37,15 +37,13 @@ class ServerHandler {
|
|||
},
|
||||
});
|
||||
|
||||
logger.info(`Server running at http://${server.hostname}:${server.port}`, {
|
||||
breakLine: true,
|
||||
});
|
||||
echo.info(`Server running at http://${server.hostname}:${server.port}`);
|
||||
|
||||
this.logRoutes();
|
||||
}
|
||||
|
||||
private logRoutes(): void {
|
||||
logger.info("Available routes:");
|
||||
echo.info("Available routes:");
|
||||
|
||||
const sortedRoutes: [string, string][] = Object.entries(
|
||||
this.router.routes,
|
||||
|
@ -54,14 +52,19 @@ class ServerHandler {
|
|||
);
|
||||
|
||||
for (const [path, filePath] of sortedRoutes) {
|
||||
logger.info(`Route: ${path}, File: ${filePath}`);
|
||||
echo.info(`Route: ${path}, File: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async serveStaticFile(pathname: string): Promise<Response> {
|
||||
try {
|
||||
let filePath: string;
|
||||
private async serveStaticFile(
|
||||
request: ExtendedRequest,
|
||||
pathname: string,
|
||||
ip: string,
|
||||
): Promise<Response> {
|
||||
let filePath: string;
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
if (pathname === "/favicon.ico") {
|
||||
filePath = resolve("public", "assets", "favicon.ico");
|
||||
} else {
|
||||
|
@ -72,18 +75,49 @@ class ServerHandler {
|
|||
|
||||
if (await file.exists()) {
|
||||
const fileContent: ArrayBuffer = await file.arrayBuffer();
|
||||
const contentType: string = file.type || "application/octet-stream";
|
||||
const contentType: string = file.type ?? "application/octet-stream";
|
||||
|
||||
return new Response(fileContent, {
|
||||
response = new Response(fileContent, {
|
||||
headers: { "Content-Type": contentType },
|
||||
});
|
||||
} else {
|
||||
echo.warn(`File not found: ${filePath}`);
|
||||
response = new Response("Not Found", { status: 404 });
|
||||
}
|
||||
logger.warn(`File not found: ${filePath}`);
|
||||
return new Response("Not Found", { status: 404 });
|
||||
} catch (error) {
|
||||
logger.error([`Error serving static file: ${pathname}`, error as Error]);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
echo.error({
|
||||
message: `Error serving static file: ${pathname}`,
|
||||
error: error as Error,
|
||||
});
|
||||
response = new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
|
||||
this.logRequest(request, response, ip);
|
||||
return response;
|
||||
}
|
||||
|
||||
private logRequest(
|
||||
request: ExtendedRequest,
|
||||
response: Response,
|
||||
ip: string | undefined,
|
||||
): void {
|
||||
const pathname = new URL(request.url).pathname;
|
||||
|
||||
const ignoredStartsWith: string[] = ["/public"];
|
||||
const ignoredPaths: string[] = ["/favicon.ico"];
|
||||
|
||||
if (
|
||||
ignoredStartsWith.some((prefix) => pathname.startsWith(prefix)) ||
|
||||
ignoredPaths.includes(pathname)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo.custom(`${request.method}`, `${response.status}`, [
|
||||
request.url,
|
||||
`${(performance.now() - request.startPerf).toFixed(2)}ms`,
|
||||
ip || "unknown",
|
||||
]);
|
||||
}
|
||||
|
||||
private async handleRequest(
|
||||
|
@ -95,44 +129,44 @@ class ServerHandler {
|
|||
|
||||
const headers = request.headers;
|
||||
let ip = server.requestIP(request)?.address;
|
||||
let response: Response;
|
||||
|
||||
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
|
||||
ip =
|
||||
headers.get("CF-Connecting-IP")?.trim() ||
|
||||
headers.get("X-Real-IP")?.trim() ||
|
||||
headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
|
||||
headers.get("X-Forwarded-For")?.split(",")[0]?.trim() ||
|
||||
"unknown";
|
||||
}
|
||||
|
||||
const pathname: string = new URL(request.url).pathname;
|
||||
if (pathname === "/robots.txt" && robotstxtPath) {
|
||||
try {
|
||||
const file: BunFile = Bun.file(robotstxtPath);
|
||||
if (await file.exists()) {
|
||||
const fileContent: ArrayBuffer = await file.arrayBuffer();
|
||||
const contentType: string = file.type || "text/plain";
|
||||
return new Response(fileContent, {
|
||||
headers: { "Content-Type": contentType },
|
||||
});
|
||||
}
|
||||
logger.warn(`File not found: ${robotstxtPath}`);
|
||||
return new Response("Not Found", { status: 404 });
|
||||
} catch (error) {
|
||||
logger.error([
|
||||
`Error serving robots.txt: ${robotstxtPath}`,
|
||||
error as Error,
|
||||
]);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
|
||||
const baseDir = resolve("public", "custom");
|
||||
const customPath = resolve(baseDir, pathname.slice(1));
|
||||
|
||||
if (!customPath.startsWith(baseDir)) {
|
||||
response = new Response("Forbidden", { status: 403 });
|
||||
this.logRequest(extendedRequest, response, ip);
|
||||
return response;
|
||||
}
|
||||
|
||||
const customFile = Bun.file(customPath);
|
||||
if (await customFile.exists()) {
|
||||
const content = await customFile.arrayBuffer();
|
||||
const type: string = customFile.type ?? "application/octet-stream";
|
||||
response = new Response(content, {
|
||||
headers: { "Content-Type": type },
|
||||
});
|
||||
this.logRequest(extendedRequest, response, ip);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/public") || pathname === "/favicon.ico") {
|
||||
return await this.serveStaticFile(pathname);
|
||||
return await this.serveStaticFile(extendedRequest, pathname, ip);
|
||||
}
|
||||
|
||||
const match: MatchedRoute | null = this.router.match(request);
|
||||
let requestBody: unknown = {};
|
||||
let response: Response;
|
||||
|
||||
if (match) {
|
||||
const { filePath, params, query } = match;
|
||||
|
@ -141,7 +175,7 @@ class ServerHandler {
|
|||
const routeModule: RouteModule = await import(filePath);
|
||||
const contentType: string | null = request.headers.get("Content-Type");
|
||||
const actualContentType: string | null = contentType
|
||||
? contentType.split(";")[0].trim()
|
||||
? (contentType.split(";")[0]?.trim() ?? null)
|
||||
: null;
|
||||
|
||||
if (
|
||||
|
@ -230,7 +264,10 @@ class ServerHandler {
|
|||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
logger.error([`Error handling route ${request.url}:`, error as Error]);
|
||||
echo.error({
|
||||
message: `Error handling route ${request.url}`,
|
||||
error: error,
|
||||
});
|
||||
|
||||
response = Response.json(
|
||||
{
|
||||
|
@ -252,20 +289,11 @@ class ServerHandler {
|
|||
);
|
||||
}
|
||||
|
||||
logger.custom(
|
||||
`[${request.method}]`,
|
||||
`(${response.status})`,
|
||||
[
|
||||
request.url,
|
||||
`${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`,
|
||||
ip || "unknown",
|
||||
],
|
||||
"90",
|
||||
);
|
||||
|
||||
this.logRequest(extendedRequest, response, ip);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
const serverHandler: ServerHandler = new ServerHandler(
|
||||
environment.port,
|
||||
environment.host,
|
||||
|
|
|
@ -1,30 +1,29 @@
|
|||
import { logger } from "@creations.works/logger";
|
||||
import { echo } from "@atums/echo";
|
||||
import type { ServerWebSocket } from "bun";
|
||||
|
||||
class WebSocketHandler {
|
||||
public handleMessage(ws: ServerWebSocket, message: string): void {
|
||||
logger.info(`WebSocket received: ${message}`);
|
||||
echo.info(`WebSocket received: ${message}`);
|
||||
try {
|
||||
ws.send(`You said: ${message}`);
|
||||
} catch (error) {
|
||||
logger.error(["WebSocket send error", error as Error]);
|
||||
echo.error({ message: "WebSocket send error", error });
|
||||
}
|
||||
}
|
||||
|
||||
public handleOpen(ws: ServerWebSocket): void {
|
||||
logger.info("WebSocket connection opened.");
|
||||
echo.info("WebSocket connection opened.");
|
||||
try {
|
||||
ws.send("Welcome to the WebSocket server!");
|
||||
} catch (error) {
|
||||
logger.error(["WebSocket send error", error as Error]);
|
||||
echo.error({ message: "WebSocket send error", error });
|
||||
}
|
||||
}
|
||||
|
||||
public handleClose(ws: ServerWebSocket, code: number, reason: string): void {
|
||||
logger.warn(`WebSocket closed with code ${code}, reason: ${reason}`);
|
||||
public handleClose(_ws: ServerWebSocket, code: number, reason: string): void {
|
||||
echo.warn(`WebSocket closed with code ${code}, reason: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
const webSocketHandler: WebSocketHandler = new WebSocketHandler();
|
||||
|
||||
export { webSocketHandler };
|
||||
export { webSocketHandler, WebSocketHandler };
|
||||
|
|
|
@ -2,32 +2,38 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@*": ["src/*"],
|
||||
"@config/*": ["config/*"],
|
||||
"@types/*": ["types/*"],
|
||||
"@helpers/*": ["src/helpers/*"]
|
||||
},
|
||||
"typeRoots": ["./src/types", "./node_modules/@types"],
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"typeRoots": [
|
||||
"./types",
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
// Bundler mode
|
||||
"allowJs": false,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
"include": ["src", "types", "config"]
|
||||
"include": [
|
||||
"src",
|
||||
"types"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue