diff --git a/package-lock.json b/package-lock.json index e25b52c..7698889 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,10 @@ "@hono/node-ws": "^1.3.0", "fast-xml-parser": "^5.3.5", "hono": "^4.12.8" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "typescript": "^5.9.3" } }, "node_modules/@hono/node-server": { @@ -43,6 +47,16 @@ "hono": "^4.6.0" } }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/fast-xml-parser": { "version": "5.3.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.5.tgz", @@ -82,6 +96,27 @@ ], "license": "MIT" }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", diff --git a/package.json b/package.json index 9fa1c72..2fe3ec4 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "license": "ISC", "author": "ericek111", "type": "module", - "main": "src/index.js", + "main": "src/index.ts", "scripts": { - "start": "node src/index.js", + "start": "node --experimental-strip-types src/index.ts", + "typecheck": "tsc --noEmit", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { @@ -15,5 +16,9 @@ "@hono/node-ws": "^1.3.0", "fast-xml-parser": "^5.3.5", "hono": "^4.12.8" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "typescript": "^5.9.3" } } diff --git a/src/N1mmServer.js b/src/N1mmServer.js deleted file mode 100644 index cabf5e8..0000000 --- a/src/N1mmServer.js +++ /dev/null @@ -1,50 +0,0 @@ -import dgram from 'node:dgram'; -import { XMLParser } from 'fast-xml-parser'; - -export default class N1mmServer { - - constructor() { - this.xmlParser = new XMLParser(); - this.turnHandlers = []; // TurnMessageHandler[] - } - - start() { - const server = dgram.createSocket('udp4'); - - server.on('error', (err) => { - console.error(`server error:\n${err.stack}`); - server.close(); - }); - server.on('listening', () => { - const address = server.address(); - console.log(`server listening ${address.address}:${address.port}`); - }); - server.on('message', this._onUdpMessage.bind(this)); - server.bind(12040); - } - - _onUdpMessage(msg, rinfo) { - console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`); - this.processN1mmXml(msg); - } - - onTurnMessage(handler) { - this.turnHandlers.push(handler); - } - - processN1mmXml(xml) { - try { - const msg = this.xmlParser.parse(xml); - const band = parseInt(msg?.N1MMRotor?.freqband.split(',')[0] || 0, 10); - const az = parseInt(msg?.N1MMRotor?.goazi.split(',')[0] || 0, 10); - if (!band || !az) { - console.error(`Missing band or azimuth`); - return; - } - - this.turnHandlers.forEach(h => h.handleTurnMessage({ band, az })); - } catch (ex) { - console.error(`Failed to parse: ${xml}`, ex); - } - } -} \ No newline at end of file diff --git a/src/N1mmServer.ts b/src/N1mmServer.ts new file mode 100644 index 0000000..cfbed78 --- /dev/null +++ b/src/N1mmServer.ts @@ -0,0 +1,57 @@ +import dgram from 'node:dgram'; +import { XMLParser } from 'fast-xml-parser'; +import type { TurnMessageHandler } from './types.js'; + +interface N1MMRotorXml { + N1MMRotor?: { + freqband?: string; + goazi?: string; + }; +} + +export default class N1mmServer { + private readonly xmlParser = new XMLParser(); + private readonly turnHandlers: TurnMessageHandler[] = []; + + start(): void { + const server = dgram.createSocket('udp4'); + + server.on('error', err => { + console.error(`server error:\n${err.stack}`); + server.close(); + }); + + server.on('listening', () => { + const { address, port } = server.address(); + console.log(`server listening ${address}:${port}`); + }); + + server.on('message', (msg, rinfo) => { + console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`); + this.processN1mmXml(msg.toString()); + }); + + server.bind(12040); + } + + onTurnMessage(handler: TurnMessageHandler): void { + this.turnHandlers.push(handler); + } + + private processN1mmXml(xml: string): void { + try { + const parsed = this.xmlParser.parse(xml) as N1MMRotorXml; + const band = parseInt(parsed?.N1MMRotor?.freqband?.split(',')[0] ?? '0', 10); + const az = parseInt(parsed?.N1MMRotor?.goazi?.split(',')[0] ?? '0', 10); + + if (!band || !az) { + console.error('Missing band or azimuth'); + return; + } + + this.turnHandlers.forEach(h => h.handleTurnMessage({ band, az })); + } catch (ex) { + console.error(`Failed to parse: ${xml}`, ex); + } + } +} diff --git a/src/RotClient.js b/src/RotClient.js deleted file mode 100644 index 07cf968..0000000 --- a/src/RotClient.js +++ /dev/null @@ -1,124 +0,0 @@ -import {handle} from "@hono/node-server/vercel"; - -/** - * Implementation of a simple client for simple_rotator_interface_v -- https://remoteqth.com/w/doku.php?id=simple_rotator_interface_v - */ -export default class RotClient { - - constructor(label, clientConfig) { - this.label = label; - this.ip = clientConfig.ip; - this.bands = clientConfig.bands; - this.cachedData = null - this.dynamicData = null; - this.dynamicHandlers = []; - - setInterval(async () => { - await this._readDynamicData(); - console.log(`${this.label}: `, this.dynamicData); - }, 1000); - } - - async readInitData() { - if (this.cachedData) - return this.cachedData; - - const endpoints = [ - { key: 'azShift', url: 'readStart', numType: 1 }, - { key: 'azRange', url: 'readMax', numType: 1 }, - { key: 'antRadiationAngle', url: 'readAnt', numType: 1 }, - { key: 'antName', url: 'readAntName', numType: 0 }, - { key: 'mapUrl', url: 'readMapUrl', numType: 0 }, - { key: 'mac', url: 'readMAC', numType: 0 }, - { key: 'elevation', url: 'readElevation', numType: 1 }, - ]; - - this.cachedData = await this._readEndpoints(endpoints); - - console.log(`${this.label} Set the initial offset to ${this.cachedData.azShift}.`); - - return this.cachedData; - } - - async _readEndpoints(endpoints) { - const data = {}; - for (const row of endpoints) { - data[row.key] = await this.readKey(row.url, row.numType); - } - return data; - } - - async _readDynamicData() { - const dynamicEndpoints = [ - { key: 'adc', url: 'readADC', numType: 2 }, - { key: 'azimuth', url: 'readAZ', numType: 1 }, - { key: 'status', url: 'readStat', numType: 1 }, - ]; - - const data = await this._readEndpoints(dynamicEndpoints); - - if (this.dynamicData !== null) { - const oldData = this.dynamicData; - // const changed = {}; // send everything for now - let hasChanged = false; - for (const key in oldData) { - if (oldData[key] !== data[key]) { - // changed[key] = data[key]; - hasChanged = true; - } - } - - if (hasChanged) - this.dynamicHandlers.forEach((handler) => handler(data)); - } - - this.dynamicData = data; - - return data; - } - - onDynamicDataUpdate(handler) { - this.dynamicHandlers.push(handler); - } - - hasBand(band) { - return this.bands.includes(band); - } - - turn(az) { - fetch(`http://${this.ip}:88/`, { - "body": "ROT=" + az, - "method": "POST", - }).catch(() => {}); - console.log(`Turning ${this.label} to ${az}°...`); - } - - async readKey(endpoint, numType) { - try { - const resp = await fetch(`http://${this.ip}:88/${endpoint}`); - const text = await resp.text(); - // TODO: End my suffering. - return numType ? (numType === 1 ? parseInt(text, 10) : parseFloat(text)) : text; - } catch(ex) { - console.error(`${this.label}: Failed to read ${endpoint}:`, ex); - return null; - } - } - - getDynamicData() { - return this.dynamicData; - } - - getAzi() { - return this.dynamicData.azimuth; - } - - getAdc() { - return this.dynamicData.adc; - } - - getStatus() { - return this.dynamicData.status; - } - -} \ No newline at end of file diff --git a/src/RotClient.ts b/src/RotClient.ts new file mode 100644 index 0000000..5af3359 --- /dev/null +++ b/src/RotClient.ts @@ -0,0 +1,130 @@ +import type { ClientConfig, DynamicData, InitData } from './types.js'; + +// Numeric parsing strategy for rotator HTTP endpoints +const NumType = { + String: 0, + Int: 1, + Float: 2, +} as const; +type NumType = (typeof NumType)[keyof typeof NumType]; + +interface EndpointDef { + key: string; + url: string; + numType: NumType; +} + +/** + * Client for the simple_rotator_interface_v protocol. + * @see https://remoteqth.com/w/doku.php?id=simple_rotator_interface_v + */ +export default class RotClient { + readonly label: string; + private readonly ip: string; + private readonly bands: number[]; + private cachedData: InitData | null = null; + private dynamicData: DynamicData | null = null; + private readonly dynamicHandlers: Array<(data: DynamicData) => void> = []; + + constructor(label: string, { ip, bands }: ClientConfig) { + this.label = label; + this.ip = ip; + this.bands = bands; + + setInterval(() => void this.fetchDynamicData(), 1000); + } + + async readInitData(): Promise { + if (this.cachedData) return this.cachedData; + + const endpoints: EndpointDef[] = [ + { key: 'azShift', url: 'readStart', numType: NumType.Int }, + { key: 'azRange', url: 'readMax', numType: NumType.Int }, + { key: 'antRadiationAngle', url: 'readAnt', numType: NumType.Int }, + { key: 'antName', url: 'readAntName', numType: NumType.String }, + { key: 'mapUrl', url: 'readMapUrl', numType: NumType.String }, + { key: 'mac', url: 'readMAC', numType: NumType.String }, + { key: 'elevation', url: 'readElevation', numType: NumType.Int }, + ]; + + this.cachedData = await this.readEndpoints(endpoints) as unknown as InitData; + console.log(`${this.label} Set the initial offset to ${this.cachedData.azShift}.`); + return this.cachedData; + } + + private async readEndpoints(endpoints: EndpointDef[]): Promise> { + const data: Record = {}; + for (const { key, url, numType } of endpoints) { + data[key] = await this.readKey(url, numType); + } + return data; + } + + private async fetchDynamicData(): Promise { + const endpoints: EndpointDef[] = [ + { key: 'adc', url: 'readADC', numType: NumType.Float }, + { key: 'azimuth', url: 'readAZ', numType: NumType.Int }, + { key: 'status', url: 'readStat', numType: NumType.Int }, + ]; + + const data = await this.readEndpoints(endpoints) as unknown as DynamicData; + + if (this.dynamicData !== null) { + const oldData = this.dynamicData; + const hasChanged = (Object.keys(oldData) as Array).some( + key => oldData[key] !== data[key] + ); + if (hasChanged) { + this.dynamicHandlers.forEach(handler => handler(data)); + } + } + + this.dynamicData = data; + console.log(`${this.label}: `, this.dynamicData); + } + + onDynamicDataUpdate(handler: (data: DynamicData) => void): void { + this.dynamicHandlers.push(handler); + } + + hasBand(band: number): boolean { + return this.bands.includes(band); + } + + turn(az: number): void { + fetch(`http://${this.ip}:88/`, { + body: `ROT=${az}`, + method: 'POST', + }).catch(() => {}); + console.log(`Turning ${this.label} to ${az}°...`); + } + + private async readKey(endpoint: string, numType: NumType): Promise { + try { + const resp = await fetch(`http://${this.ip}:88/${endpoint}`); + const text = await resp.text(); + if (numType === NumType.String) return text; + if (numType === NumType.Int) return parseInt(text, 10); + return parseFloat(text); + } catch (ex) { + console.error(`${this.label}: Failed to read ${endpoint}:`, ex); + return null; + } + } + + getDynamicData(): DynamicData | null { + return this.dynamicData; + } + + getAzimuth(): number | null { + return this.dynamicData?.azimuth ?? null; + } + + getAdc(): number | null { + return this.dynamicData?.adc ?? null; + } + + getStatus(): number | null { + return this.dynamicData?.status ?? null; + } +} diff --git a/src/RotRouter.js b/src/RotRouter.js deleted file mode 100644 index e221c24..0000000 --- a/src/RotRouter.js +++ /dev/null @@ -1,50 +0,0 @@ -import RotClient from './RotClient.js'; -import fs from 'node:fs'; - -export default class RotRouter { // implements TurnMessageHandler - constructor() { - this.clients = {}; - this.loadConfig(); - this.routerDataHandlers = []; // array of callbacks accepting (RotClient, data) - - } - - loadConfig() { - const raw = fs.readFileSync("config.json", 'utf-8'); // TODO: Is the path correct? - const parsed = JSON.parse(raw); - for (const [label, clientConfig] of Object.entries(parsed)) { - const client = new RotClient(label, clientConfig); - client.onDynamicDataUpdate(data => this.routerDataHandlers.forEach(handler => handler(client, data))); - this.clients[label] = client; - } - } - - findClientForBand(band) { - return Object.values(this.clients).find(c => c.hasBand(band)) || null; - } - - handleTurnMessage(msg) { // msg : { band: 14, az: 321 } - const client = this.findClientForBand(msg.band); - if (!client) { - console.error(`No RotClient found for the ${msg.band} band!`); - return; - } - - client.turn(msg.az); - } - - onRotatorData(handler) { // will call with (RotClient, data) - this.routerDataHandlers.push(handler); - } - - async readInitialState() { - const ret = {}; - for (const client of Object.values(this.clients)) { - ret[client.label] = { - initData: await client.readInitData() - }; - } - return ret; - } - -} \ No newline at end of file diff --git a/src/RotRouter.ts b/src/RotRouter.ts new file mode 100644 index 0000000..ae71c5e --- /dev/null +++ b/src/RotRouter.ts @@ -0,0 +1,47 @@ +import { readFileSync } from 'node:fs'; +import RotClient from './RotClient.js'; +import type { Config, DynamicData, InitialState, TurnMessage, TurnMessageHandler } from './types.js'; + +export default class RotRouter implements TurnMessageHandler { + private readonly clients: Record = {}; + private readonly routerDataHandlers: Array<(rotator: RotClient, data: DynamicData) => void> = []; + + constructor() { + this.loadConfig(); + } + + private loadConfig(): void { + const raw = readFileSync('config.json', 'utf-8'); + const parsed = JSON.parse(raw) as Config; + for (const [label, clientConfig] of Object.entries(parsed)) { + const client = new RotClient(label, clientConfig); + client.onDynamicDataUpdate(data => this.routerDataHandlers.forEach(h => h(client, data))); + this.clients[label] = client; + } + } + + private findClientForBand(band: number): RotClient | null { + return Object.values(this.clients).find(c => c.hasBand(band)) ?? null; + } + + handleTurnMessage(msg: TurnMessage): void { + const client = this.findClientForBand(msg.band); + if (!client) { + console.error(`No RotClient found for the ${msg.band} band!`); + return; + } + client.turn(msg.az); + } + + onRotatorData(handler: (rotator: RotClient, data: DynamicData) => void): void { + this.routerDataHandlers.push(handler); + } + + async readInitialState(): Promise { + const state: InitialState = {}; + for (const client of Object.values(this.clients)) { + state[client.label] = { initData: await client.readInitData() }; + } + return state; + } +} diff --git a/src/WebsocketManager.js b/src/WebsocketManager.js deleted file mode 100644 index feec76c..0000000 --- a/src/WebsocketManager.js +++ /dev/null @@ -1,73 +0,0 @@ -import { createNodeWebSocket } from '@hono/node-ws' -import {serve} from "@hono/node-server"; - -export default class WebsocketManager { - - constructor() { - this.clients = []; - this.lastPing = {}; - this.initData = null; - - setInterval(() => { - const thr = Date.now() - 60000; - this.clients.filter(client => this.lastPing[client] < thr).forEach((client) => this.leaveClient(client)); - }, 10000); - } - - getHandler() { - const _this = this; - return { - onOpen: (event, ws) => { - this.joinClient(ws); - }, - onMessage(event, ws) { - if (!event.data) - return; - - const data = JSON.parse(event.data); - if (!data) - return; - - if (data.ping) { - _this.lastPing[ws] = Date.now(); - } - - if (data.data) { - _this.handleMessage(ws, data.data); - console.log(`Data from client: ${event.data}`); - } - }, - onClose: (event, ws) => { - this.leaveClient(ws); - }, - }; - } - - handleMessage(ws, data) { - - } - - send(ws, data) { - ws.send(JSON.stringify({data: data})); - } - - broadcast(data) { - this.clients.forEach(client => this.send(client, data)); - } - - joinClient(ws) { - this.clients.push(ws); - this.lastPing[ws] = Date.now(); - this.send(ws, this.initData) - } - - leaveClient(ws) { - this.clients = this.clients.filter(s => s !== ws); - delete this.lastPing[ws]; - } - - setInitData(initData) { - this.initData = initData; - } - -} diff --git a/src/WebsocketManager.ts b/src/WebsocketManager.ts new file mode 100644 index 0000000..d2a1738 --- /dev/null +++ b/src/WebsocketManager.ts @@ -0,0 +1,80 @@ +import type { WSContext } from 'hono/ws'; + +interface IncomingMessage { + ping?: boolean; + data?: unknown; +} + +export default class WebsocketManager { + private readonly clients = new Set(); + // Map correctly tracks identity — plain object keys would stringify all WSContext to "[object Object]" + private readonly lastPing = new Map(); + private initData: unknown = null; + + constructor() { + setInterval(() => { + const threshold = Date.now() - 60_000; + for (const [client, ping] of this.lastPing) { + if (ping < threshold) this.leaveClient(client); + } + }, 10_000); + } + + getHandler() { + return { + onOpen: (_event: Event, ws: WSContext) => { + this.joinClient(ws); + }, + onMessage: (event: MessageEvent, ws: WSContext) => { + if (!event.data) return; + + let msg: IncomingMessage; + try { + msg = JSON.parse(event.data as string) as IncomingMessage; + } catch { + return; + } + + if (msg.ping) { + this.lastPing.set(ws, Date.now()); + } + + if (msg.data !== undefined) { + this.handleMessage(ws, msg.data); + console.log(`Data from client: ${event.data}`); + } + }, + onClose: (_event: CloseEvent, ws: WSContext) => { + this.leaveClient(ws); + }, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private handleMessage(_ws: WSContext, _data: unknown): void { + // Reserved for future incoming-message handling + } + + private send(ws: WSContext, data: unknown): void { + ws.send(JSON.stringify({ data })); + } + + broadcast(data: unknown): void { + this.clients.forEach(client => this.send(client, data)); + } + + private joinClient(ws: WSContext): void { + this.clients.add(ws); + this.lastPing.set(ws, Date.now()); + this.send(ws, this.initData); + } + + private leaveClient(ws: WSContext): void { + this.clients.delete(ws); + this.lastPing.delete(ws); + } + + setInitData(initData: unknown): void { + this.initData = initData; + } +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 15b5ecb..0000000 --- a/src/index.js +++ /dev/null @@ -1,42 +0,0 @@ -import RotRouter from './RotRouter.js'; -import N1mmServer from "./N1mmServer.js"; -import {Hono} from 'hono'; -import { serve } from '@hono/node-server' -import { serveStatic } from '@hono/node-server/serve-static' -import { createNodeWebSocket } from "@hono/node-ws"; -import WebsocketManager from "./WebsocketManager.js"; - -const router = new RotRouter(); - -const n1mm = new N1mmServer(); -n1mm.onTurnMessage(router); -// n1mm.start(); - -const app = new Hono(); -// app.get('/', (c) => c.text('Hono!')); -app.use('/*', serveStatic({ root: './public' })) - - -const websocketManager = new WebsocketManager(); -const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }) -websocketManager.setInitData(await router.readInitialState()); - -router.onRotatorData((rotator, data) => { - const wsUpdate = {}; - wsUpdate[rotator.label] = { dynamic: data }; - websocketManager.broadcast(wsUpdate); -}); - -app.get( - '/ws', - upgradeWebSocket((c) => websocketManager.getHandler()) -); - -const server = serve({ - fetch: app.fetch, - port: 8787, -}, (info) => { - console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000 -}); - -injectWebSocket(server) \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6136ebb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,34 @@ +import { Hono } from 'hono'; +import { serve } from '@hono/node-server'; +import { serveStatic } from '@hono/node-server/serve-static'; +import { createNodeWebSocket } from '@hono/node-ws'; +import RotRouter from './RotRouter.js'; +import N1mmServer from './N1mmServer.js'; +import WebsocketManager from './WebsocketManager.js'; + +const router = new RotRouter(); + +const n1mm = new N1mmServer(); +n1mm.onTurnMessage(router); +// n1mm.start(); + +const app = new Hono(); +app.use('/*', serveStatic({ root: './public' })); + +const websocketManager = new WebsocketManager(); +const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }); + +websocketManager.setInitData(await router.readInitialState()); + +router.onRotatorData((rotator, data) => { + websocketManager.broadcast({ [rotator.label]: { dynamic: data } }); +}); + +app.get('/ws', upgradeWebSocket(() => websocketManager.getHandler())); + +const server = serve( + { fetch: app.fetch, port: 8787 }, + info => console.log(`Listening on http://localhost:${info.port}`), +); + +injectWebSocket(server); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..f621f63 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,37 @@ +export interface TurnMessage { + band: number; + az: number; +} + +export interface TurnMessageHandler { + handleTurnMessage(msg: TurnMessage): void; +} + +export interface ClientConfig { + ip: string; + bands: number[]; +} + +export type Config = Record; + +export interface DynamicData { + adc: number | null; + azimuth: number | null; + status: number | null; +} + +export interface InitData { + azShift: number | null; + azRange: number | null; + antRadiationAngle: number | null; + antName: string | null; + mapUrl: string | null; + mac: string | null; + elevation: number | null; +} + +export interface ClientState { + initData: InitData; +} + +export type InitialState = Record; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5c430cd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*"] +}