mayhaps functioning TS conversion

This commit is contained in:
Ulysia 2024-12-27 21:17:59 +01:00 committed by Heli-o
parent e9d2c9a6cf
commit 97ac5bf9f1
15 changed files with 467 additions and 242 deletions

View file

@ -5,3 +5,4 @@ README*
node_modules/ node_modules/
.* .*
pnpm-lock* pnpm-lock*
dist

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules node_modules
.env .env
pnpm-lock* pnpm-lock*
dist

View file

@ -1,8 +1,9 @@
{ {
"trailingComma": "es5", "trailingComma": "none",
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"parser": "babel-flow", "parser": "babel-ts",
"bracketSameLine": true "bracketSameLine": true,
"printWidth": 120
} }

View file

@ -1,18 +1,21 @@
const { default: axios } = require('axios'); import axios from 'axios';
const { import { getParsed, getProfit, splitNumber, getRawCraft } from './src/helperFunctions';
getParsed, import { parentPort, workerData, isMainThread } from 'worker_threads';
getProfit, import { Item } from './src/Item';
splitNumber,
getRawCraft, import { loadConfig } from './src/configLoader';
} = require('./src/helperFunctions'); const config = loadConfig();
const { parentPort, workerData } = require('worker_threads');
const config = require('./config.json'); if (isMainThread || !parentPort) {
throw new Error('This module can only be run in a Worker thread.');
}
const threadsToUse: number = config.data['threadsToUse/speed'];
let minProfit = config.data.minSnipeProfit; let minProfit = config.data.minSnipeProfit;
let minPercentProfit = config.data.minSnipePP; let minPercentProfit = config.data.minSnipePP;
let ignoredAuctions = []; let ignoredAuctions: any[] = [];
const { Item } = require('./src/Item'); const promises: Promise<any>[] = [];
const threadsToUse = require('./config.json').data['threadsToUse/speed'];
const promises = [];
console.log(`[Worker ${workerData.workerNumber}] Worker started`); console.log(`[Worker ${workerData.workerNumber}] Worker started`);
@ -94,7 +97,7 @@ async function parsePage(i) {
) { ) {
if (lbin + rcCost - startingBid > minProfit) { if (lbin + rcCost - startingBid > minProfit) {
const profitData = getProfit(startingBid, rcCost, lbin); const profitData = getProfit(startingBid, rcCost, lbin);
let auctionType = null; let auctionType: string | null = null;
if ( if (
rcCost > lbin - startingBid && rcCost > lbin - startingBid &&
profitData.snipeProfit < minProfit profitData.snipeProfit < minProfit
@ -118,7 +121,7 @@ async function parsePage(i) {
) { ) {
prettyItem.auctionData.profit = profitData.RCProfit; prettyItem.auctionData.profit = profitData.RCProfit;
prettyItem.auctionData.percentProfit = profitData.RCPP; prettyItem.auctionData.percentProfit = profitData.RCPP;
parentPort.postMessage(prettyItem); parentPort!.postMessage(prettyItem);
} }
} else { } else {
if ( if (
@ -127,7 +130,7 @@ async function parsePage(i) {
) { ) {
prettyItem.auctionData.profit = profitData.snipeProfit; prettyItem.auctionData.profit = profitData.snipeProfit;
prettyItem.auctionData.percentProfit = profitData.snipePP; prettyItem.auctionData.percentProfit = profitData.snipePP;
parentPort.postMessage(prettyItem); parentPort!.postMessage(prettyItem);
} }
} }
} }
@ -156,8 +159,7 @@ async function doTask(totalPages) {
}); });
} }
let pageToStop = let pageToStop = startingPage + pagePerThread[workerData.workerNumber];
parseInt(startingPage) + parseInt(pagePerThread[workerData.workerNumber]);
if (pageToStop !== totalPages) { if (pageToStop !== totalPages) {
pageToStop -= 1; pageToStop -= 1;
@ -172,5 +174,5 @@ async function doTask(totalPages) {
} }
await Promise.all(promises); await Promise.all(promises);
console.log(`[Worker ${workerData.workerNumber}] Finished task`); console.log(`[Worker ${workerData.workerNumber}] Finished task`);
parentPort.postMessage('finished'); parentPort!.postMessage('finished');
} }

