@ops-ai/feature-flags-toggly 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ ## 0.0.1
2
+
3
+ 2022-11-21 (Date of Last Commit)
4
+
5
+ * Toggly classe & models
6
+ * Allow usage without Toggly service (by providing flagDefaults)
7
+ * Allow usage with Toggly service (by providing your App Key & Environment name)
8
+ * Feature evaluation methods unit tests
9
+ * Documentation
10
+ * License
11
+
12
+
13
+
package/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Copyright 2022 opsAI LLC
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ Lightweight package that provides feature flags support for javascript applications allowing you to check feature status and enable/disable them easily.
2
+
3
+ Can be used *WITH* or *WITHOUT* [Toggly.io](https://toggly.io).
4
+
5
+ ## What is a Feature Flag
6
+
7
+ A feature flag (or toggle) in software development provides an alternative to maintaining multiple feature branches in source code. A condition within the code enables or disables a feature during runtime.
8
+
9
+ In agile settings the feature flag is used in production, to switch on the feature on demand, for some or all the users. Thus, feature flags make it easier to release often. Advanced roll out strategies such as canary roll out and A/B testing are easier to handle.
10
+
11
+ ## Installation
12
+
13
+ Simply embed our latest bundle from the following CDN.
14
+
15
+ ```html
16
+ <script src="..."></script>
17
+ ```
18
+
19
+ Alternatively, you can use NPM to manually build the bundled *.js file.
20
+
21
+ ```shell
22
+ $ npm install @ops-ai/feature-flags-toggly
23
+ $ cd node_modules/@ops-ai/feature-flags-toggly && npm run build
24
+ ```
25
+
26
+ And then grab the generated bundled file from the */dist directory.
27
+
28
+ ## Basic Usage (with Toggly.io)
29
+
30
+ Initialize Toggly by running the Toggly.init method and by providing your App Key from your [Toggly application page](https://app.toggly.io)
31
+
32
+ ```js
33
+ var featureFlagsDefaults = {
34
+ "SignUpButton": true,
35
+ "DemoScreenshot": true
36
+ };
37
+
38
+ Toggly.init({
39
+ appKey: '<YOUR_APP_KEY>',
40
+ environment: '<YOUR_APP_ENVIRONMENT>'
41
+ })
42
+ .then(function () {
43
+ // Now you can check if a feature (or more) is Enabled/Disabled
44
+
45
+ if (Toggly.isFeatureOn('SignUpButton')) {
46
+ // SignUpButton is ON
47
+ }
48
+
49
+ if (Toggly.isFeatureOff('DemoScreenshot')) {
50
+ // DemoScreenshot is OFF
51
+ }
52
+ });
53
+ ```
54
+
55
+ You can also check multiple feature keys and make use of the *requirement* (FeatureRequirement.all, FeatureRequirement.any) and *negate* (bool) options.
56
+
57
+ ```js
58
+ if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.all)) {
59
+ // ALL the provided feature keys are TRUE
60
+ }
61
+ ```
62
+
63
+ ```js
64
+ if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.any)) {
65
+ // AT LEAST ONE the provided feature keys is TRUE
66
+ }
67
+ ```
68
+
69
+ ```js
70
+ if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.all, true)) {
71
+ // ALL the provided feature keys are FALSE
72
+ }
73
+ ```
74
+
75
+ Lastly, you can set how often you would like to synchronize (re-fetch from Toggly) the feature flags values by setting the *.featureFlagsRefreshInterval when runnint *.init.
76
+
77
+ ```js
78
+ Toggly.init({
79
+ appKey: '<YOUR_APP_KEY>',
80
+ environment: '<YOUR_APP_ENVIRONMENT>',
81
+ featureFlagsRefreshInterval: 3 * 60 * 1000
82
+ })
83
+ .then(function () {
84
+ // Now you can check if a feature (or more) is Enabled/Disabled ...
85
+ });
86
+ ```
87
+
88
+ ## Basic Usage (without Toggly.io)
89
+
90
+ Initialize Toggly by running the Toggly.init method
91
+
92
+ ```js
93
+ var featureFlagsDefaults = {
94
+ "SignUpButton": true,
95
+ "DemoScreenshot": true
96
+ };
97
+
98
+ Toggly.init({ flagDefaults: featureFlagsDefaults }).then(function () {
99
+
100
+ // Now you can check if a feature (or more) is Enabled/Disabled
101
+
102
+ if (Toggly.isFeatureOn('SignUpButton')) {
103
+ // SignUpButton is ON
104
+ }
105
+
106
+ if (Toggly.isFeatureOff('DemoScreenshot')) {
107
+ // DemoScreenshot is OFF
108
+ }
109
+ });
110
+ ```
111
+
112
+ You can also check multiple feature keys and make use of the *requirement* (FeatureRequirement.all, FeatureRequirement.any) and *negate* (bool) options.
113
+
114
+ ```js
115
+ if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.all)) {
116
+ // ALL the provided feature keys are TRUE
117
+ }
118
+ ```
119
+
120
+ ```js
121
+ if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.any)) {
122
+ // AT LEAST ONE the provided feature keys is TRUE
123
+ }
124
+ ```
125
+
126
+ ```js
127
+ if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.all, true)) {
128
+ // ALL the provided feature keys are FALSE
129
+ }
130
+ ```
131
+
132
+ ## Find out more about Toggly.io
133
+
134
+ Visit [our official website](https://toggly.io) or [check out a video overview of our product](https://docs.toggly.io/).
Binary file
@@ -0,0 +1,65 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>Feature Flags Example</title>
9
+
10
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet"
11
+ integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
12
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
13
+ integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
14
+ crossorigin="anonymous"></script>
15
+ </head>
16
+
17
+ <body>
18
+ <div class="px-4 pt-5 my-5 text-center border-bottom">
19
+ <h1 class="display-5 fw-bold mb-5">What is a Feature Flag?</h1>
20
+ <div class="col-lg-6 mx-auto">
21
+ <p class="lead mb-4">A feature flag (or toggle) in software development provides an alternative to maintaining
22
+ multiple feature branches in source code. A condition within the code enables or disables a feature during
23
+ runtime.</p>
24
+ <p class="lead mb-4">In agile settings the feature flag is used in production, to switch on the feature on demand,
25
+ for some or all the users. Thus, feature flags make it easier to release often. Advanced roll out strategies
26
+ such as canary roll out and A/B testing are easier to handle.</p>
27
+ <div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
28
+ <a href="https://app.toggly.io/register" id="signUpButton" type="button"
29
+ class="d-none btn btn-primary btn-lg px-4 gap-3" style="background: #556ee6; border: #556ee6;">Sign up for
30
+ FREE</a>
31
+ <a href="https://toggly.io" type="button" class="btn btn-outline-secondary btn-lg px-4">Find out more on
32
+ Toggly.io</a>
33
+ </div>
34
+ <small class="d-block text-muted my-3">Note: You can use this library <strong>WITH</strong> or
35
+ <strong>WITHOUT</strong> Toggly.io</small>
36
+ </div>
37
+ <div id="demoScreenshot" class="overflow-hidden" style="max-height: 30vh;">
38
+ <div class="container px-5">
39
+ <img src="./demo-app-screenshot.png" class="img-fluid border rounded-3 shadow-lg mb-4" alt="Example image"
40
+ width="700" height="500" loading="lazy">
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <script src="../dist/feature-flags-toggly.bundle.js"></script>
46
+ <script>
47
+ var featureFlagsDefaults = {
48
+ "SignUpButton": true,
49
+ "DemoScreenshot": true
50
+ };
51
+
52
+ Toggly.init({ flagDefaults: featureFlagsDefaults }).then(function () {
53
+
54
+ // Now you can check if a feature (or more) is Enabled/Disabled
55
+ if (Toggly.isFeatureOn('SignUpButton')) {
56
+ document.querySelector('#signUpButton').classList.remove('d-none');
57
+ }
58
+ if (Toggly.isFeatureOff('DemoScreenshot')) {
59
+ document.querySelector('#demoScreenshot').classList.add('d-none');
60
+ }
61
+ });
62
+ </script>
63
+ </body>
64
+
65
+ </html>
Binary file
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'jsdom',
5
+ };
@@ -0,0 +1,4 @@
1
+ export enum FeatureRequirement {
2
+ all,
3
+ any
4
+ }
@@ -0,0 +1,4 @@
1
+ export * from './toggly-config';
2
+ export * from './toggly-init-response';
3
+ export * from './feature-requirement';
4
+ export * from './storage-keys';
@@ -0,0 +1,4 @@
1
+ export enum StorageKeys {
2
+ togglyIdentityKey,
3
+ togglyFeatureFlagsKey,
4
+ }
@@ -0,0 +1,11 @@
1
+ export interface TogglyConfig {
2
+ baseURI?: string;
3
+ reloadOnFeatureFlagValidation?: boolean;
4
+ connectTimeout?: number;
5
+ featureFlagsRefreshInterval?: number;
6
+ isDebug?: boolean;
7
+
8
+ appKey?: string;
9
+ environment?: string;
10
+ flagDefaults?: { [key: string]: boolean };
11
+ }
@@ -0,0 +1,14 @@
1
+ export enum TogglyLoadFeatureFlagsResponse {
2
+ fetched,
3
+ cached,
4
+ defaults,
5
+ error,
6
+ }
7
+
8
+ export class TogglyInitResponse {
9
+ status: TogglyLoadFeatureFlagsResponse;
10
+
11
+ constructor(status: TogglyLoadFeatureFlagsResponse) {
12
+ this.status = status;
13
+ }
14
+ }
package/lib/toggly.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { FeatureRequirement, StorageKeys, TogglyConfig } from './models';
3
+
4
+ export class Toggly {
5
+ private static _config: TogglyConfig;
6
+ private static _refreshInterval: number | undefined;
7
+
8
+ static init(config: TogglyConfig = {} as TogglyConfig): Promise<{ [key: string]: boolean }> {
9
+ Toggly._config = Object.assign({
10
+ baseURI: 'https://client.toggly.io',
11
+ reloadOnFeatureFlagValidation: false,
12
+ connectTimeout: 5 * 1000,
13
+ featureFlagsRefreshInterval: 3 * 60 * 1000,
14
+ isDebug: false,
15
+ environment: 'Production',
16
+ flagDefaults: {}
17
+ }, config);
18
+
19
+ if (!Toggly.identity) {
20
+ Toggly.identity = uuidv4();
21
+ }
22
+
23
+ Toggly.clearFeatureFlagsCache();
24
+ Toggly.startRefreshInterval();
25
+
26
+ return Toggly.refresh();
27
+ }
28
+
29
+ static get featureFlagsValue(): { [key: string]: boolean } {
30
+ var cachedFlags = JSON.parse(localStorage.getItem(StorageKeys.togglyFeatureFlagsKey.toString()) ?? null);
31
+ return Toggly._config?.appKey && cachedFlags ? cachedFlags : Toggly._config?.flagDefaults ?? {};
32
+ }
33
+
34
+ static get identity(): string {
35
+ return localStorage.getItem(StorageKeys.togglyIdentityKey.toString());
36
+ }
37
+
38
+ static set identity(v: string) {
39
+ localStorage.setItem(StorageKeys.togglyIdentityKey.toString(), v);
40
+ }
41
+
42
+ static clearIdentity() {
43
+ localStorage.removeItem(StorageKeys.togglyIdentityKey.toString());
44
+ }
45
+
46
+ private static get _cachedFeatureFlags(): { [key: string]: boolean } {
47
+ return JSON.parse(localStorage.getItem(StorageKeys.togglyFeatureFlagsKey.toString()) ?? null);
48
+ }
49
+
50
+ static cacheFeatureFlags(flags: { [key: string]: boolean }) {
51
+ localStorage.setItem(StorageKeys.togglyFeatureFlagsKey.toString(), JSON.stringify(flags));
52
+ }
53
+
54
+ static clearFeatureFlagsCache() {
55
+ localStorage.removeItem(StorageKeys.togglyFeatureFlagsKey.toString());
56
+ }
57
+
58
+ static fetchFeatureFlags(): Promise<{ [key: string]: boolean }> {
59
+ return new Promise((resolve, reject) => {
60
+ var url = `${Toggly._config.baseURI}/${Toggly._config.appKey}-${Toggly._config.environment}/defs`;
61
+
62
+ if (Toggly.identity) {
63
+ url += `?u=${Toggly.identity}`;
64
+ }
65
+
66
+ fetch(url)
67
+ .then((response) => response.json())
68
+ .then((flags) => {
69
+ // Cache flags on successful response
70
+ Toggly.cacheFeatureFlags(flags);
71
+ resolve(flags);
72
+
73
+ if (Toggly._config.isDebug) { console.log(`Toggly.fetchFeatureFlags - ${JSON.stringify(flags)}`); }
74
+ })
75
+ .catch((error) => {
76
+ // Try to use flags from cache, otherwise use provided default flags
77
+ var flags = Toggly._cachedFeatureFlags ?? Toggly._config.flagDefaults;
78
+ resolve(flags);
79
+
80
+ if (Toggly._config.isDebug) { console.log(`Toggly.loadedFromCache - ${JSON.stringify(flags)}`); }
81
+ });
82
+ });
83
+ }
84
+
85
+ static refresh(): Promise<{ [key: string]: boolean }> {
86
+ if (Toggly._config.isDebug) { console.log('Toggly.refresh'); }
87
+
88
+ // In case there is no API key provided, only the flag defaults shall be used
89
+ if (!Toggly._config.appKey) {
90
+ if (Toggly._config.isDebug) { console.log(`Toggly.usedFlagDefaults - ${JSON.stringify(Toggly._config.flagDefaults)}`); }
91
+
92
+ return new Promise((resolve, reject) => {
93
+ resolve(Toggly._config.flagDefaults);
94
+ });
95
+ }
96
+
97
+ // Try to fetch flags from the API
98
+ return Toggly.fetchFeatureFlags();
99
+ }
100
+
101
+ private static _evaluateFeatureGate(flags: { [key: string]: boolean } = {}, featureGate: string[], requirement: FeatureRequirement = FeatureRequirement.all, negate: boolean = false) {
102
+ var isEnabled: boolean;
103
+
104
+ if (requirement === FeatureRequirement.any) {
105
+ isEnabled = featureGate.reduce((isEnabled, featureKey) => {
106
+ return isEnabled ||
107
+ (flags[featureKey] && flags[featureKey] === true);
108
+ }, false);
109
+ } else {
110
+ isEnabled = featureGate.reduce((isEnabled, featureKey) => {
111
+ return isEnabled &&
112
+ (flags[featureKey] && flags[featureKey] === true);
113
+ }, true);
114
+ }
115
+
116
+ if (Toggly._config.isDebug) { console.log(`Toggly._evaluateFeatureGate - ${JSON.stringify(featureGate)}`); }
117
+
118
+ isEnabled = negate ? !isEnabled : isEnabled;
119
+
120
+ return isEnabled;
121
+ }
122
+
123
+ static evaluateFeatureGate(featureGate: string[], requirement: FeatureRequirement = FeatureRequirement.all, negate: boolean = false): boolean {
124
+ return Toggly._evaluateFeatureGate(Toggly.featureFlagsValue, featureGate, requirement, negate);
125
+ }
126
+
127
+ static isFeatureOn(featureKey: string): boolean {
128
+ return Toggly._evaluateFeatureGate(Toggly.featureFlagsValue, [featureKey]);
129
+ }
130
+
131
+ static isFeatureOff(featureKey: string): boolean {
132
+ return Toggly._evaluateFeatureGate(Toggly.featureFlagsValue, [featureKey], FeatureRequirement.all, true);
133
+ }
134
+
135
+ static cancelRefreshInterval() {
136
+ window.clearInterval(Toggly._refreshInterval);
137
+ Toggly._refreshInterval = undefined;
138
+ }
139
+
140
+ static startRefreshInterval() {
141
+ Toggly.cancelRefreshInterval();
142
+
143
+ if (Toggly._config.appKey && Toggly._config.featureFlagsRefreshInterval > 0) {
144
+ Toggly._refreshInterval = window.setInterval(() => Toggly.refresh(), Toggly._config.featureFlagsRefreshInterval);
145
+ }
146
+ }
147
+ }
148
+
149
+ (window as any).Toggly = Toggly;
150
+
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@ops-ai/feature-flags-toggly",
3
+ "version": "1.0.0",
4
+ "description": "Provides feature flags support for Javascript applications allowing you to enable and disable features easily. Can be used with or without Toggly.io.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "webpack",
8
+ "dev": "npx webpack -w",
9
+ "test": "npx jest"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/ops-ai/Toggly.FeatureManagement.git#develop"
14
+ },
15
+ "author": {
16
+ "name": "Cosmin Atomei",
17
+ "email": "cosmin.atomei@gmail.com"
18
+ },
19
+ "license": "BSD-3-Clause",
20
+ "bugs": {
21
+ "url": "https://github.com/ops-ai/Toggly.FeatureManagement/issues"
22
+ },
23
+ "homepage": "https://github.com/ops-ai/Toggly.FeatureManagement/tree/develop#readme",
24
+ "dependencies": {
25
+ "rxjs": "^7.5.7",
26
+ "uuid": "^9.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/jest": "^29.2.3",
30
+ "@types/uuid": "^8.3.4",
31
+ "jest": "^29.3.1",
32
+ "jest-environment-jsdom": "^29.3.1",
33
+ "ts-jest": "^29.0.3",
34
+ "ts-loader": "^9.4.1",
35
+ "typescript": "^4.9.3",
36
+ "webpack": "^5.75.0",
37
+ "webpack-cli": "^5.0.0"
38
+ }
39
+ }
@@ -0,0 +1,33 @@
1
+ import { FeatureRequirement } from '../lib/models';
2
+ import { Toggly } from '../lib/toggly';
3
+
4
+ beforeAll(() => {
5
+ return Toggly.init({
6
+ flagDefaults: {
7
+ "ExampleFeatureKey1": true,
8
+ "ExampleFeatureKey2": false
9
+ }
10
+ });
11
+ });
12
+
13
+ test('Check (isFeatureOn) result based on provided *.flagDefaults', () => {
14
+ expect(Toggly.isFeatureOn('ExampleFeatureKey1')).toBe(true);
15
+ expect(Toggly.isFeatureOn('ExampleFeatureKey2')).toBe(false);
16
+ });
17
+
18
+ test('Check (isFeatureOff) result based on provided *.flagDefaults', () => {
19
+ expect(Toggly.isFeatureOff('ExampleFeatureKey1')).toBe(false);
20
+ expect(Toggly.isFeatureOff('ExampleFeatureKey2')).toBe(true);
21
+ });
22
+
23
+ test('Check (evaluateFeatureGate, requirement: All) result based on provided *.flagDefaults', () => {
24
+ return expect(Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.all)).toBe(false);
25
+ });
26
+
27
+ test('Check (evaluateFeatureGate, requirement: Any) result based on provided *.flagDefaults', () => {
28
+ return expect(Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], FeatureRequirement.any)).toBe(true);
29
+ });
30
+
31
+ test('Check (evaluateFeatureGate, negate) result based on provided *.flagDefaults', () => {
32
+ return expect(Toggly.evaluateFeatureGate(['ExampleFeatureKey1'], FeatureRequirement.all, true)).toBe(false);
33
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["es2016", "DOM"],
5
+ "rootDir": "./",
6
+ "sourceMap": true
7
+ }
8
+ }
@@ -0,0 +1,23 @@
1
+ const path = require('path');
2
+
3
+ module.exports = {
4
+ mode: "production",
5
+ entry: {
6
+ main: "./lib/toggly.ts",
7
+ },
8
+ output: {
9
+ path: path.resolve(__dirname, './dist'),
10
+ filename: "feature-flags-toggly.bundle.js"
11
+ },
12
+ resolve: {
13
+ extensions: [".ts", ".tsx", ".js"],
14
+ },
15
+ module: {
16
+ rules: [
17
+ {
18
+ test: /\.tsx?$/,
19
+ loader: "ts-loader"
20
+ }
21
+ ]
22
+ }
23
+ };