Bring in packages/montar

This commit is contained in:
Abel Luck 2023-03-13 10:04:22 +00:00
parent fe4509a2ae
commit 67f7cf8e1b
19 changed files with 800 additions and 24 deletions

2
.gitignore vendored
View file

@ -6,3 +6,5 @@ dist/**
.next/** .next/**
docker/zammad/auto_install/** docker/zammad/auto_install/**
.npmrc .npmrc
coverage/
build/

48
package-lock.json generated
View file

@ -3369,18 +3369,8 @@
} }
}, },
"node_modules/@digiresilience/montar": { "node_modules/@digiresilience/montar": {
"version": "0.1.6", "resolved": "packages/montar",
"resolved": "https://gitlab.com/api/v4/projects/22248969/packages/npm/@digiresilience/montar/-/@digiresilience/montar-0.1.6.tgz", "link": true
"integrity": "sha1-PSVHC+/DCyaCnbYJHi3xH4d5e1o=",
"dependencies": {
"@types/node": "14.14.6",
"debug": "^4.3.2"
}
},
"node_modules/@digiresilience/montar/node_modules/@types/node": {
"version": "14.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw=="
}, },
"node_modules/@digiresilience/node-signald": { "node_modules/@digiresilience/node-signald": {
"version": "0.0.3", "version": "0.0.3",
@ -25757,6 +25747,22 @@
"typescript": "4.9.4" "typescript": "4.9.4"
} }
}, },
"packages/montar": {
"version": "0.1.6",
"license": "AGPL-3.0-or-later",
"dependencies": {
"debug": "^4.3.2"
},
"devDependencies": {
"babel-preset-link": "*",
"eslint-config-link": "*",
"jest-config-link": "*",
"tsconfig-link": "*"
},
"engines": {
"node": ">=14"
}
},
"packages/tsconfig": { "packages/tsconfig": {
"version": "0.1.4", "version": "0.1.4",
"extraneous": true, "extraneous": true,
@ -27612,19 +27618,13 @@
} }
}, },
"@digiresilience/montar": { "@digiresilience/montar": {
"version": "0.1.6", "version": "file:packages/montar",
"resolved": "https://gitlab.com/api/v4/projects/22248969/packages/npm/@digiresilience/montar/-/@digiresilience/montar-0.1.6.tgz",
"integrity": "sha1-PSVHC+/DCyaCnbYJHi3xH4d5e1o=",
"requires": { "requires": {
"@types/node": "14.14.6", "babel-preset-link": "*",
"debug": "^4.3.2" "debug": "^4.3.2",
}, "eslint-config-link": "*",
"dependencies": { "jest-config-link": "*",
"@types/node": { "tsconfig-link": "*"
"version": "14.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw=="
}
} }
}, },
"@digiresilience/node-signald": { "@digiresilience/node-signald": {

View file

@ -17,5 +17,12 @@ module.exports = {
], ],
"no-extra-semi": "off", "no-extra-semi": "off",
"@typescript-eslint/no-extra-semi": "error", "@typescript-eslint/no-extra-semi": "error",
"@typescript-eslint/ban-ts-comment": [
"error",
{
"ts-nocheck": "allow-with-description",
"ts-expect-error": "allow-with-description",
},
],
}, },
}; };

View file

@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false

View file

@ -0,0 +1,18 @@
require("eslint-config-link/patch/modern-module-resolution");
module.exports = {
extends: [
"eslint-config-link/profile/node",
"eslint-config-link/profile/typescript"
],
parserOptions: { tsconfigRootDir: __dirname, project: "./tsconfig.spec.json" },
rules: {
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
},
],
},
};

View file

@ -0,0 +1,2 @@
# package.json is formatted by package managers, so we ignore it here
package.json

View file

@ -0,0 +1,32 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.1.6](https://gitlab.com/digiresilience/link/montar/compare/0.1.5...0.1.6) (2021-10-11)
### [0.1.5](https://gitlab.com/digiresilience/link/montar/compare/0.1.4...0.1.5) (2021-10-08)
### [0.1.4](https://gitlab.com/digiresilience/link/montar/compare/0.1.3...0.1.4) (2021-10-08)
### [0.1.3](https://gitlab.com/digiresilience/link/montar/compare/0.1.2...0.1.3) (2021-10-08)
### [0.1.2](https://gitlab.com/digiresilience/link/montar/compare/0.1.1...0.1.2) (2020-11-12)
### Features
* add startOnly function to start only certain states ([b776b66](https://gitlab.com/digiresilience/link/montar/commit/b776b6640efe52f934b8afecc64f5e8c2b1be0f3))
### Bug Fixes
* only stop states that were started ([178bbd2](https://gitlab.com/digiresilience/link/montar/commit/178bbd2296671ed480bcc8d22c5f72736f6335b8))
### [0.1.1](https://gitlab.com/digiresilience/link/montar/compare/0.1.0...0.1.1) (2020-11-10)
### Features
* Use debug module for optional debugging ([0cb617c](https://gitlab.com/digiresilience/link/montar/commit/0cb617cf4eac9178cd56af8834bc429f656beba5))
## 0.1.0 (2020-11-10)

37
packages/montar/Makefile Normal file
View file

@ -0,0 +1,37 @@
test: build
mkdir -p coverage
yarn test
.PHONY: build
build: node_modules/
yarn build
lint:
yarn test:lint
fmt:
yarn fix:prettier
yarn fix:lint
doc:
yarn doc
publish: test
npm publish
node_modules/: .npmrc
test -d node_modules || yarn
.npmrc:
ifdef CI_JOB_TOKEN
echo '@guardianproject-ops:registry=https://gitlab.com/api/v4/packages/npm/' > .npmrc
echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
echo '//gitlab.com/api/v4/projects/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
endif
clean:
rm -rf build tmp coverage yarn-error.log
distclean: clean
rm -rf node_modules

138
packages/montar/README.md Normal file
View file

@ -0,0 +1,138 @@
# montar
manage state in typescript. inspired by [tolitius/mount](https://github.com/tolitius/mount) from clojure.
What's this all about? Watch this [video from Stuart Sierra](https://www.youtube.com/watch?v=13cmHf_kt-Q) to learn more about the background of component, mount, and montar.
## Install
```console
$ yarn add -D @digiresilience/montar
```
## Usage
```
import { defState } from "@digiresilience/montar"
```
### Creating State
```
// db.ts
const db = defState("db", {
start: createDbConnection
})
export default db
```
where the createDbConnection function creates a connection (for example to a database) and is defined elsewhere.
### Starting state
```
import { start } from "@digiresilience/montar"
const bootYourApp = async () => {
// .. prepare for app boot
// ..
// boot!
return start()
}
```
### Using State
But wait, there is more.. this state is a top level being, which means it can be simply imported by other namespaces:
For example let's say an app needs a connection above. No problem:
```
// server.ts
import db from "./db"
async function doStuff() {
return db.executeSql("foo")
}
```
Note that before `start()` is called, `db` will be uninitialized and therefore unusable. You must call `start()` before accessing any states.
### Starting/Stopping State
`montar` has start and stop functions that will walk all the states created with `defState` and start / stop them accordingly: i.e. will call their start and stop defined functions.
When testing you might be interested in starting only certain states. You can do this with
- `startOnly(string[])` - only start the given states. WARNING: dependencies are not auto started, so you must ensure all dependencies are passed.
- `startWithout(string[])` - start all states _except_ those passed
### Start and Stop Order
Since dependencies are "injected" by requiring on the namespace level, montar trusts the typescript compiler and node.js runtime to maintain the start and stop order for all the defStates.
The "start" order is then recorded and replayed on each subsequent start
The "stop" order is simply the reverse of the start order.
### Troubleshooting / Debugging
Set the `DEBUG=montar` environment variable for runtime debug logs.
## Differences to mount
montar was created to bring management of top-level application state under
control. It steals greedily from
[tolitius/mount](https://github.com/tolitius/mount) a well-known library in the
clojure and clojurescript community.
The problem solved is: how to load different sub-systems of your application while respecting dependency order? Also, how do achieve this without complicated dependency injection containers that make the code harder to read.
With montar once you define your state, then every other module can import and
use your state without special syntax or wrappers.
montar differs from mount in these ways:
- **start/stop are async** - the start and stop functions are async
- **no var rebinding** - instead we use the little-known [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
- this comes with limitations: only functions, arrays, objects can be defined as state. scalars do not work.
- **not concerned with reloading** - in clj/cljs you have the repl and great hot-code reloading (thanks to immutable and persistent data structures). we do not have this in javascript.
## Credits
Copyright © 2020-present [Center for Digital Resilience][cdr]
### Contributors
| [![Abel Luck][abelxluck_avatar]][abelxluck_homepage]<br/>[Abel Luck][abelxluck_homepage] |
| ---------------------------------------------------------------------------------------- |
[abelxluck_homepage]: https://gitlab.com/abelxluck
[abelxluck_avatar]: https://secure.gravatar.com/avatar/0f605397e0ead93a68e1be26dc26481a?s=100&d=identicon
### License
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
[cdrtech]: https://digiresilience.org/tech/
[cdr]: https://digiresilience.org

View file

@ -0,0 +1,5 @@
{
"presets": [
"babel-preset-link"
]
}

View file

@ -0,0 +1,4 @@
{
"preset": "jest-config-link"
}

View file

@ -0,0 +1,33 @@
{
"name": "@digiresilience/montar",
"version": "0.1.6",
"description": "manage typescript state",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",
"module": "build/module/index.js",
"author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later",
"private": false,
"scripts": {
"build": "tsc -p tsconfig.json",
"fix:lint": "eslint src --ext .ts --fix",
"fmt": "prettier \"src/**/*.ts\" --write",
"test": "DEBUG=montar jest --config jest.config.json",
"lint": "eslint src --ext .ts",
"lint-fmt": "prettier \"src/**/*.ts\" --list-different",
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
"watch:build": "tsc -p tsconfig.json -w"
},
"engines": {
"node": ">=14"
},
"devDependencies": {
"tsconfig-link": "*",
"eslint-config-link": "*",
"jest-config-link": "*",
"babel-preset-link": "*"
},
"dependencies": {
"debug": "^4.3.2"
}
}