View file

@ -6,4 +6,6 @@ WORKDIR /app
RUN npm install RUN npm install
CMD ["node", "index.js"] RUN npm run build
CMD npm start

View file

@ -1,12 +1,14 @@
--- ---
services: services:
hypixel-auc-notifier: bot:
container_name: hypixel-auc-notifier container_name: hypixel-auc-notifier
build: . build: .
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env - .env
environment:
NODE_ENV: "production"
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- ./config.json:/app/config.json - ./config.json:/app/dist/src/config.json

10
env.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
WEBHOOK_URL: string;
WEBHOOK_NAME: string;
WEBHOOK_PROFILE_PICTURE: string;
}
}
}
export { };

View file

@ -1,9 +1,12 @@
const { default: axios } = require('axios'); import axios from 'axios';
const config = require('./config.json'); import { WebhookClient, EmbedBuilder, Embed } from 'discord.js';
const { WebhookClient, EmbedBuilder, Embed } = require('discord.js'); import { Worker } from 'worker_threads';
const { Worker } = require('worker_threads'); import { asyncInterval, addNotation } from './src/helperFunctions';
const { asyncInterval, addNotation } = require('./src/helperFunctions'); import { string } from 'prismarine-nbt';
const { string } = require('prismarine-nbt');
import { loadConfig } from './src/configLoader';
const config = loadConfig();
let threadsToUse = config.data['threadsToUse/speed'] ?? 1; let threadsToUse = config.data['threadsToUse/speed'] ?? 1;
let lastUpdated = 0; let lastUpdated = 0;
@ -11,7 +14,7 @@ let doneWorkers = 0;
let startingTime; let startingTime;
let maxPrice = 0; let maxPrice = 0;
let itemDatas = {}; let itemDatas = {};
const workers = []; const workers: Worker[] = [];
const webhookRegex = /https:\/\/discord.com\/api\/webhooks\/(.+)\/(.+)/; const webhookRegex = /https:\/\/discord.com\/api\/webhooks\/(.+)\/(.+)/;
const bazaarPrice = { const bazaarPrice = {
@ -30,7 +33,7 @@ async function initialize() {
await getLBINs(); await getLBINs();
for (let j = 0; j < threadsToUse; j++) { for (let j = 0; j < threadsToUse; j++) {
workers[j] = new Worker('./AuctionHandler.js', { workers[j] = new Worker('./AuctionHandler.worker.ts', {
workerData: { workerData: {
itemDatas: itemDatas, itemDatas: itemDatas,
bazaarData: bazaarPrice, bazaarData: bazaarPrice,
@ -44,7 +47,7 @@ async function initialize() {
let averagePrice = itemDatas[result.itemData.id]?.cleanPrice || 'N/A'; let averagePrice = itemDatas[result.itemData.id]?.cleanPrice || 'N/A';
if ( if (
result.auctionData.lbin - result.auctionData.price >= result.auctionData.lbin - result.auctionData.price >=
config.data.minSnipeProfit && config.data.minSnipeProfit &&
averagePrice - result.auctionData.price >= config.data.minAvgProfit averagePrice - result.auctionData.price >= config.data.minAvgProfit
) { ) {
let mustBuyMessage = ''; let mustBuyMessage = '';
@ -167,8 +170,8 @@ async function getMoulberry() {
cleanPriceData[item] !== undefined cleanPriceData[item] !== undefined
? Math.round(cleanPriceData[item]) ? Math.round(cleanPriceData[item])
: itemInfo.clean_price !== undefined : itemInfo.clean_price !== undefined
? itemInfo.clean_price ? itemInfo.clean_price
: itemInfo.price; : itemInfo.price;
} }
} }

View file

@ -1,22 +1,27 @@
{ {
"name": "hypixel-auction-flipper", "name": "hypixel-auction-flipper",
"version": "0.6.9", "version": "1.0.0",
"dependencies": {
"axios": "^0.24.0",
"copy-paste": "^1.3.0",
"discord.js": "^14.16.3",
"express": "^4.17.1",
"node-notifier": "^10.0.0",
"open": "^8.4.0",
"prismarine-nbt": "^2.0.0",
"socket.io": "^4.4.0",
"toastify-js": "^1.11.2"
},
"description": "Hypixel Skyblock Auction House Flip Notifier", "description": "Hypixel Skyblock Auction House Flip Notifier",
"main": "index.js", "main": "dist/index.js",
"scripts": { "author": "DuckySoLucky + MashClashXD + Ulysia + Sol",
"start": "node ."
},
"keywords": [], "keywords": [],
"author": "DuckySoLucky + MashClashXD" "scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node index.ts"
},
"dependencies": {
"@types/toastify-js": "^1.12.3",
"axios": "^0.24.0",
"copy-paste": "^1.5.3",
"discord.js": "^14.16.3",
"express": "^4.21.2",
"node-notifier": "^10.0.1",
"open": "^8.4.2",
"prismarine-nbt": "^2.7.0",
"socket.io": "^4.8.1",
"toastify-js": "^1.12.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
}
} }

View file

@ -1,28 +0,0 @@
function Item(name, auctionID, price, rarity, enchants, hpbs, fpbs, recomb, artofwar, stars, gemstones, id, category, profit, percentProfit, lbin, sales, lore) {
this.itemData = {
"name": name,
"id": id,
"stars": stars,
"rarity": rarity,
"recomb": recomb,
"enchants": enchants,
"hpbs": hpbs,
"fpbs": fpbs,
"gemstones": gemstones,
"aow": artofwar,
"lore": lore
}
this.auctionData = {
"auctionID": auctionID,
"category": category,
"sales": sales,
"price": price,
"profit": profit,
"percentProfit": percentProfit,
"lbin": lbin
}
}
module.exports = {
Item
}

78
src/Item.ts Normal file
View file

@ -0,0 +1,78 @@
// item.ts
export interface ItemData {
name: string;
id: string;
stars: number;
rarity: string;
recomb: boolean;
enchants: string[];
hpbs: number;
fpbs: number;
gemstones: string[];
aow: boolean;
lore: string;
}
export interface AuctionData {
auctionID: string;
category: string;
sales: number;
price: number;
profit: number;
percentProfit: number;
lbin: number;
ahType: string | null;
}
export class Item {
public itemData: ItemData;
public auctionData: AuctionData;
constructor(
name: string,
auctionID: string,
price: number,
rarity: string,
enchants: string[],
hpbs: number,
fpbs: number,
recomb: boolean,
artofwar: boolean,
stars: number,
gemstones: string[],
id: string,
category: string,
profit: number,
percentProfit: number,
lbin: number,
sales: number,
lore: string,
ahType: string | null = null
) {
this.itemData = {
name,
id,
stars,
rarity,
recomb,
enchants,
hpbs,
fpbs,
gemstones,
aow: artofwar,
lore,
};
this.auctionData = {
auctionID,
category,
sales,
price,
profit,
percentProfit,
lbin,
ahType,
};
}
}

41
src/configLoader.ts Normal file
View file

@ -0,0 +1,41 @@
import * as fs from 'fs';
import * as path from 'path';
export interface ConfigType {
data: {
'threadsToUse/speed': number;
minSnipeProfit: number;
minAvgProfit: number;
minCraftProfit: number;
maxAvgLbinDiff: number;
rawCraftMaxWeightPP: number;
minSnipePP: number;
minCraftPP: number;
ignoreCategories: {
weapon: boolean;
accessories: boolean;
armor: boolean;
misc: boolean;
blocks: boolean;
consumables: boolean;
}
minSales: number;
includeCraftCost: boolean;
minPriceForRawcraft: number;
};
filters: {
EnchantThresholdConditionalBypass: Record<string, Record<string, number>>;
EnchantThreshold: Record<string, number>;
itemIDExclusions: string[];
};
}
export function loadConfig(): ConfigType {
const configPath = path.join(__dirname, 'config.json');
try {
const fileContents = fs.readFileSync(configPath, 'utf8');
return JSON.parse(fileContents) as ConfigType;
} catch (error) {
throw new Error(`Failed to load config.json at ${configPath}: ${error}`);
}
}

View file

@ -1,159 +0,0 @@
const config = require('../config.json')
const nbt = require('prismarine-nbt')
let currentAsyncIntervals = {}
function addNotation(type, value) {
let returnVal = value;
let notList = [];
if (type === "shortScale") {
notList = [
" Thousand",
" Million",
" Billion",
" Trillion",
" Quadrillion",
" Quintillion"
];
}
if (type === "oneLetters") {
notList = ["K", "M", "B", "T"];
}
let checkNum = 1000;
if (type !== "none" && type !== "commas") {
let notValue = notList[notList.length - 1];
for (let u = notList.length; u >= 1; u--) {
notValue = notList.shift();
for (let o = 3; o >= 1; o--) {
if (value >= checkNum) {
returnVal = value / (checkNum / 100);
returnVal = Math.floor(returnVal);
returnVal = (returnVal / Math.pow(10, o)) * 10;
returnVal = +returnVal.toFixed(o - 1) + notValue;
}
checkNum *= 10;
}
}
} else {
returnVal = numberWithCommas(value.toFixed(0));
}
return returnVal;
}
async function getParsed(encoded) {
return new Promise((resolve) => {
let buf = Buffer.from(encoded, 'base64');
nbt.parse(buf, (err, dat) => {
if (err) throw err;
resolve(nbt.simplify(dat))
});
})
}
function getProfit(price, rcCost, lbin) {
const profitItem = {}
if (price >= 1000000) {
profitItem.RCProfit = ((lbin + rcCost) - price)
- ((lbin + rcCost) * 0.02);
profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1))
profitItem.snipeProfit = (lbin - price) - (lbin * 0.02)
profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1))
} else {
profitItem.RCProfit = ((lbin + rcCost) - price)
- ((lbin + rcCost) * 0.01);
profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1))
profitItem.snipeProfit = (lbin - price) - (lbin * 0.01)
profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1))
}
return profitItem
}
function splitNumber (num = 1, parts = 1) {
let n = Math.floor(num / parts);
const arr = [];
for (let i = 0; i < parts; i++){
arr.push(n)
}
if(arr.reduce((a, b)=> a + b,0) === num){
return arr;
}
for(let i = 0; i < parts; i++){
arr[i]++;
if(arr.reduce((a, b) => a + b, 0) === num){
return arr;
}
}
}
function getRawCraft(item, bazaarPrice, lbins) {
let price = 0
const ignoreMatch = Object.keys(
config.filters.EnchantThresholdConditionalBypass
).find((key) => {
if (item.itemData.id.includes(key)) return true;
});
if (item.auctionData.lbin < config.data.minPriceForRawcraft) return 0
let isInIgnore = ignoreMatch ? ignoreMatch : false
if (item.itemData.enchants && !item.itemData.id.includes(';')) {
for (const enchant of Object.keys(item.itemData.enchants)) {
const degree = item.itemData.enchants[enchant]
const badEnchant =
typeof config.filters.EnchantThreshold[enchant] === 'number'
? degree >= config.filters.EnchantThreshold[enchant]
: false;
if (isInIgnore) {
const enchantMinValue =
config.filters.EnchantThresholdConditionalBypass[ignoreMatch][
enchant
];
if (enchantMinValue >= degree) continue
}
if (badEnchant) {
price += lbins[`${enchant.toUpperCase()};${degree.toString()}`] ? lbins[`${enchant.toUpperCase()};${degree.toString()}`].lbin * 0.5 : 0
}
}
}
if (item.itemData.aow) {
price += lbins['THE_ART_OF_WAR'] * 0.3
}
if (item.itemData.recomb && (item.auctionData.category === 'weapon' || item.auctionData.category === 'armor' || item.auctionData.category === 'accessories')) {
price += bazaarPrice['RECOMBOBULATOR_3000'] * 0.5
}
price += (item.itemData.hpbs ? item.itemData.hpbs : 0) * bazaarPrice['HOT_POTATO_BOOK'] * 0.05
price += (item.itemData.fpbs ? item.itemData.fpbs : 0) * bazaarPrice['FUMING_POTATO_BOOK'] * 0.1
return price
}
async function asyncInterval(asyncTask, intervalname, timeout) {
currentAsyncIntervals[intervalname] = true
setTimeout(async function () {
if (!currentAsyncIntervals[intervalname]) return
asyncTask().then(async function () {
await asyncInterval(asyncTask, intervalname, timeout)
})
}, timeout)
}
function stopAsyncInterval(intervalname) {
currentAsyncIntervals[intervalname] = false
}
function currentIntervals() {
return currentAsyncIntervals
}
module.exports = {
addNotation,
getParsed,
getProfit,
splitNumber,
getRawCraft,
asyncInterval,
stopAsyncInterval,
currentIntervals
}

247
src/helperFunctions.ts Normal file
View file

@ -0,0 +1,247 @@
// utils.ts
import nbt from 'prismarine-nbt';
import { Item } from './Item';
import { loadConfig } from './configLoader';
const config = loadConfig();
// If you have a defined structure for your config file, feel free to refine this type.
interface ConfigType {
data: {
minPriceForRawcraft: number;
[key: string]: any;
};
filters: {
EnchantThreshold: Record<string, number>;
EnchantThresholdConditionalBypass: Record<string, Record<string, number>>;
[key: string]: any;
};
}
// Prismarine-nbt parses callback has this shape:
interface NbtData {
// If you know the exact shape of the parsed data, define it here
[key: string]: any;
}
// Keep track of current intervals by name
let currentAsyncIntervals: Record<string, boolean> = {};
type ProfitItem = {
RCProfit: number;
RCPP: number;
snipeProfit: number;
snipePP: number;
};
function numberWithCommas(x: number | string): string {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function addNotation(type: string, value: number): string {
let returnVal: string = value.toString();
let notList: string[] = [];
if (type === 'shortScale') {
notList = [' Thousand', ' Million', ' Billion', ' Trillion', ' Quadrillion', ' Quintillion'];
} else if (type === 'oneLetters') {
notList = ['K', 'M', 'B', 'T'];
}
let checkNum = 1000;
if (type !== 'none' && type !== 'commas') {
let notValue = notList[notList.length - 1] ?? '';
for (let u = notList.length; u >= 1; u--) {
notValue = notList.shift() ?? '';
for (let o = 3; o >= 1; o--) {
if (value >= checkNum) {
// Use a fixed decimal approach
returnVal = (
+(Math.floor(value / (checkNum / 100)) / Math.pow(10, o)) * 10
).toFixed(o - 1) + notValue;
}
checkNum *= 10;
}
}
} else {
returnVal = numberWithCommas(value.toFixed(0));
}
return returnVal;
}
async function getParsed(encoded: string): Promise<any> {
return new Promise((resolve, reject) => {
const buf = Buffer.from(encoded, 'base64');
// The callback signature must match the prismarine-nbt types:
nbt.parse(buf, (err: Error | null, data) => {
if (err) return reject(err);
resolve(nbt.simplify(data));
});
});
}
function getProfit(price: number, rcCost: number, lbin: number): ProfitItem {
const profitItem: ProfitItem = {
RCProfit: 0,
RCPP: 0,
snipeProfit: 0,
snipePP: 0,
};
// Auction house fee logic
if (price >= 1_000_000) {
profitItem.RCProfit = lbin + rcCost - price - (lbin + rcCost) * 0.02;
profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1));
profitItem.snipeProfit = lbin - price - lbin * 0.02;
profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1));
} else {
profitItem.RCProfit = lbin + rcCost - price - (lbin + rcCost) * 0.01;
profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1));
profitItem.snipeProfit = lbin - price - lbin * 0.01;
profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1));
}
return profitItem;
}
function splitNumber(num: number = 1, parts: number = 1): number[] {
const n: number = Math.floor(num / parts);
const arr: number[] = [];
for (let i = 0; i < parts; i++) {
arr.push(n);
}
if (arr.reduce((a, b) => a + b, 0) === num) {
return arr;
}
for (let i = 0; i < parts; i++) {
arr[i]++;
if (arr.reduce((a, b) => a + b, 0) === num) {
return arr;
}
}
// If we still havent balanced it out, itll just return this array
return arr;
}
// Adjust the below types as needed to match your item structure:
interface ItemType {
itemData: {
id: string;
enchants?: Record<string, number>;
aow?: boolean;
recomb?: boolean;
hpbs?: number;
fpbs?: number;
};
auctionData: {
lbin: number;
category?: string;
};
[key: string]: any;
}
interface LbinsType {
[key: string]: {
lbin: number;
};
}
interface BazaarPriceType {
[key: string]: number;
}
function getRawCraft(
item: Item,
bazaarPrice: BazaarPriceType,
lbins: LbinsType
): number {
let price = 0;
// If you have a more specific type for config, use that
const typedConfig = config as ConfigType;
const ignoreMatch: string =
Object.keys(typedConfig.filters.EnchantThresholdConditionalBypass ?? {}).find((key) => {
return item.itemData.id.includes(key);
}) ?? '';
if (item.auctionData.lbin < typedConfig.data.minPriceForRawcraft) return 0;
const isInIgnore = !!ignoreMatch;
if (item.itemData.enchants && !item.itemData.id.includes(';')) {
for (const enchant of Object.keys(item.itemData.enchants)) {
const degree = item.itemData.enchants[enchant];
const badEnchant =
typeof typedConfig.filters.EnchantThreshold[enchant] === 'number'
? degree >= typedConfig.filters.EnchantThreshold[enchant]
: false;
if (isInIgnore) {
const enchantMinValue =
typedConfig.filters.EnchantThresholdConditionalBypass[ignoreMatch]?.[enchant];
// If enchantMinValue is defined and the degree is within that threshold, skip.
if (enchantMinValue && degree <= enchantMinValue) continue;
}
if (badEnchant) {
price += lbins[`${enchant.toUpperCase()};${degree.toString()}`]
? lbins[`${enchant.toUpperCase()};${degree.toString()}`].lbin * 0.5
: 0;
}
}
}
if (item.itemData.aow) {
price += lbins['THE_ART_OF_WAR'].lbin * 0.3;
}
if (
item.itemData.recomb &&
['weapon', 'armor', 'accessories'].includes(item.auctionData.category ?? '')
) {
price += bazaarPrice['RECOMBOBULATOR_3000'] * 0.5;
}
price += (item.itemData.hpbs ?? 0) * bazaarPrice['HOT_POTATO_BOOK'] * 0.05;
price += (item.itemData.fpbs ?? 0) * bazaarPrice['FUMING_POTATO_BOOK'] * 0.1;
return price;
}
// Schedules a recurring async task.
async function asyncInterval(
asyncTask: () => Promise<void>,
intervalName: string,
timeout: number
): Promise<void> {
currentAsyncIntervals[intervalName] = true;
setTimeout(async function () {
if (!currentAsyncIntervals[intervalName]) return;
await asyncTask();
await asyncInterval(asyncTask, intervalName, timeout);
}, timeout);
}
function stopAsyncInterval(intervalName: string): void {
currentAsyncIntervals[intervalName] = false;
}
function currentIntervals(): Record<string, boolean> {
return currentAsyncIntervals;
}
// Exports
export {
addNotation,
getParsed,
getProfit,
splitNumber,
getRawCraft,
asyncInterval,
stopAsyncInterval,
currentIntervals,
};

19
tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "nodenext",
"target": "ES2024",
"jsx": "preserve",
"strictNullChecks": true,
"strictFunctionTypes": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}