@orangebeard-io/playwright-orangebeard-reporter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ <h1 align="center">
2
+ <a href="https://github.com/orangebeard-io/playwright-listener">
3
+ <img src="https://raw.githubusercontent.com/orangebeard-io/playwright-listener/main/.github/logo.svg" alt="Orangebeard.io Playwright Listener" height="200">
4
+ </a>
5
+ <br>Orangebeard.io Playwright Listener<br>
6
+ </h1>
7
+
8
+ <h4 align="center">Orangebeard listener for <a href="https://playwright.dev" target="_blank" rel="noopener">Playwright</a></h4>
9
+
10
+ <p align="center">
11
+ <a href="https://www.npmjs.com/package/@orangebeard-io/playwright-orangebeard-reporter">
12
+ <img src="https://img.shields.io/npm/v/@orangebeard-io/playwright-orangebeard-reporter.svg?style=flat-square"
13
+ alt="NPM Version" />
14
+ </a>
15
+ <a href="https://github.com/orangebeard-io/playwright-listener/actions">
16
+ <img src="https://img.shields.io/github/actions/workflow/status/orangebeard-io/playwright-listener/release.yml?branch=main&style=flat-square"
17
+ alt="Build Status" />
18
+ </a>
19
+ <a href="https://github.com/orangebeard-io/playwright-listener/blob/main/LICENSE">
20
+ <img src="https://img.shields.io/github/license/orangebeard-io/playwright-listener?style=flat-square"
21
+ alt="License" />
22
+ </a>
23
+ </p>
24
+
25
+ <div align="center">
26
+ <h4>
27
+ <a href="https://orangebeard.io">Orangebeard</a> |
28
+ <a href="#installation">Installation</a> |
29
+ <a href="#configuration">Configuration</a>
30
+ </h4>
31
+ </div>
32
+
33
+ ## Installation
34
+
35
+ ### Install the npm package
36
+
37
+ ```shell
38
+ npm install @orangebeard-io/playwright-orangebeard-reporter
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Create orangebeard.json (in your test projects's folder (or above))
44
+
45
+ ```JSON
46
+ {
47
+ "endpoint": "https://XXX.orangebeard.app",
48
+ "accessToken": "00000000-0000-0000-0000-00000000",
49
+ "project": "my_project_name",
50
+ "testset": "My Test Set Name",
51
+ "description": "A run from playwright",
52
+ "attributes": [
53
+ {
54
+ "key": "SomeKey",
55
+ "value": "SomeValue"
56
+ },
57
+ {
58
+ "value": "Tag value"
59
+ }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ Configure the reporter in playwright-config.ts:
65
+ ```ts
66
+ export default defineConfig({
67
+ testDir: './my-tests',
68
+ reporter: [['@orangebeard-io/playwright-orangebeard-reporter']],
69
+ projects: [
70
+ {
71
+ name: 'chromium',
72
+ use: { ...devices['Desktop Chrome'] },
73
+ }]
74
+ });
75
+ ```
76
+
77
+ ### Running
78
+
79
+ Run your tests as usual!
80
+
81
+ Alternatively, configure Orangebeard variables as ENV (without or on top of orangebeard.json):
82
+
83
+ ```shell
84
+ ORANGEBEARD_ENDPOINT=https://company.orangebeard.app
85
+ ORANGEBEARD_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
86
+ ORANGEBEARD_PROJECT="my project"
87
+ ORANGEBEARD_TESTSET="my test set"
88
+ ORANGEBEARD_DESCRIPTION="My awesome testrun"
89
+ ORANGEBEARD_ATTRIBUTES="key:value; value;"
90
+ ```
@@ -0,0 +1,29 @@
1
+ {{#each releases}}
2
+ {{#if summary}}
3
+ {{summary}}
4
+ {{/if}}
5
+
6
+ {{#if merges}}
7
+ ### :twisted_rightwards_arrows: Merged
8
+
9
+ {{#each merges}}
10
+ - {{#if commit.breaking}}**Breaking change:** {{/if}}{{message}} {{#if href}}[`#{{id}}`]({{href}}){{/if}}
11
+ {{/each}}
12
+ {{/if}}
13
+
14
+ {{#if fixes}}
15
+ ### :bug: Fixed
16
+
17
+ {{#each fixes}}
18
+ - {{#if commit.breaking}}**Breaking change:** {{/if}}{{commit.subject}}{{#each fixes}} {{#if href}}[`#{{id}}`]({{href}}){{/if}}{{/each}}
19
+ {{/each}}
20
+ {{/if}}
21
+
22
+ {{#commit-list commits heading='### :mag: Commits'}}
23
+ - {{#if breaking}}**Breaking change:** {{/if}}{{subject}} {{#if href}}[`{{shorthash}}`]({{href}}){{/if}}
24
+ {{/commit-list}}
25
+
26
+ {{#if href}}
27
+ See full comparison at [{{title}}]({{href}})
28
+ {{/if}}
29
+ {{/each}}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const OrangebeardReporter_1 = require("./reporter/OrangebeardReporter");
4
+ exports.default = OrangebeardReporter_1.OrangebeardReporter;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@orangebeard-io/playwright-orangebeard-reporter",
3
+ "version": "1.0.0",
4
+ "description": "A Playwright reporter to report to Orangebeard.io",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "rimraf ./dist && tsc",
8
+ "lint": "eslint . --ext .ts",
9
+ "format:js": "npm run lint -- --fix",
10
+ "format:md": "prettier --write README.md",
11
+ "format": "npm run format:js && npm run format:md",
12
+ "test": "jest --unhandled-rejections=none --config ./jest.config.js",
13
+ "test:coverage": "jest --unhandled-rejections=none --coverage",
14
+ "get-version": "echo $npm_package_version",
15
+ "update-version": "release-it --ci --no-git --no-npm.publish",
16
+ "create-changelog": "auto-changelog --template changelog-template.hbs --starting-version v$npm_package_version"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/orangebeard-io/playwright-listener.git"
21
+ },
22
+ "keywords": [
23
+ "playwright",
24
+ "orangebeard",
25
+ "reporter",
26
+ "testing"
27
+ ],
28
+ "author": "Orangebeard.io",
29
+ "license": "Apache-2.0",
30
+ "bugs": {
31
+ "url": "https://github.com/orangebeard-io/playwright-listener/issues"
32
+ },
33
+ "homepage": "https://github.com/orangebeard-io/playwright-listener#readme",
34
+ "devDependencies": {
35
+ "auto-changelog": "^2.5.0",
36
+ "@js-joda/core": "^5.6.3",
37
+ "@orangebeard-io/javascript-client": "^2.0.6",
38
+ "@playwright/test": "^1.49.0",
39
+ "@typescript-eslint/parser": "^8.15.0",
40
+ "eslint": "^9.15.0",
41
+ "eslint-config-prettier": "^9.1.0",
42
+ "eslint-plugin-prettier": "^5.2.1",
43
+ "prettier": "^3.3.3",
44
+ "rimraf": "^6.0.1",
45
+ "typescript": "^5.6.3"
46
+ }
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import {OrangebeardReporter} from "./reporter/OrangebeardReporter";
2
+
3
+ export default OrangebeardReporter;
@@ -0,0 +1,265 @@
1
+ import {UUID} from 'crypto';
2
+ import {Reporter, TestCase, TestResult, TestStep} from '@playwright/test/reporter'
3
+ import {ansiToMarkdown, getBytes, getCodeSnippet, getTime, removeAnsi, testStatusMap} from './utils'
4
+ import {OrangebeardParameters} from "@orangebeard-io/javascript-client/dist/client/models/OrangebeardParameters";
5
+ import OrangebeardAsyncV3Client from "@orangebeard-io/javascript-client/dist/client/OrangebeardAsyncV3Client";
6
+ import {StartTest} from "@orangebeard-io/javascript-client/dist/client/models/StartTest";
7
+ import {Attachment} from "@orangebeard-io/javascript-client/dist/client/models/Attachment";
8
+ import {Log} from "@orangebeard-io/javascript-client/dist/client/models/Log";
9
+ import {Attribute} from "@orangebeard-io/javascript-client/dist/client/models/Attribute";
10
+ import {FinishStep} from "@orangebeard-io/javascript-client/dist/client/models/FinishStep";
11
+ import TestType = StartTest.TestType;
12
+ import LogFormat = Log.LogFormat;
13
+ import LogLevel = Log.LogLevel;
14
+ import Status = FinishStep.Status;
15
+ import * as path from "node:path";
16
+
17
+ export class OrangebeardReporter implements Reporter {
18
+
19
+ config: OrangebeardParameters;
20
+ client: OrangebeardAsyncV3Client;
21
+
22
+ //CONTEXT TRACKING
23
+ testRunId: UUID;
24
+ suites: Map<string, UUID> = new Map<string, UUID>(); //suiteNames , uuid
25
+ tests: Map<string, UUID> = new Map<string, UUID>(); //testId, uuid
26
+ steps: Map<string, UUID> = new Map<string, UUID>(); //testId_stepPath, uuid
27
+ promises: Promise<void>[] = [];
28
+
29
+ constructor() {
30
+ this.client = new OrangebeardAsyncV3Client();
31
+ this.config = this.client.config;
32
+ }
33
+
34
+ onBegin(): void {
35
+ this.testRunId = this.client.startTestRun({
36
+ testSetName: this.config.testset,
37
+ description: this.config.description,
38
+ startTime: getTime(),
39
+ attributes: this.config.attributes
40
+ })
41
+ }
42
+
43
+ async onEnd(): Promise<void> {
44
+ await Promise.all(this.promises)
45
+ return this.client.finishTestRun(this.testRunId, {endTime: getTime()})
46
+ }
47
+
48
+ onStdErr(chunk: string | Buffer, test: void | TestCase, _result: void | TestResult): void {
49
+ //log error level
50
+
51
+ if (typeof test === 'object' && test !== null) {
52
+ const testUUID = this.tests.get(test.id);
53
+ const message = chunk.toString();
54
+ this.client.log({
55
+ logFormat: LogFormat.PLAIN_TEXT,
56
+ logLevel: LogLevel.ERROR,
57
+ logTime: getTime(),
58
+ message: message,
59
+ testRunUUID: this.testRunId,
60
+ testUUID: testUUID
61
+ });
62
+ }
63
+ }
64
+
65
+ onStdOut(chunk: string | Buffer, test: void | TestCase, _result: void | TestResult): void {
66
+ if (typeof test === 'object' && test !== null) {
67
+ const testUUID = this.tests.get(test.id);
68
+ const message = chunk.toString();
69
+ this.client.log({
70
+ logFormat: LogFormat.PLAIN_TEXT,
71
+ logLevel: LogLevel.INFO,
72
+ logTime: getTime(),
73
+ message: message,
74
+ testRunUUID: this.testRunId,
75
+ testUUID: testUUID
76
+ });
77
+ }
78
+ }
79
+
80
+ onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {
81
+ //start step
82
+ const testUUID = this.tests.get(test.id);
83
+
84
+ const stepUUID = this.client.startStep({
85
+ startTime: getTime(),
86
+ stepName: step.title,
87
+ description: step.location ? `${path.basename(step.location.file)}:${step.location.line}`: undefined,
88
+ testRunUUID: this.testRunId,
89
+ testUUID: testUUID,
90
+ parentStepUUID: step.parent ? this.steps.get(test.id + "|" + step.parent.titlePath()) : undefined,
91
+ })
92
+ this.steps.set(test.id + "|" + step.titlePath(), stepUUID)
93
+
94
+ if(step.location) {
95
+ this.client.log({
96
+ logFormat: LogFormat.MARKDOWN,
97
+ logLevel: LogLevel.INFO,
98
+ logTime: getTime(),
99
+ message: getCodeSnippet(step.location.file, step.location.line),
100
+ testRunUUID: this.testRunId,
101
+ testUUID: testUUID,
102
+ stepUUID: stepUUID
103
+ });
104
+ }
105
+ }
106
+
107
+ onStepEnd(test: TestCase, _result: TestResult, step: TestStep): void {
108
+ const testUUID = this.tests.get(test.id);
109
+ const stepUUID = this.steps.get(test.id + "|" + step.titlePath())
110
+ if(step.error) {
111
+ const message = step.error.message;
112
+ this.client.log({
113
+ logFormat: LogFormat.MARKDOWN,
114
+ logLevel: LogLevel.ERROR,
115
+ logTime: getTime(),
116
+ message: ansiToMarkdown(message),
117
+ testRunUUID: this.testRunId,
118
+ testUUID: testUUID,
119
+ stepUUID: stepUUID
120
+ });
121
+
122
+ if (step.error.snippet) {
123
+ this.client.log({
124
+ logFormat: LogFormat.MARKDOWN,
125
+ logLevel: LogLevel.ERROR,
126
+ logTime: getTime(),
127
+ message: `\`\`\`js\n${removeAnsi(step.error.snippet)}\n\`\`\``,
128
+ testRunUUID: this.testRunId,
129
+ testUUID: testUUID,
130
+ stepUUID: stepUUID
131
+ });
132
+ }
133
+ }
134
+
135
+ this.client.finishStep(this.steps.get(test.id + "|" + step.titlePath()), {
136
+ endTime: getTime(),
137
+ status: step.error ? Status.FAILED : Status.PASSED,
138
+ testRunUUID: this.testRunId
139
+ })
140
+ this.steps.delete(test.id + "|" + step.titlePath())
141
+ }
142
+
143
+ onTestBegin(test: TestCase): void {
144
+ //check suite
145
+ const suiteUUID = this.getOrStartSuite(test.parent.titlePath())
146
+ const attributes: Array<Attribute> = [];
147
+ for (const tag of test.tags) {
148
+ attributes.push({value: tag})
149
+ }
150
+ const testUUID = this.client.startTest({
151
+ testType: TestType.TEST,
152
+ testRunUUID: this.testRunId,
153
+ suiteUUID: suiteUUID,
154
+ testName: test.title,
155
+ startTime: getTime(),
156
+ description: this.getTestDescription(test),
157
+ attributes: attributes
158
+ });
159
+ this.tests.set(test.id, testUUID);
160
+ }
161
+
162
+ async onTestEnd(test: TestCase, result: TestResult): Promise<void> {
163
+ const testUUID = this.tests.get(test.id);
164
+ if (result.attachments.length > 0) {
165
+ let message = "";
166
+ for (const attachment of result.attachments) {
167
+ message += `- ${attachment.name} (${attachment.contentType})\n`
168
+ }
169
+ const attachmentsLogUUID = this.client.log({
170
+ logFormat: LogFormat.MARKDOWN,
171
+ logLevel: LogLevel.INFO,
172
+ logTime: getTime(),
173
+ message: message,
174
+ testRunUUID: this.testRunId,
175
+ testUUID: testUUID
176
+ })
177
+ for (const attachment of result.attachments) {
178
+ this.promises.push(this.logAttachment(attachment, testUUID, attachmentsLogUUID));
179
+ }
180
+ }
181
+
182
+ //determine status
183
+ const status = testStatusMap[result.status]
184
+
185
+ //finish test
186
+ this.client.finishTest(testUUID, {
187
+ testRunUUID: this.testRunId,
188
+ status: status,
189
+ endTime: getTime()
190
+ });
191
+ this.tests.delete(test.id);
192
+ }
193
+
194
+ printsToStdio(): boolean {
195
+ return false;
196
+ }
197
+
198
+ private getOrStartSuite(suitePath: Array<string>): UUID {
199
+ const filteredSuitePath = suitePath.filter(name => name !== "");
200
+ let currentPath: Array<string> = [];
201
+ let parentSuiteUUID: UUID | undefined = undefined;
202
+
203
+ for (const suiteName of filteredSuitePath) {
204
+ currentPath.push(suiteName);
205
+ const existingSuiteUUID = this.suites.get(currentPath.join('|'));
206
+
207
+ if (existingSuiteUUID) {
208
+ parentSuiteUUID = existingSuiteUUID;
209
+ } else {
210
+ const newSuitesUUIDs = this.client.startSuite({
211
+ testRunUUID: this.testRunId,
212
+ parentSuiteUUID: parentSuiteUUID,
213
+ suiteNames: [suiteName],
214
+ });
215
+
216
+ if (newSuitesUUIDs && newSuitesUUIDs.length > 0) {
217
+ parentSuiteUUID = newSuitesUUIDs[0];
218
+ this.suites.set(currentPath.join('|'), parentSuiteUUID);
219
+ } else {
220
+ console.error(`Failed to create suite for path: ${currentPath.join(' > ')}`);
221
+ }
222
+ }
223
+ }
224
+ return parentSuiteUUID as UUID;
225
+ }
226
+
227
+ private getTestDescription(test: TestCase): string {
228
+ let description = `${path.basename(test.location.file)}:${test.location.line}\n`;
229
+ for (const annotation of test.annotations) {
230
+ description = `${description + annotation.type}: ${annotation.description}\n`
231
+ }
232
+ return description;
233
+ }
234
+
235
+ private async logAttachment(attachment: {
236
+ name: string,
237
+ path?: string,
238
+ body?: Buffer,
239
+ contentType: string
240
+ }, testUUID: UUID, logUUID: UUID) {
241
+ let content: Buffer;
242
+ if (attachment.body) {
243
+ content = attachment.body;
244
+ } else if (attachment.path) {
245
+ content = await getBytes(attachment.path);
246
+ } else {
247
+ throw new Error("Attachment must have either body or path defined.");
248
+ }
249
+
250
+ const orangebeardAttachment: Attachment = {
251
+ file: {
252
+ name: path.basename(attachment.path),
253
+ content: content,
254
+ contentType: attachment.contentType,
255
+ },
256
+ metaData: {
257
+ testRunUUID: this.testRunId,
258
+ testUUID: testUUID,
259
+ logUUID: logUUID,
260
+ attachmentTime: getTime()
261
+ },
262
+ };
263
+ this.client.sendAttachment(orangebeardAttachment);
264
+ }
265
+ }
@@ -0,0 +1,160 @@
1
+ import {ZonedDateTime} from "@js-joda/core";
2
+ import {FinishTest} from "@orangebeard-io/javascript-client/dist/client/models/FinishTest";
3
+ import * as fs from "node:fs";
4
+ import {promisify} from "util";
5
+ import Status = FinishTest.Status;
6
+
7
+ const stat = promisify(fs.stat);
8
+ const access = promisify(fs.access);
9
+
10
+ export function getTime() {
11
+ return ZonedDateTime.now().withFixedOffsetZone().toString();
12
+ }
13
+
14
+ export const testStatusMap = {
15
+ "passed": Status.PASSED,
16
+ "failed": Status.FAILED,
17
+ "timedOut": Status.TIMED_OUT,
18
+ "skipped": Status.SKIPPED,
19
+ "interrupted": Status.STOPPED
20
+ };
21
+
22
+ export function removeAnsi(ansiString: string): string {
23
+ const parts = ansiString.split(/(\u001b\[[0-9;]*[mG])/);
24
+ let result = "";
25
+ for (const part of parts) {
26
+ if (!part.startsWith("\u001b[")) {
27
+ result += part;
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+
33
+ export function ansiToMarkdown(ansiString: string): string {
34
+ let markdown = "";
35
+ let currentStyle: { italic?: boolean, code?: boolean } = {};
36
+
37
+ const ansiCodes = {
38
+ "31": {italic: true},
39
+ "32": {italic: true},
40
+ "39": {italic: false}, // Reset styles
41
+ "2": {code: true},
42
+ "22": {code: false},
43
+ };
44
+
45
+ const parts = ansiString.split(/(\u001b\[[0-9;]*[mG])/);
46
+
47
+ for (const part of parts) {
48
+ if (part.startsWith("\u001b[")) {
49
+ const code = part.slice(2, -1);
50
+ const codes = code.split(';');
51
+ for (const c of codes) {
52
+ const style = ansiCodes[c as keyof typeof ansiCodes]; // Type guard
53
+ if (style) {
54
+ currentStyle = {...currentStyle, ...style};
55
+ }
56
+ }
57
+ } else {
58
+ let formattedPart = part.replace(/\n/g, " \n");
59
+
60
+ if (currentStyle.italic) {
61
+ formattedPart = formattedPart.endsWith(" ") ? `*${formattedPart.trim()}* ` : `*${formattedPart}*`;
62
+
63
+ }
64
+ if (currentStyle.code) {
65
+ formattedPart = `${formattedPart}`;
66
+ }
67
+
68
+ markdown += formattedPart
69
+
70
+ }
71
+ }
72
+
73
+ return markdown;
74
+ }
75
+
76
+ /**
77
+ * Reads a 3-line snippet from a file, centered around the specified line number.
78
+ *
79
+ * @param filePath - The path to the file.
80
+ * @param lineNumber - The line number to center the snippet around (1-based index).
81
+ * @returns A promise that resolves with the 3-line snippet or an error message if the line is out of range.
82
+ */
83
+ export /**
84
+ * Reads a 3-line snippet from a file, centered around the specified line number.
85
+ *
86
+ * @param filePath - The path to the file.
87
+ * @param lineNumber - The line number to center the snippet around (1-based index).
88
+ * @returns The 3-line snippet or an error message if the line is out of range.
89
+ */
90
+ function getCodeSnippet(filePath: string, lineNumber: number): string {
91
+ if (lineNumber < 1) {
92
+ throw new Error('Line number must be 1 or greater.');
93
+ }
94
+
95
+ const fileContent = fs.readFileSync(filePath, 'utf8');
96
+ const lines = fileContent.split(/\r?\n/); // Support both Unix and Windows line endings
97
+
98
+ const startLine = Math.max(0, lineNumber - 2); // Zero-based index for one line before
99
+ const endLine = Math.min(lines.length, lineNumber + 1); // One line after
100
+
101
+ if (startLine >= lines.length) {
102
+ throw new Error('Line number is out of range.');
103
+ }
104
+
105
+ let snippet = lines.slice(startLine, endLine);
106
+ if (snippet.length > 0 && snippet[0].trim() === "") {
107
+ snippet = snippet.slice(1);
108
+ }
109
+
110
+ return `\`\`\`js\n${snippet.join('\n')}\n\`\`\``;
111
+ }
112
+
113
+ const fileExists = async (filepath: string) => {
114
+ try {
115
+ await access(filepath, fs.constants.F_OK);
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ };
121
+
122
+ const waitForFile = async (filepath: string, interval = 1000, timeout = 60000) => {
123
+ const start = Date.now();
124
+
125
+ while (true) {
126
+ const now = Date.now();
127
+ if (now - start > timeout) {
128
+ throw new Error(`Timeout: ${filepath} did not become available within ${timeout}ms`);
129
+ }
130
+
131
+ if (await fileExists(filepath)) {
132
+ const stats = [];
133
+ for (let i = 0; i < 2; i++) {
134
+ stats.push(await stat(filepath));
135
+ await new Promise((resolve) => setTimeout(resolve, interval));
136
+ }
137
+
138
+ const [first, second] = stats;
139
+ if (
140
+ first.mtimeMs === second.mtimeMs &&
141
+ first.size === second.size
142
+ ) {
143
+ return;
144
+ }
145
+ }
146
+
147
+ await new Promise((resolve) => setTimeout(resolve, interval));
148
+ }
149
+ };
150
+
151
+ export const getBytes = async (filePath: string) => {
152
+ try {
153
+ await waitForFile(filePath, 100, 5000)
154
+ return fs.readFileSync(filePath);
155
+ } catch (err) {
156
+ console.error('Error reading file:', err);
157
+ throw err;
158
+ }
159
+ };
160
+
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es6",
4
+ "module": "commonjs",
5
+ "allowJs": false,
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "noImplicitAny": true,
9
+ "esModuleInterop": true,
10
+ "resolveJsonModule": true,
11
+ "moduleResolution": "node",
12
+ "baseUrl": ".",
13
+ "paths": {
14
+ "*": ["node_modules/*"]
15
+ },
16
+ "typeRoots": ["./node_modules/@types"]
17
+ },
18
+ "include": ["./src/**/*"],
19
+ "exclude": ["node_modules"],
20
+ }