This commit is contained in:
BaekRyang 2026-04-02 09:00:10 +09:00
commit a3a13e408a
26 changed files with 10871 additions and 0 deletions

33
.eslintrc.js Normal file
View File

@ -0,0 +1,33 @@
module.exports = {
root: true,
env: {
es6: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"google",
"plugin:@typescript-eslint/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: ["tsconfig.json", "tsconfig.dev.json"],
sourceType: "module",
},
ignorePatterns: [
"/lib/**/*", // Ignore built files.
"/generated/**/*", // Ignore generated files.
],
plugins: [
"@typescript-eslint",
"import",
],
rules: {
"quotes": ["error", "double"],
"import/no-unresolved": 0,
"indent": ["error", 2],
},
};

7
.firebaserc Normal file
View File

@ -0,0 +1,7 @@
{
"projects": {
"default": "mmday-7900f"
},
"targets": {},
"etags": {}
}

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Compiled JavaScript files
lib/**/*.js
lib/**/*.js.map
# TypeScript v1 declaration files
typings/
# Node.js dependency directory
node_modules/
*.local

10
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,10 @@
# 디폴트 무시된 파일
/shelf/
/workspace.xml
# 에디터 기반 HTTP 클라이언트 요청
/httpRequests/
# 쿼리 파일을 포함한 무시된 디폴트 폴더
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/discord.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

View File

@ -0,0 +1,29 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="customHeaders">
<set>
<option value="&quot;name&quot;" />
</set>
</option>
</inspection_tool>
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="uvloop" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N803" />
</list>
</option>
</inspection_tool>
</profile>
</component>

8
.idea/mmday-firebase.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mmday-firebase.iml" filepath="$PROJECT_DIR$/.idea/mmday-firebase.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

6
database.rules.json Normal file
View File

@ -0,0 +1,6 @@
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}

1
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export declare const helloWorld: import("firebase-functions/v2/https").HttpsFunction;

10
dist/index.js vendored Normal file
View File

@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.helloWorld = void 0;
const app_1 = require("firebase-admin/app");
const https_1 = require("firebase-functions/v2/https");
(0, app_1.initializeApp)();
exports.helloWorld = (0, https_1.onRequest)((req, res) => {
res.json({ message: "Hello from mmday!" });
});
//# sourceMappingURL=index.js.map

1
dist/index.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,4CAAmD;AACnD,uDAAwD;AAExD,IAAA,mBAAa,GAAE,CAAC;AAEH,QAAA,UAAU,GAAG,IAAA,iBAAS,EAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC/C,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,CAAC;AAC7C,CAAC,CAAC,CAAC"}

48
firebase.json Normal file
View File

@ -0,0 +1,48 @@
{
"database": {
"rules": "y"
},
"firestore": {
"database": "(default)",
"location": "asia-northeast3",
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"functions": [
{
"codebase": "default",
"source": ".",
"runtime": "nodejs22",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log",
"*.local"
],
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
]
}
],
"hosting": {
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
},
"storage": {
"rules": "storage.rules"
},
"auth": {
"providers": {}
}
}

4
firestore.indexes.json Normal file
View File

@ -0,0 +1,4 @@
{
"indexes": [],
"fieldOverrides": []
}

19
firestore.rules Normal file
View File

@ -0,0 +1,19 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// This rule allows anyone with your Firestore database reference to view, edit,
// and delete all data in your Firestore database. It is useful for getting
// started, but it is configured to expire after 30 days because it
// leaves your app open to attackers. At that time, all client
// requests to your Firestore database will be denied.
//
// Make sure to write security rules for your app before that time, or else
// all client requests to your Firestore database will be denied until you Update
// your rules
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 5, 2);
}
}
}

10146
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "functions",
"scripts": {
"lint": "eslint --ext .js,.ts .",
"build": "tsc",
"build:watch": "tsc --watch",
"serve": "npm run build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "24"
},
"main": "lib/index.js",
"dependencies": {
"firebase-admin": "^13.6.0",
"firebase-functions": "^7.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"eslint": "^8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"firebase-functions-test": "^3.4.1",
"typescript": "^5.7.3"
},
"private": true
}