View file

@ -0,0 +1,73 @@
import { defState, start, stop, isStarted } from ".";
const _inc41 = (): number => 41 + 1;
const _inc = (n: number): number => n + 1;
const obj = defState("obj", {
start: async () => ({
value: 42,
}),
});
const mutableObj = defState("mutableObj", {
start: async () => ({
value: 41,
}),
});
type FortyOneAdder = () => number;
type Incrementer = (n: number) => number;
const inc41 = defState<FortyOneAdder>("inc41", {
isFunction: true,
start: async () => _inc41,
});
const inc = defState<Incrementer>("inc", {
isFunction: true,
start: async () => _inc,
});
describe("defstate", () => {
beforeEach(async () => {
await start();
});
afterEach(async () => {
await stop();
});
test("obj", async () => {
expect(obj.value).toBe(42);
});
test("mutable obj", async () => {
expect(mutableObj.value).toBe(41);
mutableObj.value++;
expect(mutableObj.value).toBe(42);
});
test("inc41", async () => {
expect(inc41()).toBe(42);
expect(isStarted("inc41")).toBe(true);
});
test("inc", async () => {
expect(inc(41)).toBe(42);
});
});
describe("errors", () => {
test("doesn't exist", () => {
expect(() => isStarted("not-real")).toThrow();
});
test("invalid type", () => {
defState("invalid", { start: () => 42 as any });
expect(start()).rejects.toThrow();
});
test("multiple defs", () => {
defState("foo", { async start() {} });
expect(() => {
defState("foo", { async start() {} });
}).toThrow();
});
});

