From 9ff13ea2ae3994bdde75d26d77e5d8290eb7521a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20BECHER?= Date: Sat, 18 May 2024 18:02:09 +0200 Subject: [PATCH] initial commit --- .editorconfig | 8 + .gitignore | 2 + package-lock.json | 213 ++++++++++++++++++++++++++ package.json | 18 +++ src/exceptions.ts | 21 +++ src/index.ts | 7 + src/pattern.ts | 17 ++ src/placeholder.ts | 171 +++++++++++++++++++++ src/resolver/index.ts | 30 ++++ src/resolver/types.ts | 5 + src/string-section.ts | 47 ++++++ src/unknown-variable-handler/index.ts | 6 + src/unknown-variable-handler/types.ts | 4 + src/utils.ts | 5 + ts-loader.js | 4 + tsconfig.json | 24 +++ 16 files changed, 582 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/exceptions.ts create mode 100644 src/index.ts create mode 100644 src/pattern.ts create mode 100644 src/placeholder.ts create mode 100644 src/resolver/index.ts create mode 100644 src/resolver/types.ts create mode 100644 src/string-section.ts create mode 100644 src/unknown-variable-handler/index.ts create mode 100644 src/unknown-variable-handler/types.ts create mode 100644 src/utils.ts create mode 100644 ts-loader.js create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7278fb6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15c8a3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/src/test.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..451d2b0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,213 @@ +{ + "name": "placeholder-js", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "placeholder-js", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.12.11", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", + "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..98e7790 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "placeholder-js", + "version": "1.0.0", + "description": "String substitutor", + "main": "src/index.ts", + "type": "module", + "scripts": { + "start": "node --import ./ts-loader.js src/index.ts", + "test": "node --import ./ts-loader.js src/test.ts" + }, + "author": "Hervé BECHER", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.12.11", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + } +} diff --git a/src/exceptions.ts b/src/exceptions.ts new file mode 100644 index 0000000..7073eda --- /dev/null +++ b/src/exceptions.ts @@ -0,0 +1,21 @@ +export class AbortSubstitutionException extends Error { } + +export class PlaceholderException extends Error { + readonly #input: string; + readonly #pos: number; + + constructor(input: string, pos: number, message?: string, cause?: any) { + super(message, { cause }); + + this.#input = input; + this.#pos = pos; + } + + public get input() { + return this.#input; + } + + public get position() { + return this.#pos; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7dbf8cc --- /dev/null +++ b/src/index.ts @@ -0,0 +1,7 @@ +export { PlaceholderOptions, PlaceholderSubstitutor } from "./placeholder.js"; +export * from "./pattern.js"; +export * as VariableResolvers from "./resolver/index.js"; +export * from "./resolver/types.js"; +export * as UnknownVariableHandlers from "./unknown-variable-handler/index.js"; +export * from "./unknown-variable-handler/types.js"; +export * from "./exceptions.js"; diff --git a/src/pattern.ts b/src/pattern.ts new file mode 100644 index 0000000..8f3228d --- /dev/null +++ b/src/pattern.ts @@ -0,0 +1,17 @@ +export default class PlaceholderPattern { + readonly #prefix: string; + readonly #suffix: string; + + public constructor(prefix: string, suffix: string) { + this.#prefix = prefix; + this.#suffix = suffix; + } + + public get prefix() { + return this.#prefix; + } + + public get suffix() { + return this.#suffix; + } +} diff --git a/src/placeholder.ts b/src/placeholder.ts new file mode 100644 index 0000000..844fa18 --- /dev/null +++ b/src/placeholder.ts @@ -0,0 +1,171 @@ +import { AbortSubstitutionException, PlaceholderException } from "./exceptions.js"; +import PlaceholderPattern from "./pattern.js"; +import { EMPTY as emptyResolver } from "./resolver/index.js"; +import { VariableResolver } from "./resolver/types.js"; +import StringSection from "./string-section.js"; +import { UnknownVariableHandler } from "./unknown-variable-handler/types.js"; +import { isDefined, Nullable } from "./utils.js"; + + +export type PlaceholderOptions = { + pattern?: PlaceholderPattern; + escapeChar?: string; + recursive?: boolean; + resolver?: VariableResolver; + unknownVariableHandler?: Nullable; +}; + +const DEFAULT_OPTIONS: Required = { + pattern: new PlaceholderPattern("${", "}"), + escapeChar: '\\', + recursive: false, + resolver: emptyResolver, + unknownVariableHandler: null +}; + +export class PlaceholderSubstitutor { + readonly #pattern: PlaceholderPattern; + readonly #escapeChar: string; + readonly #recursive: boolean; + readonly #resolver: VariableResolver; + readonly #unknownVariableHandler: Nullable; + + public constructor(options: PlaceholderOptions = DEFAULT_OPTIONS) { + this.#pattern = options.pattern ?? DEFAULT_OPTIONS.pattern; + this.#escapeChar = options.escapeChar ?? DEFAULT_OPTIONS.escapeChar; + this.#recursive = options.recursive ?? DEFAULT_OPTIONS.recursive; + this.#resolver = options.resolver ?? DEFAULT_OPTIONS.resolver; + this.#unknownVariableHandler = options.unknownVariableHandler ?? DEFAULT_OPTIONS.unknownVariableHandler; + } + + public get pattern() { + return this.#pattern; + } + + public get escapeChar() { + return this.#escapeChar; + } + + public get recursive() { + return this.#recursive; + } + + public get resolver() { + return this.#resolver; + } + + public get unknownVariableHandler() { + return this.#unknownVariableHandler; + } + + public replace(s: string): string; + public replace(s: Nullable): Nullable; + public replace(s: Nullable) { + return s ? new Substitutor(this, new StringSection(s), false).replace() : s; + } +} + +class Substitutor { + readonly #substitutor: PlaceholderSubstitutor; + readonly #input: StringSection; + readonly #inner: boolean; + #pos = 0; + + constructor(substitutor: PlaceholderSubstitutor, input: StringSection, inner: boolean) { + this.#substitutor = substitutor; + this.#input = input; + this.#inner = inner; + } + + replace(): string { + const result: string[] = []; + + let escaping = false; + let hasSuffix = false; + + while (this.#pos < this.#input.length) { + const c = this.#input.charAt(this.#pos); + + if (escaping) { + result.push(c); + escaping = false; + } else if (c === this.#substitutor.escapeChar) { + escaping = true; + } else if (this.tryAdvance(this.#substitutor.pattern.prefix)) { + const innerSubstitutor = new Substitutor(this.#substitutor, this.#input.subSection(this.#pos), true); + const variable = innerSubstitutor.replace(); + + if (variable.length === 0) { + throw new PlaceholderException(this.#input.getSection(), this.#pos, "Empty variable"); + } + + const value = this.resolveVariable(variable); + + if (!isDefined(value)) { + result.push(this.#substitutor.pattern.prefix, variable, this.#substitutor.pattern.suffix); + } else if (this.#substitutor.recursive) { + result.push(this.#substitutor.replace(value)); + } else { + result.push(value); + } + + this.#pos += innerSubstitutor.#pos; + + continue; + } else if (this.tryAdvance(this.#substitutor.pattern.suffix)) { + if (this.#inner) { + hasSuffix = true; + break; + } + + throw new PlaceholderException(this.#input.getSection(), this.#pos, "Missing prefix"); + } else { + result.push(c); + } + + this.#pos++; + } + + if (this.#inner && !hasSuffix) { + throw new PlaceholderException(this.#input.getSection(), this.#pos, "Missing suffix"); + } + + return result.join(""); + } + + private continuesWith(sequence: string) { + return this.#input.startsWith(sequence, this.#pos); + } + + private tryAdvance(sequence: string) { + const b = this.continuesWith(sequence); + + if (b) { + this.#pos += sequence.length; + } + + return b; + } + + private resolveVariable(name: string) { + const value = this.#substitutor.resolver(name); + + if (isDefined(value)) { + return typeof value === "string" ? value : value(); + } + + if (this.#substitutor.unknownVariableHandler) { + try { + return this.#substitutor.unknownVariableHandler(this.#substitutor, name); + } catch (e: any) { + if (e instanceof AbortSubstitutionException) { + throw new PlaceholderException(this.#input.getSection(), this.#pos, "Substitution aborted due to unknown variable", e); + } + + throw new PlaceholderException(this.#input.getSection(), this.#pos, "An error occurred while handling unknown variable", e); + } + } + + return null; + } +} diff --git a/src/resolver/index.ts b/src/resolver/index.ts new file mode 100644 index 0000000..7b2e767 --- /dev/null +++ b/src/resolver/index.ts @@ -0,0 +1,30 @@ +import { isDefined, Nullable } from "../utils.js"; +import { ObjectResolver, VariableResolver, VariableValue } from "./types.js"; + +export const EMPTY: VariableResolver = () => null; + +export function combine(...resolvers: VariableResolver[]) { + return (name: string) => { + for (const resolver of resolvers) { + const value = resolver(name); + + if (isDefined(value)) { + return value; + } + } + + return null; + }; +} + +export function map(values: Map): VariableResolver { + return (name: string) => values.get(name); +} + +export function object(values: Record): VariableResolver { + return (name: string) => values[name]; +} + +export function mapper(mappers: (name: string) => Nullable>): (obj: T) => VariableResolver { + return (obj: T) => (name: string) => mappers(name)?.(obj); +} diff --git a/src/resolver/types.ts b/src/resolver/types.ts new file mode 100644 index 0000000..1ddb44a --- /dev/null +++ b/src/resolver/types.ts @@ -0,0 +1,5 @@ +import { Nullable } from "../utils.js"; + +export type VariableValue = string | (() => string); +export type VariableResolver = (name: string) => Nullable; +export type ObjectResolver = (obj: T) => Nullable; diff --git a/src/string-section.ts b/src/string-section.ts new file mode 100644 index 0000000..c9f0d77 --- /dev/null +++ b/src/string-section.ts @@ -0,0 +1,47 @@ +export default class StringSection { + readonly #parent: string; + readonly #offset: number; + readonly #length: number; + + constructor(parent: string, offset: number = 0, length: number = parent.length - offset) { + if (offset < 0) { + throw new Error("offset < 0"); + } + + if (length < 0) { + throw new Error("length < 0"); + } + + if (offset + length > parent.length) { + throw new Error("offset + length > parent.length"); + } + + this.#parent = parent; + this.#offset = offset; + this.#length = length; + } + + public get length() { + return this.#length; + } + + public getSection() { + return this.substring(0); + } + + public subSection(offset: number = 0, length: number = this.#length - offset) { + return new StringSection(this.#parent, this.#offset + offset, length); + } + + public charAt(index: number) { + return this.#parent.charAt(this.#offset + index); + } + + public startsWith(prefix: string, offset: number = 0) { + return this.#parent.startsWith(prefix, this.#offset + offset); + } + + public substring(beginIndex: number = 0, endIndex: number = this.#length - beginIndex) { + return this.#parent.substring(this.#offset + beginIndex, this.#offset + endIndex); + } +} diff --git a/src/unknown-variable-handler/index.ts b/src/unknown-variable-handler/index.ts new file mode 100644 index 0000000..1d9f1cb --- /dev/null +++ b/src/unknown-variable-handler/index.ts @@ -0,0 +1,6 @@ +import { AbortSubstitutionException } from "../exceptions.js"; +import { UnknownVariableHandler } from "./types.js"; + +export const ABORT: UnknownVariableHandler = () => { + throw new AbortSubstitutionException(); +}; diff --git a/src/unknown-variable-handler/types.ts b/src/unknown-variable-handler/types.ts new file mode 100644 index 0000000..692aae9 --- /dev/null +++ b/src/unknown-variable-handler/types.ts @@ -0,0 +1,4 @@ +import { PlaceholderSubstitutor } from "../placeholder.js"; +import { Nullable } from "../utils.js"; + +export type UnknownVariableHandler = (substitutor: PlaceholderSubstitutor, variable: string) => Nullable; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..aec55fe --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,5 @@ +export type Nullable = T | null | undefined; + +export function isDefined(obj: Nullable): obj is T & {} { + return obj !== null && obj !== undefined; +} diff --git a/ts-loader.js b/ts-loader.js new file mode 100644 index 0000000..566b780 --- /dev/null +++ b/ts-loader.js @@ -0,0 +1,4 @@ +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; + +register("ts-node/esm", pathToFileURL("./")); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5f32ff9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "alwaysStrict": true, + "esModuleInterop": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noErrorTruncation": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "allowUnreachableCode": true, + "strict": true + }, + "exclude": [ + "node_modules" + ], + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node" + } +} \ No newline at end of file