diff --git a/.dockerignore b/.dockerignore index d9d2aad..7bd6c38 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,5 @@ Docker* README* node_modules/ .* -pnpm-lock* \ No newline at end of file +pnpm-lock* +dist \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7ad6f33..d1319a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .env -pnpm-lock* \ No newline at end of file +pnpm-lock* +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index b4ecedb..a383926 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,9 @@ { - "trailingComma": "es5", + "trailingComma": "none", "tabWidth": 2, "semi": true, "singleQuote": true, - "parser": "babel-flow", - "bracketSameLine": true + "parser": "babel-ts", + "bracketSameLine": true, + "printWidth": 120 } diff --git a/AuctionHandler.js b/AuctionHandler.worker.ts similarity index 85% rename from AuctionHandler.js rename to AuctionHandler.worker.ts index 089f949..2a90d06 100644 --- a/AuctionHandler.js +++ b/AuctionHandler.worker.ts @@ -1,18 +1,21 @@ -const { default: axios } = require('axios'); -const { - getParsed, - getProfit, - splitNumber, - getRawCraft, -} = require('./src/helperFunctions'); -const { parentPort, workerData } = require('worker_threads'); -const config = require('./config.json'); +import axios from 'axios'; +import { getParsed, getProfit, splitNumber, getRawCraft } from './src/helperFunctions'; +import { parentPort, workerData, isMainThread } from 'worker_threads'; +import { Item } from './src/Item'; + +import { loadConfig } from './src/configLoader'; +const config = loadConfig(); + +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 minPercentProfit = config.data.minSnipePP; -let ignoredAuctions = []; -const { Item } = require('./src/Item'); -const threadsToUse = require('./config.json').data['threadsToUse/speed']; -const promises = []; +let ignoredAuctions: any[] = []; +const promises: Promise[] = []; console.log(`[Worker ${workerData.workerNumber}] Worker started`); @@ -94,7 +97,7 @@ async function parsePage(i) { ) { if (lbin + rcCost - startingBid > minProfit) { const profitData = getProfit(startingBid, rcCost, lbin); - let auctionType = null; + let auctionType: string | null = null; if ( rcCost > lbin - startingBid && profitData.snipeProfit < minProfit @@ -118,7 +121,7 @@ async function parsePage(i) { ) { prettyItem.auctionData.profit = profitData.RCProfit; prettyItem.auctionData.percentProfit = profitData.RCPP; - parentPort.postMessage(prettyItem); + parentPort!.postMessage(prettyItem); } } else { if ( @@ -127,7 +130,7 @@ async function parsePage(i) { ) { prettyItem.auctionData.profit = profitData.snipeProfit; prettyItem.auctionData.percentProfit = profitData.snipePP; - parentPort.postMessage(prettyItem); + parentPort!.postMessage(prettyItem); } } } @@ -156,8 +159,7 @@ async function doTask(totalPages) { }); } - let pageToStop = - parseInt(startingPage) + parseInt(pagePerThread[workerData.workerNumber]); + let pageToStop = startingPage + pagePerThread[workerData.workerNumber]; if (pageToStop !== totalPages) { pageToStop -= 1; @@ -172,5 +174,5 @@ async function doTask(totalPages) { } await Promise.all(promises); console.log(`[Worker ${workerData.workerNumber}] Finished task`); - parentPort.postMessage('finished'); + parentPort!.postMessage('finished'); } diff --git a/Dockerfile b/Dockerfile index c1bbdf0..607e2fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,6 @@ WORKDIR /app RUN npm install -CMD ["node", "index.js"] \ No newline at end of file +RUN npm run build + +CMD npm start \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1c0bfef..b5ffae7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,14 @@ --- services: - hypixel-auc-notifier: + bot: container_name: hypixel-auc-notifier build: . restart: unless-stopped env_file: - .env + environment: + NODE_ENV: "production" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - - ./config.json:/app/config.json \ No newline at end of file + - ./config.json:/app/dist/src/config.json \ No newline at end of file diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..e1914a0 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,10 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + WEBHOOK_URL: string; + WEBHOOK_NAME: string; + WEBHOOK_PROFILE_PICTURE: string; + } + } +} +export { }; diff --git a/index.js b/index.ts similarity index 91% rename from index.js rename to index.ts index e17cc67..fb9faee 100644 --- a/index.js +++ b/index.ts @@ -1,9 +1,12 @@ -const { default: axios } = require('axios'); -const config = require('./config.json'); -const { WebhookClient, EmbedBuilder, Embed } = require('discord.js'); -const { Worker } = require('worker_threads'); -const { asyncInterval, addNotation } = require('./src/helperFunctions'); -const { string } = require('prismarine-nbt'); +import axios from 'axios'; +import { WebhookClient, EmbedBuilder, Embed } from 'discord.js'; +import { Worker } from 'worker_threads'; +import { asyncInterval, addNotation } from './src/helperFunctions'; +import { string } from 'prismarine-nbt'; + +import { loadConfig } from './src/configLoader'; +const config = loadConfig(); + let threadsToUse = config.data['threadsToUse/speed'] ?? 1; let lastUpdated = 0; @@ -11,7 +14,7 @@ let doneWorkers = 0; let startingTime; let maxPrice = 0; let itemDatas = {}; -const workers = []; +const workers: Worker[] = []; const webhookRegex = /https:\/\/discord.com\/api\/webhooks\/(.+)\/(.+)/; const bazaarPrice = { @@ -30,7 +33,7 @@ async function initialize() { await getLBINs(); for (let j = 0; j < threadsToUse; j++) { - workers[j] = new Worker('./AuctionHandler.js', { + workers[j] = new Worker('./AuctionHandler.worker.ts', { workerData: { itemDatas: itemDatas, bazaarData: bazaarPrice, @@ -44,7 +47,7 @@ async function initialize() { let averagePrice = itemDatas[result.itemData.id]?.cleanPrice || 'N/A'; if ( result.auctionData.lbin - result.auctionData.price >= - config.data.minSnipeProfit && + config.data.minSnipeProfit && averagePrice - result.auctionData.price >= config.data.minAvgProfit ) { let mustBuyMessage = ''; @@ -167,8 +170,8 @@ async function getMoulberry() { cleanPriceData[item] !== undefined ? Math.round(cleanPriceData[item]) : itemInfo.clean_price !== undefined - ? itemInfo.clean_price - : itemInfo.price; + ? itemInfo.clean_price + : itemInfo.price; } } diff --git a/package.json b/package.json index 2323c54..7266540 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,27 @@ { "name": "hypixel-auction-flipper", - "version": "0.6.9", - "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" - }, + "version": "1.0.0", "description": "Hypixel Skyblock Auction House Flip Notifier", - "main": "index.js", - "scripts": { - "start": "node ." - }, + "main": "dist/index.js", + "author": "DuckySoLucky + MashClashXD + Ulysia + Sol", "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" + } } diff --git a/src/Item.js b/src/Item.js deleted file mode 100644 index 27b0717..0000000 --- a/src/Item.js +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/src/Item.ts b/src/Item.ts new file mode 100644 index 0000000..ee054a7 --- /dev/null +++ b/src/Item.ts @@ -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, + }; + } +} diff --git a/src/configLoader.ts b/src/configLoader.ts new file mode 100644 index 0000000..c477fde --- /dev/null +++ b/src/configLoader.ts @@ -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>; + EnchantThreshold: Record; + 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}`); + } +} diff --git a/src/helperFunctions.js b/src/helperFunctions.js deleted file mode 100644 index f546094..0000000 --- a/src/helperFunctions.js +++ /dev/null @@ -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 -} diff --git a/src/helperFunctions.ts b/src/helperFunctions.ts new file mode 100644 index 0000000..859c590 --- /dev/null +++ b/src/helperFunctions.ts @@ -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; + EnchantThresholdConditionalBypass: Record>; + [key: string]: any; + }; +} + +// Prismarine-nbt parse’s 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 = {}; + +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 { + 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 haven’t balanced it out, it’ll just return this array + return arr; +} + +// Adjust the below types as needed to match your item structure: +interface ItemType { + itemData: { + id: string; + enchants?: Record; + 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, + intervalName: string, + timeout: number +): Promise { + 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 { + return currentAsyncIntervals; +} + +// Exports +export { + addNotation, + getParsed, + getProfit, + splitNumber, + getRawCraft, + asyncInterval, + stopAsyncInterval, + currentIntervals, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5b3c33d --- /dev/null +++ b/tsconfig.json @@ -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/*" + ] +} \ No newline at end of file