89
public/index.html Normal file
View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome to Firebase Hosting</title>
<!-- update the version number as needed -->
<script defer src="/__/firebase/12.11.0/firebase-app-compat.js"></script>
<!-- include only the Firebase features as you need -->
<script defer src="/__/firebase/12.11.0/firebase-auth-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-database-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-firestore-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-functions-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-messaging-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-storage-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-analytics-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-remote-config-compat.js"></script>
<script defer src="/__/firebase/12.11.0/firebase-performance-compat.js"></script>
<!--
initialize the SDK after all desired features are loaded, set useEmulator to false
to avoid connecting the SDK to running emulators.
-->
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
<style media="screen">
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px; border-radius: 3px; }
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
@media (max-width: 600px) {
body, #message { margin-top: 0; background: white; box-shadow: none; }
body { border-top: 16px solid #ffa100; }
}
</style>
</head>
<body>
<div id="message">
<h2>Welcome</h2>
<h1>Firebase Hosting Setup Complete</h1>
<p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
<a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
</div>
<p id="load">Firebase SDK Loading&hellip;</p>
<script>
document.addEventListener('DOMContentLoaded', function() {
const loadEl = document.querySelector('#load');
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// // The Firebase SDK is initialized and available here!
//
// firebase.auth().onAuthStateChanged(user => { });
// firebase.database().ref('/path/to/ref').on('value', snapshot => { });
// firebase.firestore().doc('/foo/bar').get().then(() => { });
// firebase.functions().httpsCallable('yourFunction')().then(() => { });
// firebase.messaging().requestPermission().then(() => { });
// firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
// firebase.analytics(); // call to activate
// firebase.analytics().logEvent('tutorial_completed');
// firebase.performance(); // call to activate
//
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
try {
let app = firebase.app();
let features = [
'auth',
'database',
'firestore',
'functions',
'messaging',
'storage',
'analytics',
'remoteConfig',
'performance',
].filter(feature => typeof app[feature] === 'function');
loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`;
} catch (e) {
console.error(e);
loadEl.textContent = 'Error loading the Firebase SDK, check the console.';
}
});
</script>
</body>
</html>

32
src/index.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* Import function triggers from their respective submodules:
*
* import {onCall} from "firebase-functions/v2/https";
* import {onDocumentWritten} from "firebase-functions/v2/firestore";
*
* See a full list of supported triggers at https://firebase.google.com/docs/functions
*/
import {setGlobalOptions} from "firebase-functions";
import {onRequest} from "firebase-functions/https";
import * as logger from "firebase-functions/logger";
// Start writing functions
// https://firebase.google.com/docs/functions/typescript
// For cost control, you can set the maximum number of containers that can be
// running at the same time. This helps mitigate the impact of unexpected
// traffic spikes by instead downgrading performance. This limit is a
// per-function limit. You can override the limit for each function using the
// `maxInstances` option in the function's options, e.g.
// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`.
// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1
// functions should each use functions.runWith({ maxInstances: 10 }) instead.
// In the v1 API, each function can only serve one request per container, so
// this will be the maximum concurrent request count.
setGlobalOptions({ maxInstances: 10 });
// export const helloWorld = onRequest((request, response) => {
// logger.info("Hello logs!", {structuredData: true});
// response.send("Hello from Firebase!");
// });

96
src/kbo/aspnet-client.ts Normal file
View File

@ -0,0 +1,96 @@
import { extractHiddenField } from "./html-utils.js";
export const PREFIX =
"ctl00$ctl00$ctl00$cphContents$cphContents$cphContents$";
export interface PageState {
viewState: string;
viewStateGenerator: string;
eventValidation: string;
}
export interface PostbackRequest {
url: string;
eventTarget: string;
scriptManager: string;
formFields: Record<string, string>;
state: PageState;
}
export interface PostbackResponse {
html: string;
newState: PageState;
}
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/147.0.0.0 Safari/537.36";
export async function getInitialPageState(
url: string
): Promise<{ html: string; state: PageState }> {
const res = await fetch(url, {
headers: { "User-Agent": USER_AGENT },
});
const html = await res.text();
return {
html,
state: {
viewState: extractHiddenField(html, "__VIEWSTATE"),
viewStateGenerator: extractHiddenField(html, "__VIEWSTATEGENERATOR"),
eventValidation: extractHiddenField(html, "__EVENTVALIDATION"),
},
};
}
export async function postback(
request: PostbackRequest
): Promise<PostbackResponse> {
const formData = new URLSearchParams();
formData.set(`${PREFIX}ScriptManager`, request.scriptManager);
for (const [key, value] of Object.entries(request.formFields)) {
formData.set(key, value);
}
formData.set("__EVENTTARGET", request.eventTarget);
formData.set("__EVENTARGUMENT", "");
formData.set("__LASTFOCUS", "");
formData.set("__VIEWSTATE", request.state.viewState);
formData.set("__VIEWSTATEGENERATOR", request.state.viewStateGenerator);
formData.set("__EVENTVALIDATION", request.state.eventValidation);
formData.set("__ASYNCPOST", "true");
const res = await fetch(request.url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-MicrosoftAjax": "Delta=true",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": USER_AGENT,
Referer: request.url,
Origin: new URL(request.url).origin,
},
body: formData.toString(),
});
const text = await res.text();
const panelMatch = text.match(
/updatePanel\|[^|]*\|([\s\S]*?)\|[0-9]+\|hiddenField\|/
);
const html = panelMatch?.[1] ?? text;
const vsMatch = text.match(/__VIEWSTATE\|([^|]*)\|/);
const vsgMatch = text.match(/__VIEWSTATEGENERATOR\|([^|]*)\|/);
const evMatch = text.match(/__EVENTVALIDATION\|([^|]*)\|/);
const newState: PageState = {
viewState: vsMatch?.[1] ?? request.state.viewState,
viewStateGenerator: vsgMatch?.[1] ?? request.state.viewStateGenerator,
eventValidation: evMatch?.[1] ?? request.state.eventValidation,
};
return { html, newState };
}

29
src/kbo/html-utils.ts Normal file
View File

@ -0,0 +1,29 @@
export function extractHiddenField(html: string, name: string): string {
const re = new RegExp(`id="${name}"[^>]*value="([^"]*)"`, "i");
const match = html.match(re);
if (match) return match[1];
const re2 = new RegExp(`name="${name}"[^>]*value="([^"]*)"`, "i");
const match2 = html.match(re2);
return match2?.[1] ?? "";
}
export function stripTags(html: string): string {
return html.replace(/<[^>]*>/g, "").trim();
}
export function decodeHtmlEntities(text: string): string {
return text
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
}
export function padCell(val: string, w: number): string {
const displayWidth = [...val].reduce(
(sum, ch) => sum + (ch.charCodeAt(0) > 0x7f ? 2 : 1),
0
);
return val + " ".repeat(Math.max(0, w - displayWidth));
}

210
src/kbo/team-rank.ts Normal file
View File

@ -0,0 +1,210 @@
import { stripTags, decodeHtmlEntities } from "./html-utils.js";
import {
PREFIX,
postback,
getInitialPageState,
type PageState,
type PostbackResponse,
} from "./aspnet-client.js";
const PAGE_URL =
"https://www.koreabaseball.com/Record/TeamRank/TeamRank.aspx";
// ── Types ──
export interface TeamRank {
rank: string;
team: string;
games: string;
wins: string;
losses: string;
draws: string;
winRate: string;
gamesBehind: string;
last10: string;
streak: string;
home: string;
away: string;
}
export type WinLossDraw = [win: number, loss: number, draw: number];
export interface TeamVsRecord {
team: string;
headToHead: Record<string, WinLossDraw>;
total: WinLossDraw;
}
export interface TeamRankResult {
year: number;
teams: TeamRank[];
vsRecords: TeamVsRecord[];
}
// ── Parsing ──
export function parseWLD(s: string): WinLossDraw {
const parts = s.split("-").map(Number);
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
}
export function parseRankTable(html: string): TeamRank[] {
const teams: TeamRank[] = [];
const trRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let trMatch: RegExpExecArray | null;
while ((trMatch = trRegex.exec(html)) !== null) {
const trContent = trMatch[1];
const tds: string[] = [];
const tdRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
let tdMatch: RegExpExecArray | null;
while ((tdMatch = tdRegex.exec(trContent)) !== null) {
tds.push(stripTags(decodeHtmlEntities(tdMatch[1])));
}
if (tds.length >= 10) {
const teamName = tds[1];
if (/^[A-Z가-힣]/.test(teamName)) {
teams.push({
rank: tds[0],
team: tds[1],
games: tds[2],
wins: tds[3],
losses: tds[4],
draws: tds[5],
winRate: tds[6],
gamesBehind: tds[7],
last10: tds[8],
streak: tds[9],
home: tds[10] ?? "",
away: tds[11] ?? "",
});
}
}
}
return teams;
}
export function parseVsTable(html: string): TeamVsRecord[] {
const tableMatch = html.match(
/<table[^>]*summary="팀간승패표"[^>]*>([\s\S]*?)<\/table>/
);
if (!tableMatch) return [];
const tableHtml = tableMatch[1];
const theadMatch = tableHtml.match(/<thead[^>]*>([\s\S]*?)<\/thead>/);
const teamNames: string[] = [];
if (theadMatch) {
const thRegex = /<th[^>]*>([\s\S]*?)<\/th>/gi;
let thMatch: RegExpExecArray | null;
while ((thMatch = thRegex.exec(theadMatch[1])) !== null) {
const text = stripTags(decodeHtmlEntities(thMatch[1]))
.replace(/\(승-패-무\)/g, "")
.trim();
if (text && text !== "팀명" && text !== "합계") {
teamNames.push(text);
}
}
}
const records: TeamVsRecord[] = [];
const tbodyMatch = tableHtml.match(/<tbody[^>]*>([\s\S]*?)<\/tbody>/);
if (!tbodyMatch) return [];
const trRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let trMatch: RegExpExecArray | null;
while ((trMatch = trRegex.exec(tbodyMatch[1])) !== null) {
const cells: string[] = [];
const tdRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
let tdMatch: RegExpExecArray | null;
while ((tdMatch = tdRegex.exec(trMatch[1])) !== null) {
cells.push(stripTags(decodeHtmlEntities(tdMatch[1])));
}
if (cells.length < teamNames.length + 2) continue;
const team = cells[0];
const headToHead: Record<string, WinLossDraw> = {};
for (let i = 0; i < teamNames.length; i++) {
const val = cells[i + 1];
if (val === "■" || val === "▲" || team === teamNames[i]) continue;
headToHead[teamNames[i]] = parseWLD(val);
}
const total = parseWLD(cells[cells.length - 1]);
records.push({ team, headToHead, total });
}
return records;
}
// ── Page-specific helpers ──
export function extractCurrentYear(html: string): number {
const m = html.match(
/id="cphContents_cphContents_cphContents_lblSearchDateTitle">(\d{4})/
);
return m ? parseInt(m[1], 10) : new Date().getFullYear();
}
function buildYearFormFields(
year: number,
currentYear: number
): Record<string, string> {
return {
[`${PREFIX}ddlYear`]: String(year),
[`${PREFIX}ddlSeries`]: "0",
[`${PREFIX}hfSearchYear`]: String(currentYear),
[`${PREFIX}hfSearchDate`]: `${currentYear}1231`,
[`${PREFIX}hfSearchSeries`]: "0",
};
}
// ── Fetch ──
export async function fetchTeamRank(
year: number,
state: PageState,
currentYear: number
): Promise<{ result: TeamRankResult; newState: PageState }> {
const { html, newState }: PostbackResponse = await postback({
url: PAGE_URL,
eventTarget: `${PREFIX}ddlYear`,
scriptManager: `${PREFIX}udpRecord|${PREFIX}ddlYear`,
formFields: buildYearFormFields(year, currentYear),
state,
});
return {
result: {
year,
teams: parseRankTable(html),
vsRecords: parseVsTable(html),
},
newState,
};
}
export async function fetchTeamRankInitial(): Promise<{
result: TeamRankResult;
state: PageState;
currentYear: number;
}> {
const { html, state } = await getInitialPageState(PAGE_URL);
const currentYear = extractCurrentYear(html);
return {
result: {
year: currentYear,
teams: parseRankTable(html),
vsRecords: parseVsTable(html),
},
state,
currentYear,
};
}

9
storage.rules Normal file
View File

@ -0,0 +1,9 @@
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}

5
tsconfig.dev.json Normal file
View File

@ -0,0 +1,5 @@
{
"include": [
".eslintrc.js"
]
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "NodeNext",
"esModuleInterop": true,
"moduleResolution": "nodenext",
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2017"
},
"compileOnSave": true,
"include": [
"src"
]
}