View file

@ -0,0 +1,136 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-await-in-loop */
import Debug from "debug";
import { mutableProxyFactory } from "./proxy";
const debug = Debug("montar");
export type Start<T> = () => Promise<T>;
interface StateMeta<T> {
name: string;
start: Start<T>;
stop(): Promise<void>;
isFunction: boolean;
setHandler<T extends object>(handler: ProxyHandler<T>): void;
setTarget<T>(target: T): void;
}
interface DefState<T> {
start: Start<T>;
stop?(): Promise<void>;
isFunction?: boolean;
}
interface StateMetaMap<T> {
[name: string]: StateMeta<T>;
}
interface StateMap {
[name: string]: any;
}
const defaultStop = async () => {};
const states: StateMap = {};
const statesMeta: StateMetaMap<any> = {};
const statesOrder: string[] = [];
function getState(name: string) {
if (!statesMeta.hasOwnProperty(name)) {
throw new Error(`State ${name} not started.`);
}
return states[name];
}
function setState(name: string, state: any) {
states[name] = state;
}
function getStateMeta(name: string) {
return statesMeta[name];
}
function setStateMeta(name: string, meta: StateMeta<any>) {
statesMeta[name] = meta;
}
export function isStarted(name: string): boolean {
return getState(name) !== undefined;
}
const canary = {
error:
"I am the bare proxy. You shouldn't see me. If you see me, then a montar state was not started.",
};
export function defState<T>(name: string, meta: DefState<T>): any {
if (statesMeta.hasOwnProperty(name)) {
throw new Error(`Already registered ${name}`);
}
const { start, stop = defaultStop, isFunction = false } = meta;
let initialTarget: any = canary;
if (isFunction) initialTarget = () => canary.error;
const { proxy, setTarget, setHandler } = mutableProxyFactory(initialTarget);
setStateMeta(name, {
name,
start,
stop,
setTarget,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setHandler,
isFunction,
});
statesOrder.push(name);
debug(`defined: ${name}`);
return proxy;
}
async function _start(toStart?: string[]): Promise<void> {
for (const name of toStart) {
debug(` >> starting.. ${name}`);
const meta = getStateMeta(name);
const state = await meta.start();
const stateType = typeof state;
if (!["object", "function"].includes(stateType)) {
throw new Error(
`error ${name}'s start() returned a non-object, non-function type`
);
}
meta.setTarget(state);
setState(name, state);
}
}
export async function start(): Promise<void> {
return _start(statesOrder);
}
export async function startWithout(excluded: string[]): Promise<void> {
const toStart = statesOrder.filter((name) => !excluded.includes(name));
return _start(toStart);
}
export async function startOnly(included: string[]): Promise<void> {
const toStart = statesOrder.filter((name) => included.includes(name));
debug(" startOnly ", included);
return _start(toStart);
}
export async function stop(): Promise<void> {
for (let i = statesOrder.length - 1; i >= 0; i--) {
const name = statesOrder[i];
if (states[name] === undefined) continue;
const meta = statesMeta[name];
await meta.stop();
delete states[name];
debug(`<< stopping.. ${name}`);
}
}

