Prettier pass

This commit is contained in:
Heinous Tugboat 2022-10-01 10:54:26 -04:00
parent 6369534169
commit bc39f10a8e
19 changed files with 303 additions and 301 deletions

View file

@ -5,7 +5,9 @@ A file synchronisation utility for Bitburner, using the Remote File API.
It allows players to synchronize scripts and text files from their computer's disk to the game in both the Electron build and website.
## How to use (for users)
You must have a recent version of `npm` installed after which you can run
```
npx bitburner-filesync
```

View file

@ -1,4 +1,4 @@
#!/usr/bin/env ts-node
import { start } from "../src/index"
import { start } from "../src/index";
await start();

View file

@ -3,82 +3,80 @@ import { existsSync } from "fs";
// Define a schema
export let config = convict({
allowedFiletypes: {
doc: 'Filetypes that are synchronized to the game.',
format: 'Array',
default: [".js", ".script", ".txt"]
allowedFiletypes: {
doc: "Filetypes that are synchronized to the game.",
format: "Array",
default: [".js", ".script", ".txt"],
},
allowDeletingFiles: {
doc: "Allow deleting files in game if they get deleted off disk.",
format: "Boolean",
default: false,
arg: "allowDeletingFiles",
},
port: {
doc: "The port to bind to.",
format: "Number",
default: 12525,
env: "BB_PORT",
arg: "port",
},
scriptsFolder: {
doc: "The to be synchronized folder.",
format: "String",
default: ".",
env: "BB_SCRIPTFOLDER",
arg: "folder",
},
quiet: {
doc: "Log less internal events to stdout.",
format: "Boolean",
env: "BB_VERBOSE",
default: false,
arg: "quiet",
},
dry: {
doc: "Only print the files to be synchronised.",
format: "Boolean",
env: "BB_DRY",
default: false,
arg: "dry",
},
definitionFile: {
update: {
doc: "Automatically pull the definition file from the game.",
format: "Boolean",
env: "BB_UPDATE_DEF",
default: false,
},
allowDeletingFiles: {
doc: 'Allow deleting files in game if they get deleted off disk.',
format: 'Boolean',
default: false,
arg: 'allowDeletingFiles',
location: {
doc: "Location/name of where the definition file gets placed.",
format: "String",
env: "BB_LOCATION_DEF",
default: "./NetScriptDefinitions.d.ts",
},
port: {
doc: 'The port to bind to.',
format: 'Number',
default: 12525,
env: 'BB_PORT',
arg: 'port'
},
scriptsFolder: {
doc: 'The to be synchronized folder.',
format: 'String',
default: '.',
env: 'BB_SCRIPTFOLDER',
arg: 'folder'
},
quiet: {
doc: 'Log less internal events to stdout.',
format: 'Boolean',
env: 'BB_VERBOSE',
default: false,
arg: 'quiet'
},
dry: {
doc: 'Only print the files to be synchronised.',
format: 'Boolean',
env: 'BB_DRY',
default: false,
arg: 'dry'
},
definitionFile: {
update: {
doc: 'Automatically pull the definition file from the game.',
format: 'Boolean',
env: 'BB_UPDATE_DEF',
default: false
},
location: {
doc: 'Location/name of where the definition file gets placed.',
format: 'String',
env: 'BB_LOCATION_DEF',
default: "./NetScriptDefinitions.d.ts"
}
},
pushAllOnConnection: {
doc: 'Push all files when initial connection is made.',
format: 'Boolean',
env: 'BB_CON_PUSH',
default: false,
arg: 'pushAllOnConnection'
}
},
pushAllOnConnection: {
doc: "Push all files when initial connection is made.",
format: "Boolean",
env: "BB_CON_PUSH",
default: false,
arg: "pushAllOnConnection",
},
});
export function loadConfig() {
const configFile = "filesync.json";
if (existsSync(configFile)) {
try {
config.loadFile(configFile);
} catch (e) {
throw new Error(`Unable to load configuration file at ${configFile}: ${e}`);
}
} else if (!config.get("quiet")) {
console.log("No configuration file found.")
const configFile = "filesync.json";
if (existsSync(configFile)) {
try {
config.loadFile(configFile);
} catch (e) {
throw new Error(`Unable to load configuration file at ${configFile}: ${e}`);
}
} else if (!config.get("quiet")) {
console.log("No configuration file found.");
}
// Perform validation
config.validate({ allowed: 'strict' });
// Perform validation
config.validate({ allowed: "strict" });
}

View file

@ -1,7 +1,7 @@
export class EventType {
static ConnectionMade = "ConnectionMade";
static FileChanged = "FileChanged";
static FileDeleted = "FileDeleted";
static MessageReceived = "MessageReceived";
static MessageSend = "MessageSend";
}
static ConnectionMade = "ConnectionMade";
static FileChanged = "FileChanged";
static FileDeleted = "FileDeleted";
static MessageReceived = "MessageReceived";
static MessageSend = "MessageSend";
}

View file

@ -2,36 +2,38 @@ import CheapWatch from "cheap-watch";
import { config } from "./config";
import { EventType } from "./eventTypes";
import { resolve } from "path";
import type { Signal } from 'signal-js';
import type { File } from './interfaces';
import type { Signal } from "signal-js";
import type { File } from "./interfaces";
function fileFilter(file: File) {
if (config.get("allowedFiletypes").some(extension => file.path.endsWith(extension)))
return true;
if (file.stats.isDirectory())
return true;
return false;
if (config.get("allowedFiletypes").some((extension) => file.path.endsWith(extension))) return true;
if (file.stats.isDirectory()) return true;
return false;
}
export async function setupWatch(signaller: Signal) {
const watch = new CheapWatch({
dir: config.get("scriptsFolder"),
filter: fileFilter,
watch: !config.get("dry")
});
const watch = new CheapWatch({
dir: config.get("scriptsFolder"),
filter: fileFilter,
watch: !config.get("dry"),
});
if (!config.get("quiet")) console.log("Watching folder", resolve(config.get("scriptsFolder")))
if (!config.get("quiet")) console.log("Watching folder", resolve(config.get("scriptsFolder")));
watch.on('+', fileEvent => { if (fileEvent.stats.isFile()) signaller.emit(EventType.FileChanged, fileEvent) });
watch.on('-', fileEvent => { if (fileEvent.stats.isFile()) signaller.emit(EventType.FileDeleted, fileEvent) });
watch.on("+", (fileEvent) => {
if (fileEvent.stats.isFile()) signaller.emit(EventType.FileChanged, fileEvent);
});
watch.on("-", (fileEvent) => {
if (fileEvent.stats.isFile()) signaller.emit(EventType.FileDeleted, fileEvent);
});
// Wait 'till filewatcher is ready to go
await watch.init();
// Wait 'till filewatcher is ready to go
await watch.init();
if (config.get("dry")) {
console.log("Watch would've synchronised:\n", watch.paths)
process.exit();
}
if (config.get("dry")) {
console.log("Watch would've synchronised:\n", watch.paths);
process.exit();
}
return watch;
return watch;
}

View file

@ -2,57 +2,63 @@ import { setupWatch } from "./fileWatch";
import { config, loadConfig } from "./config";
import { setupSocket } from "./networking/webSocket";
import signal from "signal-js";
import { fileChangeEventToMsg, fileRemovalEventToMsg, requestFilenames, requestDefinitionFile } from "./networking/messageGenerators";
import {
fileChangeEventToMsg,
fileRemovalEventToMsg,
requestFilenames,
requestDefinitionFile,
} from "./networking/messageGenerators";
import { EventType } from "./eventTypes";
import { messageHandler } from "./networking/messageHandler";
import { FileEvent, Message } from './interfaces';
import { FileEvent, Message } from "./interfaces";
export async function start() {
loadConfig();
const watch = await setupWatch(signal);
const socket = setupSocket(signal);
loadConfig();
const watch = await setupWatch(signal);
const socket = setupSocket(signal);
// Add a handler for received messages.
signal.on(EventType.MessageReceived, (msg: Message) => messageHandler(signal, msg, watch.paths));
// Add a handler for received messages.
signal.on(EventType.MessageReceived, (msg: Message) => messageHandler(signal, msg, watch.paths));
// Add a handler for when a connection to a game is made.
signal.on(EventType.ConnectionMade, () => {
console.log("Connection made!");
// Add a handler for when a connection to a game is made.
signal.on(EventType.ConnectionMade, () => {
console.log("Connection made!");
if (config.get("definitionFile").update) {
signal.emit(EventType.MessageSend, requestDefinitionFile());
}
if (config.get("definitionFile").update) {
signal.emit(EventType.MessageSend, requestDefinitionFile());
}
if (config.get("pushAllOnConnection")) {
const extensions = config.get("allowedFiletypes");
for (const path of watch.paths.keys()) {
if (extensions.some(extension => path.endsWith(extension)))
signal.emit(EventType.MessageSend, fileChangeEventToMsg({ path }))
}
} else {
// Upload missing files to the game.
signal.emit(EventType.MessageSend, requestFilenames());
}
})
if (config.get("pushAllOnConnection")) {
const extensions = config.get("allowedFiletypes");
for (const path of watch.paths.keys()) {
if (extensions.some((extension) => path.endsWith(extension)))
signal.emit(EventType.MessageSend, fileChangeEventToMsg({ path }));
}
} else {
// Upload missing files to the game.
signal.emit(EventType.MessageSend, requestFilenames());
}
});
// Add a handler for changed files.
signal.on(EventType.FileChanged, (fileEvent: FileEvent) => {
if (!config.get("quiet")) console.log(fileEvent.path + " changed");
signal.emit(EventType.MessageSend, fileChangeEventToMsg(fileEvent))
});
// Add a handler for changed files.
signal.on(EventType.FileChanged, (fileEvent: FileEvent) => {
if (!config.get("quiet")) console.log(fileEvent.path + " changed");
signal.emit(EventType.MessageSend, fileChangeEventToMsg(fileEvent));
});
// Add a handler for removed files, if allowed.
if (config.get("allowDeletingFiles"))
signal.on(EventType.FileDeleted, (fileEvent: FileEvent) =>
signal.emit(EventType.MessageSend, fileRemovalEventToMsg(fileEvent)));
// Add a handler for removed files, if allowed.
if (config.get("allowDeletingFiles"))
signal.on(EventType.FileDeleted, (fileEvent: FileEvent) =>
signal.emit(EventType.MessageSend, fileRemovalEventToMsg(fileEvent)),
);
console.log(`Server is ready, running on ${config.get("port")}!`)
console.log(`Server is ready, running on ${config.get("port")}!`);
process.on('SIGINT', function () {
console.log("Shutting down!");
process.on("SIGINT", function () {
console.log("Shutting down!");
watch.close();
socket.close();
process.exit();
});
watch.close();
socket.close();
process.exit();
});
}

View file

@ -1,4 +1,4 @@
import type { Stats } from 'fs';
import type { Stats } from "fs";
export interface Message {
id: string;
@ -12,5 +12,5 @@ export interface FileEvent {
}
export interface File extends FileEvent {
stats: Stats
stats: Stats;
}

View file

@ -1,58 +1,56 @@
import { readFileSync } from "fs";
import { config } from "../config";
import { join } from "path";
import type { FileEvent, Message } from '../interfaces';
import type { FileEvent, Message } from "../interfaces";
let messageCounter = 0;
export function fileChangeEventToMsg({ path }: FileEvent): Message {
return {
"jsonrpc": "2.0",
"method": "pushFile",
"params": {
"server": "home",
"filename": addLeadingSlash(path),
"content": readFileSync(join(config.get("scriptsFolder"), path)).toString()
},
"id": (messageCounter++).toString()
}
return {
jsonrpc: "2.0",
method: "pushFile",
params: {
server: "home",
filename: addLeadingSlash(path),
content: readFileSync(join(config.get("scriptsFolder"), path)).toString(),
},
id: (messageCounter++).toString(),
};
}
export function fileRemovalEventToMsg({ path }: FileEvent): Message {
return {
"jsonrpc": "2.0",
"method": "deleteFile",
"params": {
"server": "home",
"filename": addLeadingSlash(path),
},
"id": (messageCounter++).toString()
}
return {
jsonrpc: "2.0",
method: "deleteFile",
params: {
server: "home",
filename: addLeadingSlash(path),
},
id: (messageCounter++).toString(),
};
}
export function requestDefinitionFile(): Message {
return {
"jsonrpc": "2.0",
"method": "getDefinitionFile",
"id": (messageCounter++).toString()
}
return {
jsonrpc: "2.0",
method: "getDefinitionFile",
id: (messageCounter++).toString(),
};
}
export function requestFilenames(): Message {
return {
"jsonrpc": "2.0",
"method": "getFileNames",
"params": {
"server": "home",
},
"id": (messageCounter++).toString()
}
return {
jsonrpc: "2.0",
method: "getFileNames",
params: {
server: "home",
},
id: (messageCounter++).toString(),
};
}
function addLeadingSlash(path: string): string {
const slashes = path.match('/');
if (slashes)
return `/${path}`
else
return path
const slashes = path.match("/");
if (slashes) return `/${path}`;
else return path;
}

View file

@ -3,41 +3,40 @@ import { Stats, writeFile } from "fs";
import { config } from "../config";
import { EventType } from "../eventTypes";
import { fileChangeEventToMsg } from "./messageGenerators";
import type { Signal } from 'signal-js';
import { Message } from '../interfaces';
import type { Signal } from "signal-js";
import { Message } from "../interfaces";
export function messageHandler(signaller: Signal, msg: Message, paths: Map<string, Stats>) {
let incoming;
let incoming;
try { incoming = JSON.parse(msg.toString()); }
catch (err) { return console.log(err); }
console.log(incoming)
if (incoming.id == undefined) return;
try {
incoming = JSON.parse(msg.toString());
} catch (err) {
return console.log(err);
}
console.log(incoming);
if (incoming.id == undefined) return;
if (incoming.result) {
const request = messageTracker.get(incoming.id);
if (request?.method &&
request.method == "getDefinitionFile"
&& incoming.result) {
writeFile(config.get("definitionFile").location, incoming.result, (err) => {
if (err) return console.log(err);
});
}
if (request?.method &&
request.method == "getFileNames"
&& incoming.result) {
const gameFiles = incoming.result.map((file: string) => removeLeadingSlash(file));
paths.forEach((stats, fileName) => {
if (!stats.isDirectory() && !gameFiles.includes(fileName))
signaller.emit(EventType.MessageSend, fileChangeEventToMsg({ path: fileName }));
})
}
if (incoming.result) {
const request = messageTracker.get(incoming.id);
if (request?.method && request.method == "getDefinitionFile" && incoming.result) {
writeFile(config.get("definitionFile").location, incoming.result, (err) => {
if (err) return console.log(err);
});
}
if (request?.method && request.method == "getFileNames" && incoming.result) {
const gameFiles = incoming.result.map((file: string) => removeLeadingSlash(file));
paths.forEach((stats, fileName) => {
if (!stats.isDirectory() && !gameFiles.includes(fileName))
signaller.emit(EventType.MessageSend, fileChangeEventToMsg({ path: fileName }));
});
}
}
}
function removeLeadingSlash(path: string) {
const reg = /^\//;
return path.replace(reg, "")
const reg = /^\//;
return path.replace(reg, "");
}

View file

@ -1,21 +1,21 @@
import type { Message } from '../interfaces';
import type { Message } from "../interfaces";
class MessageTracker {
data = new Map<string, Message>();
#maxLength = 200;
data = new Map<string, Message>();
#maxLength = 200;
push(msg: Message) {
this.data.set(msg.id, msg);
push(msg: Message) {
this.data.set(msg.id, msg);
if (this.data.size > this.#maxLength) {
const [firstKey] = this.data.keys();
this.data.delete(firstKey);
}
if (this.data.size > this.#maxLength) {
const [firstKey] = this.data.keys();
this.data.delete(firstKey);
}
}
get(index: string) {
return this.data.get(index);
}
get(index: string) {
return this.data.get(index);
}
}
export const messageTracker = new MessageTracker();

View file

@ -1,32 +1,30 @@
import type { Signal } from 'signal-js';
import { WebSocketServer } from 'ws';
import type { Signal } from "signal-js";
import { WebSocketServer } from "ws";
import { config } from "../config";
import { EventType } from "../eventTypes"
import { Message } from '../interfaces';
import { requestDefinitionFile } from './messageGenerators';
import { messageTracker } from "./messageTracker"
import { EventType } from "../eventTypes";
import { Message } from "../interfaces";
import { requestDefinitionFile } from "./messageGenerators";
import { messageTracker } from "./messageTracker";
export function setupSocket(signaller: Signal) {
const wss = new WebSocketServer({ port: config.get("port") });
const wss = new WebSocketServer({ port: config.get("port") });
wss.on("connection", function connection(ws) {
function sendMessage(msg: Message) {
messageTracker.push(msg);
ws.send(JSON.stringify(msg));
}
wss.on('connection', function connection(ws) {
function sendMessage(msg: Message) {
messageTracker.push(msg);
ws.send(JSON.stringify(msg));
}
ws.on('message', (msg) => {
signaller.emit(EventType.MessageReceived, msg);
});
signaller.on(EventType.MessageSend, (msg: Message) => {
sendMessage(msg);
});
signaller.trigger(EventType.ConnectionMade);
ws.on("message", (msg) => {
signaller.emit(EventType.MessageReceived, msg);
});
return wss;
signaller.on(EventType.MessageSend, (msg: Message) => {
sendMessage(msg);
});
signaller.trigger(EventType.ConnectionMade);
});
return wss;
}

10
src/signals.d.ts vendored
View file

@ -1,10 +1,8 @@
declare module 'signal-js' {
declare module "signal-js" {
export type Signal = typeof signal;
export default class signal {
static on<T>(event: string, callback: (data: T) => void): void
static emit<T>(event: string, data: T): void
static trigger(event: string): void
static on<T>(event: string, callback: (data: T) => void): void;
static emit<T>(event: string, data: T): void;
static trigger(event: string): void;
}
}

View file

@ -1,17 +1,17 @@
import { setupWatch } from '../src/fileWatch';
import { expect } from 'chai';
import { stub, createStubInstance } from 'sinon';
import CheapWatch from 'cheap-watch';
import signal from 'signal-js';
import { setupWatch } from "../src/fileWatch";
import { expect } from "chai";
import { stub, createStubInstance } from "sinon";
import CheapWatch from "cheap-watch";
import signal from "signal-js";
describe('fileWatch', () => {
describe('setupWatch', () => {
it('should exist', () => {
describe("fileWatch", () => {
describe("setupWatch", () => {
it("should exist", () => {
expect(setupWatch).to.exist;
});
it('should instantiate and initialize CheapWatch', async () => {
const consoleStub = stub(console, 'log');
it("should instantiate and initialize CheapWatch", async () => {
const consoleStub = stub(console, "log");
const watchInstance = createStubInstance(CheapWatch);
const watchConstructorStub = stub().returns(watchInstance);
Object.setPrototypeOf(CheapWatch, watchConstructorStub);

View file

@ -1,27 +1,32 @@
import { expect } from 'chai';
import { fileChangeEventToMsg, fileRemovalEventToMsg, requestDefinitionFile, requestFilenames } from '../../src/networking/messageGenerators';
import { expect } from "chai";
import {
fileChangeEventToMsg,
fileRemovalEventToMsg,
requestDefinitionFile,
requestFilenames,
} from "../../src/networking/messageGenerators";
describe('messageGenerators', () => {
describe('fileChangeEventToMsg', () => {
it('should exist', () => {
describe("messageGenerators", () => {
describe("fileChangeEventToMsg", () => {
it("should exist", () => {
expect(fileChangeEventToMsg).to.exist;
});
});
describe('fileRemovalEventToMsg', () => {
it('should exist', () => {
describe("fileRemovalEventToMsg", () => {
it("should exist", () => {
expect(fileRemovalEventToMsg).to.exist;
});
});
describe('requestDefinitionFile', () => {
it('should exist', () => {
describe("requestDefinitionFile", () => {
it("should exist", () => {
expect(requestDefinitionFile).to.exist;
});
});
describe('requestFilenames', () => {
it('should exist', () => {
describe("requestFilenames", () => {
it("should exist", () => {
expect(requestFilenames).to.exist;
});
});

View file

@ -1,8 +1,8 @@
import { expect } from 'chai';
import { messageHandler } from '../../src/networking/messageHandler';
import { expect } from "chai";
import { messageHandler } from "../../src/networking/messageHandler";
describe('messageHandler', () => {
it('should exist', () => {
describe("messageHandler", () => {
it("should exist", () => {
expect(messageHandler).to.exist;
});
});

View file

@ -1,8 +1,8 @@
import { expect } from 'chai';
import { messageTracker } from '../../src/networking/messageTracker';
import { expect } from "chai";
import { messageTracker } from "../../src/networking/messageTracker";
describe('messageTracker', () => {
it('should exist', () => {
describe("messageTracker", () => {
it("should exist", () => {
expect(messageTracker).to.exist;
});
});

View file

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { expect } from "chai";
describe('webSocket', () => {
it('should run test', () => {
expect(true).to.eq(true)
})
describe("webSocket", () => {
it("should run test", () => {
expect(true).to.eq(true);
});
});

View file

@ -1,6 +1,6 @@
import chai from 'chai';
import { createSandbox, stub } from 'sinon';
import sinonChai from 'sinon-chai';
import chai from "chai";
import { createSandbox, stub } from "sinon";
import sinonChai from "sinon-chai";
export async function mochaGlobalSetup() {
// initial global setup
@ -27,5 +27,5 @@ export const mochaHooks: Mocha.RootHookObject = {
afterEach(done: () => void) {
this.sandbox.restore();
done();
}
}
},
};

View file

@ -4,16 +4,12 @@
"esm": true,
"experimentalSpecifierResolution": "node"
},
"include": [
"src/signals.d.ts",
"src/**/*",
"test/**/*"
],
"include": ["src/signals.d.ts", "src/**/*", "test/**/*"],
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
@ -22,7 +18,7 @@
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
@ -32,7 +28,7 @@
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
@ -46,14 +42,14 @@
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */