diff --git a/.github/workflows/build_n_test.yml b/.github/workflows/build_n_test.yml new file mode 100644 index 0000000..87c4597 --- /dev/null +++ b/.github/workflows/build_n_test.yml @@ -0,0 +1,45 @@ +name: "Build & Test" + +on: + push: + branches: [main] + pull_request: + branches: ['*'] + workflow_dispatch: + +jobs: + start: + name: Build & Test (Node.JS v${{ matrix.node }}, Fastify v${{ matrix.fastify }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node: [ 22, 21, 20, 19 ] + fastify: ['4.14.0', '4.14', '4.15', '4.16', '5'] + + steps: + - name: Basic (1/1) - Checkout Project + uses: actions/checkout@v4 + + - name: Node.JS (1/3) - Installing + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Node.JS (2/3) - NPM Modules Installing + run: | + npm install + + - name: Node.JS (3/3) - Specific Fastify Installing + run: | + npm install fastify@${{ matrix.fastify }} + + # Устанавливаем и тестируем с Fastify 4.14 + - name: Build (1/2) - TypeScript Compilation + run: npm run build + + - name: Build (2/2) - Launch Test + run: | + npm run test + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..572406b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ba848f9 --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +/src/**/*.ts +/dist/test/** +/info/** +tsconfig.json +.github \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ad34619 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 +https://github.com/bsnext + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..55a7ea4 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Fastify-Bowser +![Build & Test](https://github.com/bsnext/fastify-bowser/actions/workflows/build_n_test.yml/badge.svg) +![Node.JS Supported](https://badgen.net/static/Node.JS/%3E=19.0.0/green) +![Fastify Supported](https://badgen.net/static/Fastify/%3E=14/green) +![Install Size](https://badgen.net/packagephobia/install/@bsnext/fastify-bowser) +![Dependencies](https://badgen.net/bundlephobia/dependency-count/@bsnext/fastify-bowser) +![License](https://badgen.net/static/license/MIT/blue) + +The plugin adds the "request.useragent" property, which returns the parsed data. + +**Tested on Fastify v4.14+ and Node.JS v19+!**
+https://github.com/bsnext/fastify-bowser/actions/workflows/build_n_test.yml + +Why "[bowser](https://www.npmjs.com/package/bowser)", not "[ua-parser-js](https://www.npmjs.com/package/ua-parser-js)" or other library? +Bowser it's a zero-dependency package with MIT license, unlike "ua-parser-js" under AGPL-3.0. Both of these libraries are good, but on top of that, bowser [is about x4 times more faster](https://github.com/bsnext/fastify-bowser/blob/master/info/benchmark.md). + +Why not [those package](https://github.com/Eomm/fastify-user-agent)? Under hood it have a "[useragent](https://www.npmjs.com/package/useragent)" - library with 2 dependencies. One of that is "LRU-Cache". But without cache it [have a performance less than both previously](https://github.com/bsnext/fastify-bowser/blob/master/info/benchmark.md) mentioned libraries. Also, **this library excludes parsing of user-agent until you call this property in request**. + +## Installing: +```bash +npm install @bsnext/fastify-bowser +``` + +## Usage: +```ts +import FastifyBowser from '@bsnext/fastify-bowser'; // TS +import { default as FastifyBowser } from "@bsnext/fastify-bowser"; // MJS +const { default: FastifyBowser } = require(`@bsnext/fastify-bowser`); // CJS + +const server = Fastify(); +await server.register(FastifyBowser, { + // Use parsed user-agent cache + cache: boolean = false; + + // Cache limit. Will be automatically purged, if cache size reach limit. + cacheLimit: number = 100; + + // Automatically cache purge interval in milliseconds. + cachePurgeTime: number = 60 * 5; +}); + +``` + +## Example + +```ts +import Fastify from 'fastify'; +import FastifyBowser from '@bsnext/fastify-bowser'; + +const server = Fastify(...); +await server.register(FastifyBowser, { cache: true }); + +server.get(`/test`, function(request, response) { + response.send(request.useragent); + + /* { + browser: { name: 'Chrome', version: '129.0.0.0' }, + os: { name: 'Windows', version: 'NT 10.0', versionName: '10' }, + platform: { type: 'desktop' }, + engine: { name: 'Blink' } + } */ +}) + +server.listen({ port: 8080 }); + +``` \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..259d5cc --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,29 @@ +declare module "fastify" { + interface FastifyRequest { + useragent: { + ua: string; + browser: { + name: string | undefined; + version: string | undefined; + }; + os: { + name: string | undefined; + version: string | undefined; + versionName: string | undefined; + }; + platform: { + type: string | undefined; + }; + engine: { + name: string | undefined; + }; + }; + } +} +export interface PluginOptions { + cache?: boolean; + cacheLimit?: number; + cachePurgeTime?: number; +} +declare const _default: (fastify: import("fastify").FastifyInstance, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>, initOptions: PluginOptions, done: (err?: Error) => void) => void; +export default _default; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..06fd7b6 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,63 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fastify_plugin_1 = require("fastify-plugin"); +const Bowser = require("bowser"); +const undefinedUserAgentInfo = { + browser: { name: `''`, version: `''` }, + os: {}, + platform: {}, + engine: {} +}; +Object.freeze(undefinedUserAgentInfo.browser); +Object.freeze(undefinedUserAgentInfo.os); +Object.freeze(undefinedUserAgentInfo.platform); +Object.freeze(undefinedUserAgentInfo.engine); +Object.freeze(undefinedUserAgentInfo); +exports.default = (0, fastify_plugin_1.default)(function (fastify, initOptions, done) { + if (initOptions.cache === true) { + if (initOptions.cacheLimit && ((typeof initOptions.cacheLimit !== `number`) || (initOptions.cacheLimit < 1))) { + console.warn(`Invalid parameter "cacheLimit" for @bsnext/fastify-bowser`); + } + if (initOptions.cachePurgeTime && ((typeof initOptions.cachePurgeTime !== `number`) || (initOptions.cachePurgeTime < 1))) { + console.warn(`Invalid parameter "cachePurgeTime" for @bsnext/fastify-bowser`); + } + } + const isCaching = initOptions.cache; + let cacheLimit = initOptions.cacheLimit || 100; + let cacheSize = 0; + let cachePool = Object.create(null); + function purgeCache() { + cachePool = Object.create(null); + cacheSize = 0; + } + if (isCaching) { + setInterval(purgeCache, (initOptions.cachePurgeTime || (60 * 5)) * 1000); + } + fastify.decorateRequest(`useragent`, { + getter() { + const userAgent = this.headers[`user-agent`]; + if ((userAgent === undefined) || (userAgent === ``)) { + return undefinedUserAgentInfo; + } + const cachedQuery = cachePool[userAgent]; + if (isCaching && cachedQuery) { + return cachedQuery; + } + const uaParseResult = Bowser.parse(userAgent); + if (isCaching) { + if (cacheSize >= cacheLimit) { + purgeCache(); + } + cacheSize = cacheSize + 1; + cachePool[userAgent] = uaParseResult; + } + ; + return uaParseResult; + } + }); + done(); +}, { + fastify: `^4.x.x || ^5.x`, + name: `@bsnext/fastify-bowser` +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..387ce36 --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/test/index.d.ts b/dist/test/index.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/test/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/test/index.js b/dist/test/index.js new file mode 100644 index 0000000..5c73daa --- /dev/null +++ b/dist/test/index.js @@ -0,0 +1,65 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const uvu_1 = require("uvu"); +const assert = require("uvu/assert"); +const fastify_1 = require("fastify"); +const index_1 = require("../index"); +function prepareServer() { + return __awaiter(this, void 0, void 0, function* () { + const server = (0, fastify_1.default)({ + logger: false, + ignoreTrailingSlash: true, + }); + yield server.register(index_1.default); + server.get(`/test`, function (request, response) { + response.send({ + test: true, + useragent: request.useragent + }); + }); + return new Promise(function (resolve, reject) { + server.listen({ port: 8080 }, function () { + return __awaiter(this, void 0, void 0, function* () { + resolve(server); + }); + }); + }); + }); +} +(0, uvu_1.test)('Test', () => __awaiter(void 0, void 0, void 0, function* () { + const server = yield prepareServer(); + try { + const result_test = yield fetch(`http://127.0.0.1:8080/test`, { + method: `GET`, + headers: { + [`User-Agent`]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36` + } + }).then((o) => __awaiter(void 0, void 0, void 0, function* () { return yield o.json(); })); + assert.equal(result_test, { + test: true, + useragent: { + browser: { name: 'Chrome', version: '129.0.0.0' }, + os: { name: 'Windows', version: 'NT 10.0', versionName: '10' }, + platform: { type: 'desktop' }, + engine: { name: 'Blink' } + } + }); + } + catch (e) { + throw e; + } + finally { + server.close(); + } +})); +uvu_1.test.run(); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/test/index.js.map b/dist/test/index.js.map new file mode 100644 index 0000000..b9cbc29 --- /dev/null +++ b/dist/test/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,6BAA2B;AAC3B,qCAAqC;AAIrC,qCAAmD;AACnD,oCAAuD;AAIvD,SAAe,aAAa;;QAC3B,MAAM,MAAM,GAAG,IAAA,iBAAO,EAAC;YACtB,MAAM,EAAE,KAAK;YACb,mBAAmB,EAAE,IAAI;SACzB,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,QAAQ,CAAC,eAAY,CAAC,CAAC;QAEpC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,OAAO,EAAE,QAAQ;YAC9C,QAAQ,CAAC,IAAI,CAAC;gBACb,IAAI,EAAE,IAAI;gBACV,SAAS,EAAE,OAAO,CAAC,SAAS;aAC5B,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,OAAO,IAAI,OAAO,CAAC,UAAU,OAAO,EAAE,MAAM;YAC3C,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAC3B;;oBACC,OAAO,CAAC,MAAM,CAAC,CAAC;gBACjB,CAAC;aAAA,CACD,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC;CAAA;AAID,IAAA,UAAI,EAAC,MAAM,EAAE,GAAS,EAAE;IACvB,MAAM,MAAM,GAAG,MAAM,aAAa,EAAE,CAAC;IAErC,IAAI,CAAC;QACJ,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,4BAA4B,EAAE;YAC7D,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACR,CAAC,YAAY,CAAC,EAAE,iHAAiH;aACjI;SACD,CAAC,CAAC,IAAI,CAAC,CAAM,CAAC,EAAC,EAAE,kDAAC,OAAA,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA,GAAA,CAAC,CAAC;QAEnC,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;YACzB,IAAI,EAAE,IAAI;YACV,SAAS,EAAE;gBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE;gBACjD,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE;gBAC9D,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBAC7B,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;aACzB;SACD,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,MAAM,CAAC,CAAC;IACT,CAAC;YAAS,CAAC;QACV,MAAM,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;AACF,CAAC,CAAA,CAAC,CAAC;AAIH,UAAI,CAAC,GAAG,EAAE,CAAC"} \ No newline at end of file diff --git a/info/benchmark.md b/info/benchmark.md new file mode 100644 index 0000000..b7a2703 --- /dev/null +++ b/info/benchmark.md @@ -0,0 +1,61 @@ +#### If you wanna try that, disable "LRU" in "useragent" for fair benchmark: +```js +// node_modules/useragent/index.js : line 510 + +exports.lookup = function lookup(userAgent, jsAgent) { + var key = (userAgent || '')+(jsAgent || ''); + return exports.parse(userAgent, jsAgent); +}; +``` + +#### Modules: +``` +npm install ua-parser-js bowser useragent +``` + +#### Code: + +```ts +import { UAParser } from "ua-parser-js"; +import * as Bowser from "bowser"; +import * as Useragent from "useragent"; + +import * as Benchmark from "benchmark"; + +const bench = new Benchmark.Suite(); + +///////////////////////////// + +const uaParserInstance = new UAParser(); + +///////////////////////////// + +bench.add(`bowser`, function () { + const result = Bowser.parse(`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`); +}); + +bench.add(`ua-parser-js`, function () { + const result = uaParserInstance.setUA(`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`).getResult(); +}); + +bench.add(`useragent`, function () { + const result = Useragent.lookup(`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`); +}); + +///////////////////////////// + +bench.on('cycle', function (e) { + console.log(e.target.toString()); +}); + +bench.run(); +``` + +#### Results: +```js +bowser x 188,346 ops/sec ±0.48% (93 runs sampled) +ua-parser-js x 50,341 ops/sec ±0.70% (94 runs sampled) +useragent x 34,391 ops/sec ±0.37% (93 runs sampled) + +// Node v22.8.0, Win 11, Ryzen 7 3800X 3.89 GHz, 32 GB RAM 3200 MHz +``` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..bcb4036 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "@bsnext/fastify-bowser", + "version": "1.0.2", + "description": "A plugin for Fastify that adds the 'request.useragent' property to get header 'user-agent' parsed data.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "start": "node dist/index.js", + "build": "tsc --project tsconfig.json", + "watch": "tsc --watch", + "test": "node dist/test/index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/bsnext/Fastify-Bowser" + }, + "keywords": [ + "fastify", + "plugin", + "useragent", + "ua", + "user", + "agent", + "parser", + "bowser" + ], + "bugs": { + "url": "https://github.com/bsnext/Fastify-Bowser/issues" + }, + "homepage": "https://github.com/bsnext/Fastify-Bowser#readme", + "author": "SalwadoR", + "type": "commonjs", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.7.5", + "fastify": "^4.14.0", + "typescript": "^5.6.3", + "uvu": "^0.5.6" + }, + "dependencies": { + "bowser": "^2.11.0", + "fastify-plugin": "^5.0.1" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4e0eb60 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,119 @@ +import { FastifyRequest } from "fastify"; +import FastifyPlugin from "fastify-plugin"; + +import * as Bowser from "bowser"; + +//////////////////////////////// + +declare module "fastify" { + interface FastifyRequest { + useragent: { + ua: string; + browser: { + name: string | undefined; + version: string | undefined; + }; + os: { + name: string | undefined; + version: string | undefined; + versionName: string | undefined; + }; + platform: { + type: string | undefined; + }; + engine: { + name: string | undefined; + }; + }; + } +} + +export interface PluginOptions { + cache?: boolean; + cacheLimit?: number; + cachePurgeTime?: number; +} + +//////////////////////////////// + +const undefinedUserAgentInfo = { + browser: { name: `''`, version: `''` }, + os: {}, + platform: {}, + engine: {} +}; + +Object.freeze(undefinedUserAgentInfo.browser); +Object.freeze(undefinedUserAgentInfo.os); +Object.freeze(undefinedUserAgentInfo.platform); +Object.freeze(undefinedUserAgentInfo.engine); +Object.freeze(undefinedUserAgentInfo); + +//////////////////////////////// + +export default FastifyPlugin( + function (fastify, initOptions: PluginOptions, done) { + if (initOptions.cache === true) { + if (initOptions.cacheLimit && ((typeof initOptions.cacheLimit !== `number`) || (initOptions.cacheLimit < 1))) { + console.warn(`Invalid parameter "cacheLimit" for @bsnext/fastify-bowser`); + } + if (initOptions.cachePurgeTime && ((typeof initOptions.cachePurgeTime !== `number`) || (initOptions.cachePurgeTime < 1))) { + console.warn(`Invalid parameter "cachePurgeTime" for @bsnext/fastify-bowser`); + } + } + + //////////////////////////////// + + const isCaching = initOptions.cache; + let cacheLimit = initOptions.cacheLimit || 100; + let cacheSize = 0; + let cachePool = Object.create(null); + + function purgeCache() { + cachePool = Object.create(null); + cacheSize = 0; + } + + if (isCaching) { + setInterval(purgeCache, (initOptions.cachePurgeTime || (60 * 5)) * 1000); + } + + //////////////////////////////// + + fastify.decorateRequest(`useragent`, { + getter() { + const userAgent = (this as FastifyRequest).headers[`user-agent`]; + + if ((userAgent === undefined) || (userAgent === ``)) { + return undefinedUserAgentInfo; + } + + const cachedQuery = cachePool[userAgent]; + + if (isCaching && cachedQuery) { + return cachedQuery; + } + + const uaParseResult = Bowser.parse(userAgent); + + if (isCaching) { + if (cacheSize >= cacheLimit!) { + purgeCache(); + } + + cacheSize = cacheSize + 1; + cachePool[userAgent] = uaParseResult; + }; + + + return uaParseResult; + } + }); + + done(); + }, + { + fastify: `^4.x.x || ^5.x`, + name: `@bsnext/fastify-bowser` + } +); \ No newline at end of file diff --git a/src/test/index.ts b/src/test/index.ts new file mode 100644 index 0000000..9b61cbe --- /dev/null +++ b/src/test/index.ts @@ -0,0 +1,66 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; + +//////////////////////////////// + +import Fastify, { FastifyInstance } from 'fastify'; +import BowserPlugin, { PluginOptions } from '../index'; + +//////////////////////////////// + +async function prepareServer(): Promise { + const server = Fastify({ + logger: false, + ignoreTrailingSlash: true, + }); + + await server.register(BowserPlugin); + + server.get(`/test`, function (request, response) { + response.send({ + test: true, + useragent: request.useragent + }); + }); + + return new Promise(function (resolve, reject) { + server.listen({ port: 8080 }, + async function () { + resolve(server); + } + ); + }); +} + +//////////////////////////////// + +test('Test', async () => { + const server = await prepareServer(); + + try { + const result_test = await fetch(`http://127.0.0.1:8080/test`, { + method: `GET`, + headers: { + [`User-Agent`]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36` + } + }).then(async o => await o.json()); + + assert.equal(result_test, { + test: true, + useragent: { + browser: { name: 'Chrome', version: '129.0.0.0' }, + os: { name: 'Windows', version: 'NT 10.0', versionName: '10' }, + platform: { type: 'desktop' }, + engine: { name: 'Blink' } + } + }); + } catch (e) { + throw e; + } finally { + server.close(); + } +}); + +//////////////////////////////// + +test.run(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e82fefc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2016", + "module": "commonjs", + "noImplicitAny": false, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "removeComments": true, + "preserveConstEnums": true, + // "experimentalDecorators": true, + // "emitDecoratorMetadata": true, + "sourceMap": true, + "declaration": true, + "declarationMap": false, + "rootDir": "src", + "outDir": "dist", + "baseUrl": "./", + }, + "include": [ + "./src/**/**.ts", + ], + "exclude": [ + "node_modules", + "dist", + ] +} \ No newline at end of file