View file

@ -0,0 +1,91 @@
// @ts-nocheck PITA for this file.. revisit later.
import { mutableProxyFactory, PProxyHandler } from "./proxy";
describe("mutable proxy types", () => {
test("function", async () => {
const { proxy, setTarget } = mutableProxyFactory(() => 42);
expect(proxy()).toBe(42);
setTarget(() => 43);
expect(proxy()).toBe(43);
});
test("object", async () => {
const { proxy, setTarget } = mutableProxyFactory({ value: 42 });
expect(proxy.value).toBe(42);
setTarget({ value: 43 });
expect(proxy.value).toBe(43);
});
test("array", async () => {
const { proxy, setTarget } = mutableProxyFactory([42]);
expect(proxy[0]).toBe(42);
setTarget([43]);
expect(proxy[0]).toBe(43);
});
test("object to function", async () => {
const { proxy, setTarget } = mutableProxyFactory({ value: 42 });
expect(proxy.value).toBe(42);
setTarget(() => 43);
expect(() => proxy()).toThrow();
});
test("scalar", async () => {
expect(() => {
mutableProxyFactory(42);
}).toThrow();
});
test("boolean", async () => {
expect(() => {
mutableProxyFactory(false);
}).toThrow();
});
test("null", async () => {
expect(() => {
mutableProxyFactory(null); // eslint-disable-line unicorn/no-null
}).toThrow();
});
test("undefined", async () => {
expect(() => {
mutableProxyFactory();
}).toThrow();
});
test("setHandler", async () => {
const { proxy, setHandler } = mutableProxyFactory({ value: 42 });
setHandler(new PProxyHandler());
expect(proxy.value).toBe(42);
});
test("setTarget", async () => {
const { proxy, setTarget } = mutableProxyFactory({ value: 42 });
setTarget([43]);
expect(proxy[0]).toBe(43);
});
test("setHandler simple", async () => {
const { proxy, setHandler } = mutableProxyFactory({ value: 41 });
expect(proxy.value).toBe(41);
setHandler({
get: () => 42,
});
expect(proxy.value).toBe(42);
});
test("getHandler", async () => {
const { getHandler, setHandler, proxy } = mutableProxyFactory({
value: 42,
});
const handler = new PProxyHandler();
setHandler(handler);
expect(getHandler()).toBe(handler);
expect(proxy.value).toBe(42);
});
test("getTarget", async () => {
const { getTarget } = mutableProxyFactory({ value: 42 });
expect(getTarget().value).toBe(42);
});
});

View file

@ -0,0 +1,130 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/* eslint-disable no-new,no-useless-call */
// mutableProxyFactory from https://stackoverflow.com/a/54460544
// (C) Alex Hall https://stackoverflow.com/users/2482744/alex-hall
// License CC BY-SA 3.0
/* eslint-disable @typescript-eslint/ban-types */
export class PProxyHandler<T extends object> implements ProxyHandler<T> {
getPrototypeOf?(target: T): object | null {
return Reflect.getPrototypeOf(target);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setPrototypeOf?(target: T, v: any): boolean {
return Reflect.setPrototypeOf(target, v);
}
isExtensible?(target: T): boolean {
return Reflect.isExtensible(target);
}
preventExtensions?(target: T): boolean {
return Reflect.preventExtensions(target);
}
getOwnPropertyDescriptor?(
target: T,
p: PropertyKey
): PropertyDescriptor | undefined {
return Reflect.getOwnPropertyDescriptor(target, p);
}
has?(target: T, p: PropertyKey): boolean {
return Reflect.has(target, p);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get?(target: T, p: PropertyKey, receiver: any): any {
return Reflect.get(target, p, receiver);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set?(target: T, p: PropertyKey, value: any, receiver: any): boolean {
return Reflect.set(target, p, value, receiver);
}
deleteProperty?(target: T, p: PropertyKey): boolean {
return Reflect.deleteProperty(target, p);
}
defineProperty?(
target: T,
p: PropertyKey,
attributes: PropertyDescriptor
): boolean {
return Reflect.defineProperty(target, p, attributes);
}
enumerate?(target: T): PropertyKey[] {
return Reflect.ownKeys(target);
}
ownKeys?(target: T): PropertyKey[] {
return Reflect.ownKeys(target);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
apply?(target: T, thisArg: any, argArray?: any): any {
return Reflect.apply(target as Function, thisArg, argArray);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
construct?(target: T, argArray: any, newTarget?: any): object {
return Reflect.construct(target as Function, argArray, newTarget);
}
}
interface MutableProxy<T extends object> {
setTarget(target: T): void;
setHandler(handler: PProxyHandler<T>): void;
getTarget(): T;
getHandler(): ProxyHandler<T>;
proxy: T;
}
export function mutableProxyFactory<T extends object>(
mutableTarget: T,
mutableHandler?: ProxyHandler<T>
): MutableProxy<T> {
if (!mutableHandler) mutableHandler = new PProxyHandler();
return {
setTarget(target: T): void {
new Proxy(target, {}); // test target validity
mutableTarget = target;
},
setHandler(handler: PProxyHandler<T>): void {
new Proxy({}, handler); // test handler validity
Object.keys(handler).forEach((key) => {
const value = handler[key];
if (Reflect[key] && typeof value !== "function") {
throw new Error(`Trap "${key}: ${value}" is not a function`);
}
});
mutableHandler = handler;
},
getTarget(): T {
return mutableTarget;
},
getHandler(): PProxyHandler<T> {
return mutableHandler;
},
proxy: new Proxy(
mutableTarget,
new Proxy(
{},
{
// Dynamically forward all the traps to the associated methods on the mutable handler
get(target, property) {
return (_target, ...args) =>
mutableHandler[property].apply(mutableHandler, [
mutableTarget,
...args,
]);
},
}
)
),
};
}

View file

@ -0,0 +1,37 @@
import { defState, stop, startOnly, startWithout } from ".";
const startOnlyState = defState("startOnlyState", {
start: async () => ({
value: 42,
}),
});
const startedState = defState("startState", {
start: async () => ({
value: 42,
}),
});
const neverStartedState = defState("neverStartedState", {
start: async () => ({
value: 42,
}),
});
describe("starting", () => {
afterEach(async () => stop());
test("startOnly", async () => {
await startOnly(["startOnlyState"]);
expect(startOnlyState.value).toBe(42);
expect(startedState.value).toBe(undefined);
expect(neverStartedState.value).toBe(undefined);
});
test("startWithout", async () => {
await startWithout(["neverStartedState"]);
expect(startOnlyState.value).toBe(42);
expect(startedState.value).toBe(42);
expect(neverStartedState.value).toBe(undefined);
});
});

View file

@ -0,0 +1,12 @@
{
"extends": "tsconfig-link",
"compilerOptions": {
"incremental": true,
"outDir": "build/main",
"rootDir": "src",
"baseUrl": "./",
"types": ["jest", "node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"]
}

View file

@ -0,0 +1,4 @@
{
"exclude": ["node_modules"],
"extends": "./tsconfig.json"
}