@govtechsg/oobee 0.10.20

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.
Files changed (123) hide show
  1. package/.dockerignore +22 -0
  2. package/.github/pull_request_template.md +11 -0
  3. package/.github/workflows/docker-test.yml +54 -0
  4. package/.github/workflows/image.yml +107 -0
  5. package/.github/workflows/publish.yml +18 -0
  6. package/.idea/modules.xml +8 -0
  7. package/.idea/purple-a11y.iml +9 -0
  8. package/.idea/vcs.xml +6 -0
  9. package/.prettierrc.json +12 -0
  10. package/.vscode/extensions.json +5 -0
  11. package/.vscode/settings.json +10 -0
  12. package/CODE_OF_CONDUCT.md +128 -0
  13. package/DETAILS.md +163 -0
  14. package/Dockerfile +60 -0
  15. package/INSTALLATION.md +146 -0
  16. package/INTEGRATION.md +785 -0
  17. package/LICENSE +22 -0
  18. package/README.md +587 -0
  19. package/SECURITY.md +5 -0
  20. package/__mocks__/mock-report.html +1431 -0
  21. package/__mocks__/mockFunctions.ts +32 -0
  22. package/__mocks__/mockIssues.ts +64 -0
  23. package/__mocks__/mock_all_issues/000000001.json +64 -0
  24. package/__mocks__/mock_all_issues/000000002.json +53 -0
  25. package/__mocks__/mock_all_issues/fake-file.txt +0 -0
  26. package/__tests__/logs.test.ts +25 -0
  27. package/__tests__/mergeAxeResults.test.ts +278 -0
  28. package/__tests__/utils.test.ts +118 -0
  29. package/a11y-scan-results.zip +0 -0
  30. package/eslint.config.js +53 -0
  31. package/exclusions.txt +2 -0
  32. package/gitlab-pipeline-template.yml +54 -0
  33. package/jest.config.js +1 -0
  34. package/package.json +96 -0
  35. package/scripts/copyFiles.js +44 -0
  36. package/scripts/install_oobee_dependencies.cmd +13 -0
  37. package/scripts/install_oobee_dependencies.command +101 -0
  38. package/scripts/install_oobee_dependencies.ps1 +110 -0
  39. package/scripts/oobee_shell.cmd +13 -0
  40. package/scripts/oobee_shell.command +11 -0
  41. package/scripts/oobee_shell.sh +55 -0
  42. package/scripts/oobee_shell_ps.ps1 +54 -0
  43. package/src/cli.ts +401 -0
  44. package/src/combine.ts +240 -0
  45. package/src/constants/__tests__/common.test.ts +44 -0
  46. package/src/constants/cliFunctions.ts +305 -0
  47. package/src/constants/common.ts +1840 -0
  48. package/src/constants/constants.ts +443 -0
  49. package/src/constants/errorMeta.json +319 -0
  50. package/src/constants/itemTypeDescription.ts +11 -0
  51. package/src/constants/oobeeAi.ts +141 -0
  52. package/src/constants/questions.ts +181 -0
  53. package/src/constants/sampleData.ts +187 -0
  54. package/src/crawlers/__tests__/commonCrawlerFunc.test.ts +51 -0
  55. package/src/crawlers/commonCrawlerFunc.ts +656 -0
  56. package/src/crawlers/crawlDomain.ts +877 -0
  57. package/src/crawlers/crawlIntelligentSitemap.ts +156 -0
  58. package/src/crawlers/crawlLocalFile.ts +193 -0
  59. package/src/crawlers/crawlSitemap.ts +356 -0
  60. package/src/crawlers/custom/extractAndGradeText.ts +57 -0
  61. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +964 -0
  62. package/src/crawlers/custom/utils.ts +486 -0
  63. package/src/crawlers/customAxeFunctions.ts +82 -0
  64. package/src/crawlers/pdfScanFunc.ts +468 -0
  65. package/src/crawlers/runCustom.ts +117 -0
  66. package/src/index.ts +173 -0
  67. package/src/logs.ts +66 -0
  68. package/src/mergeAxeResults.ts +964 -0
  69. package/src/npmIndex.ts +284 -0
  70. package/src/screenshotFunc/htmlScreenshotFunc.ts +411 -0
  71. package/src/screenshotFunc/pdfScreenshotFunc.ts +762 -0
  72. package/src/static/ejs/partials/components/categorySelector.ejs +4 -0
  73. package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +57 -0
  74. package/src/static/ejs/partials/components/pagesScannedModal.ejs +70 -0
  75. package/src/static/ejs/partials/components/reportSearch.ejs +47 -0
  76. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +105 -0
  77. package/src/static/ejs/partials/components/scanAbout.ejs +263 -0
  78. package/src/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
  79. package/src/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
  80. package/src/static/ejs/partials/components/summaryScanResults.ejs +16 -0
  81. package/src/static/ejs/partials/components/summaryTable.ejs +20 -0
  82. package/src/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
  83. package/src/static/ejs/partials/components/topFive.ejs +6 -0
  84. package/src/static/ejs/partials/components/wcagCompliance.ejs +70 -0
  85. package/src/static/ejs/partials/footer.ejs +21 -0
  86. package/src/static/ejs/partials/header.ejs +230 -0
  87. package/src/static/ejs/partials/main.ejs +40 -0
  88. package/src/static/ejs/partials/scripts/bootstrap.ejs +8 -0
  89. package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +190 -0
  90. package/src/static/ejs/partials/scripts/categorySummary.ejs +141 -0
  91. package/src/static/ejs/partials/scripts/highlightjs.ejs +335 -0
  92. package/src/static/ejs/partials/scripts/popper.ejs +7 -0
  93. package/src/static/ejs/partials/scripts/reportSearch.ejs +248 -0
  94. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +801 -0
  95. package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +71 -0
  96. package/src/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
  97. package/src/static/ejs/partials/scripts/summaryTable.ejs +78 -0
  98. package/src/static/ejs/partials/scripts/utils.ejs +441 -0
  99. package/src/static/ejs/partials/styles/bootstrap.ejs +12375 -0
  100. package/src/static/ejs/partials/styles/highlightjs.ejs +54 -0
  101. package/src/static/ejs/partials/styles/styles.ejs +1843 -0
  102. package/src/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
  103. package/src/static/ejs/partials/summaryHeader.ejs +70 -0
  104. package/src/static/ejs/partials/summaryMain.ejs +75 -0
  105. package/src/static/ejs/report.ejs +420 -0
  106. package/src/static/ejs/summary.ejs +47 -0
  107. package/src/static/mustache/.prettierrc +4 -0
  108. package/src/static/mustache/Attention Deficit.mustache +11 -0
  109. package/src/static/mustache/Blind.mustache +11 -0
  110. package/src/static/mustache/Cognitive.mustache +7 -0
  111. package/src/static/mustache/Colorblindness.mustache +20 -0
  112. package/src/static/mustache/Deaf.mustache +12 -0
  113. package/src/static/mustache/Deafblind.mustache +7 -0
  114. package/src/static/mustache/Dyslexia.mustache +14 -0
  115. package/src/static/mustache/Low Vision.mustache +7 -0
  116. package/src/static/mustache/Mobility.mustache +15 -0
  117. package/src/static/mustache/Sighted Keyboard Users.mustache +42 -0
  118. package/src/static/mustache/report.mustache +1709 -0
  119. package/src/types/print-message.d.ts +28 -0
  120. package/src/types/types.ts +46 -0
  121. package/src/types/xpath-to-css.d.ts +3 -0
  122. package/src/utils.ts +332 -0
  123. package/tsconfig.json +15 -0
@@ -0,0 +1,28 @@
1
+ declare module 'print-message' {
2
+ interface PrintConfig {
3
+ border?: boolean;
4
+ border?: boolean; // Enable border
5
+ color?: string; // Default text color from console
6
+ borderColor?: string; // Border color is yellow
7
+ borderSymbol?: string; // Symbol for top/bottom borders
8
+ sideSymbol?: string; // Symbol for left/right borders
9
+ leftTopSymbol?: string; // Symbol that uses for left top corner
10
+ leftBottomSymbol?: string; // Symbol that uses for left bottom corner
11
+ rightTopSymbol?: string; // Symbol that uses for right top corner
12
+ rightBottomSymbol?: string; // Symbol that uses for right bottom corner
13
+ marginTop?: number; // Margin before border begins
14
+ marginBottom?: number; // Margin after border ends
15
+ paddingTop?: number; // Padding after border begins
16
+ paddingBottom?: number; // Padding before border ends
17
+ }
18
+
19
+ /**
20
+ * Print messages to console.
21
+ *
22
+ * @param lines Array of lines
23
+ * @param config Additional params for print
24
+ */
25
+ function print(lines: string[], config?: PrintConfig): void;
26
+
27
+ export = print;
28
+ }
@@ -0,0 +1,46 @@
1
+ import { ScannerTypes, UrlsCrawled } from '../constants/constants.js';
2
+
3
+ export type ViewportSize = {
4
+ width: number;
5
+ height: number;
6
+ };
7
+
8
+ export interface IBboxLocation {
9
+ // page: number;
10
+ location: string;
11
+ isVisible?: boolean;
12
+ groupId?: string;
13
+ bboxTitle?: string;
14
+ }
15
+
16
+ export type StructureTree = {
17
+ name: string;
18
+ children: StructureTree[] | StructureTree;
19
+ ref: { num: number; gen: number };
20
+ mcid?: number;
21
+ pageIndex?: number;
22
+ };
23
+
24
+ type DeviceDescriptor = {
25
+ viewport: ViewportSize;
26
+ userAgent: string;
27
+ deviceScaleFactor: number;
28
+ isMobile: boolean;
29
+ hasTouch: boolean;
30
+ defaultBrowserType: 'chromium' | 'firefox' | 'webkit';
31
+ };
32
+
33
+ export type viewportSettings = {
34
+ deviceChosen: string;
35
+ customDevice: string;
36
+ viewportWidth: number;
37
+ playwrightDeviceDetailsObject: DeviceDescriptor;
38
+ };
39
+
40
+ export type ScanDetails = {
41
+ startTime: Date;
42
+ endTime: Date;
43
+ crawlType: ScannerTypes;
44
+ requestUrl: URL;
45
+ urlsCrawled: UrlsCrawled;
46
+ };
@@ -0,0 +1,3 @@
1
+ declare module 'xpath-to-css' {
2
+ export default function xPathToCss(xPath: string): string;
3
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,332 @@
1
+ import { execSync, spawnSync } from 'child_process';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs-extra';
5
+ import constants, {
6
+ BrowserTypes,
7
+ destinationPath,
8
+ getIntermediateScreenshotsPath,
9
+ } from './constants/constants.js';
10
+ import { silentLogger } from './logs.js';
11
+
12
+ export const getVersion = () => {
13
+ const loadJSON = filePath =>
14
+ JSON.parse(fs.readFileSync(new URL(filePath, import.meta.url)).toString());
15
+ const versionNum = loadJSON('../package.json').version;
16
+
17
+ return versionNum;
18
+ };
19
+
20
+ export const getHost = url => new URL(url).host;
21
+
22
+ export const getCurrentDate = () => {
23
+ const date = new Date();
24
+ return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
25
+ };
26
+
27
+ export const isWhitelistedContentType = contentType => {
28
+ const whitelist = ['text/html'];
29
+ return whitelist.filter(type => contentType.trim().startsWith(type)).length === 1;
30
+ };
31
+
32
+ export const getStoragePath = (randomToken: string): string => {
33
+ if (process.env.OOBEE_VERBOSE_STORAGE_PATH) {
34
+ return `${process.env.OOBEE_VERBOSE_STORAGE_PATH}/${randomToken}`;
35
+ }
36
+ if (constants.exportDirectory === process.cwd()) {
37
+ return `results/${randomToken}`;
38
+ }
39
+ if (!path.isAbsolute(constants.exportDirectory)) {
40
+ constants.exportDirectory = path.resolve(process.cwd(), constants.exportDirectory);
41
+ }
42
+ return `${constants.exportDirectory}/${randomToken}`;
43
+ };
44
+
45
+ export const createDetailsAndLogs = async randomToken => {
46
+ const storagePath = getStoragePath(randomToken);
47
+ const logPath = `logs/${randomToken}`;
48
+ try {
49
+ await fs.ensureDir(storagePath);
50
+
51
+ // update logs
52
+ await fs.ensureDir(logPath);
53
+ await fs.pathExists('errors.txt').then(async exists => {
54
+ if (exists) {
55
+ try {
56
+ await fs.copy('errors.txt', `${logPath}/${randomToken}.txt`);
57
+ } catch (error) {
58
+ if (error.code === 'EBUSY') {
59
+ console.log(
60
+ `Unable to copy the file from 'errors.txt' to '${logPath}/${randomToken}.txt' because it is currently in use.`,
61
+ );
62
+ console.log(
63
+ 'Please close any applications that might be using this file and try again.',
64
+ );
65
+ } else {
66
+ console.log(`An unexpected error occurred while copying the file: ${error.message}`);
67
+ }
68
+ }
69
+ }
70
+ });
71
+ } catch (error) {
72
+ console.log(`An error occurred while setting up storage or log directories: ${error.message}`);
73
+ }
74
+ };
75
+
76
+ export const getUserDataFilePath = () => {
77
+ const platform = os.platform();
78
+ if (platform === 'win32') {
79
+ return path.join(process.env.APPDATA, 'Oobee', 'userData.txt');
80
+ }
81
+ if (platform === 'darwin') {
82
+ return path.join(process.env.HOME, 'Library', 'Application Support', 'Oobee', 'userData.txt');
83
+ }
84
+ // linux and other OS
85
+ return path.join(process.env.HOME, '.config', 'oobee', 'userData.txt');
86
+ };
87
+
88
+ export const getUserDataTxt = () => {
89
+ const textFilePath = getUserDataFilePath();
90
+
91
+ // check if textFilePath exists
92
+ if (fs.existsSync(textFilePath)) {
93
+ const userData = JSON.parse(fs.readFileSync(textFilePath, 'utf8'));
94
+ return userData;
95
+ }
96
+ return null;
97
+ };
98
+
99
+ export const writeToUserDataTxt = async (key, value) => {
100
+ const textFilePath = getUserDataFilePath();
101
+
102
+ // Create file if it doesn't exist
103
+ if (fs.existsSync(textFilePath)) {
104
+ const userData = JSON.parse(fs.readFileSync(textFilePath, 'utf8'));
105
+ userData[key] = value;
106
+ fs.writeFileSync(textFilePath, JSON.stringify(userData, null, 2));
107
+ } else {
108
+ const textFilePathDir = path.dirname(textFilePath);
109
+ if (!fs.existsSync(textFilePathDir)) {
110
+ fs.mkdirSync(textFilePathDir, { recursive: true });
111
+ }
112
+ fs.appendFileSync(textFilePath, JSON.stringify({ [key]: value }, null, 2));
113
+ }
114
+ };
115
+
116
+ export const createAndUpdateResultsFolders = async randomToken => {
117
+ const storagePath = getStoragePath(randomToken);
118
+ await fs.ensureDir(`${storagePath}`);
119
+
120
+ const intermediatePdfResultsPath = `${randomToken}/${constants.pdfScanResultFileName}`;
121
+
122
+ const transferResults = async (intermPath, resultFile) => {
123
+ try {
124
+ if (fs.existsSync(intermPath)) {
125
+ await fs.copy(intermPath, `${storagePath}/${resultFile}`);
126
+ }
127
+ } catch (error) {
128
+ if (error.code === 'EBUSY') {
129
+ console.log(
130
+ `Unable to copy the file from ${intermPath} to ${storagePath}/${resultFile} because it is currently in use.`,
131
+ );
132
+ console.log('Please close any applications that might be using this file and try again.');
133
+ } else {
134
+ console.log(
135
+ `An unexpected error occurred while copying the file from ${intermPath} to ${storagePath}/${resultFile}: ${error.message}`,
136
+ );
137
+ }
138
+ }
139
+ };
140
+
141
+ await Promise.all([transferResults(intermediatePdfResultsPath, constants.pdfScanResultFileName)]);
142
+ };
143
+
144
+ export const createScreenshotsFolder = randomToken => {
145
+ const storagePath = getStoragePath(randomToken);
146
+ const intermediateScreenshotsPath = getIntermediateScreenshotsPath(randomToken);
147
+ if (fs.existsSync(intermediateScreenshotsPath)) {
148
+ fs.readdir(intermediateScreenshotsPath, (err, files) => {
149
+ if (err) {
150
+ console.log(`Screenshots were not moved successfully: ${err.message}`);
151
+ }
152
+
153
+ if (!fs.existsSync(destinationPath(storagePath))) {
154
+ try {
155
+ fs.mkdirSync(destinationPath(storagePath), { recursive: true });
156
+ } catch (error) {
157
+ console.error('Screenshots folder was not created successfully:', error);
158
+ }
159
+ }
160
+
161
+ files.forEach(file => {
162
+ fs.renameSync(
163
+ `${intermediateScreenshotsPath}/${file}`,
164
+ `${destinationPath(storagePath)}/${file}`,
165
+ );
166
+ });
167
+
168
+ fs.rmdir(intermediateScreenshotsPath, rmdirErr => {
169
+ if (rmdirErr) {
170
+ console.log(rmdirErr);
171
+ }
172
+ });
173
+ });
174
+ }
175
+ };
176
+
177
+ export const cleanUp = async pathToDelete => {
178
+ fs.removeSync(pathToDelete);
179
+ };
180
+
181
+ /* istanbul ignore next */
182
+ // export const getFormattedTime = () =>
183
+ // new Date().toLocaleTimeString('en-GB', {
184
+ // year: 'numeric',
185
+ // month: 'short',
186
+ // day: 'numeric',
187
+ // hour12: true,
188
+ // hour: 'numeric',
189
+ // minute: '2-digit',
190
+ // timeZoneName: "longGeneric",
191
+ // });
192
+
193
+ export const getWcagPassPercentage = (wcagViolations: string[]): string => {
194
+ const totalChecks = Object.keys(constants.wcagLinks).length;
195
+ const passedChecks = totalChecks - wcagViolations.length;
196
+ const passPercentage = (passedChecks / totalChecks) * 100;
197
+
198
+ return passPercentage.toFixed(2); // toFixed returns a string, which is correct here
199
+ };
200
+
201
+ export const getFormattedTime = inputDate => {
202
+ if (inputDate) {
203
+ return inputDate.toLocaleTimeString('en-GB', {
204
+ year: 'numeric',
205
+ month: 'short',
206
+ day: 'numeric',
207
+ hour12: false,
208
+ hour: 'numeric',
209
+ minute: '2-digit',
210
+ });
211
+ }
212
+ return new Date().toLocaleTimeString('en-GB', {
213
+ year: 'numeric',
214
+ month: 'short',
215
+ day: 'numeric',
216
+ hour12: false,
217
+ hour: 'numeric',
218
+ minute: '2-digit',
219
+ timeZoneName: 'longGeneric',
220
+ });
221
+ };
222
+
223
+ export const formatDateTimeForMassScanner = date => {
224
+ // Format date and time parts separately
225
+ const year = date.getFullYear().toString().slice(-2); // Get the last two digits of the year
226
+ const month = `0${date.getMonth() + 1}`.slice(-2); // Month is zero-indexed
227
+ const day = `0${date.getDate()}`.slice(-2);
228
+ const hour = `0${date.getHours()}`.slice(-2);
229
+ const minute = `0${date.getMinutes()}`.slice(-2);
230
+
231
+ // Combine formatted date and time with a slash
232
+ const formattedDateTime = `${day}/${month}/${year} ${hour}:${minute}`;
233
+
234
+ return formattedDateTime;
235
+ };
236
+
237
+ export const setHeadlessMode = (browser: string, isHeadless: boolean): void => {
238
+ const isWindowsOSAndEdgeBrowser = browser === BrowserTypes.EDGE && os.platform() === 'win32';
239
+ if (isHeadless || isWindowsOSAndEdgeBrowser) {
240
+ process.env.CRAWLEE_HEADLESS = '1';
241
+ } else {
242
+ process.env.CRAWLEE_HEADLESS = '0';
243
+ }
244
+ };
245
+
246
+ export const setThresholdLimits = setWarnLevel => {
247
+ process.env.WARN_LEVEL = setWarnLevel;
248
+ };
249
+
250
+ export const zipResults = (zipName, resultsPath) => {
251
+ // Check prior zip file exist and remove
252
+ if (fs.existsSync(zipName)) {
253
+ fs.unlinkSync(zipName);
254
+ }
255
+
256
+ if (os.platform() === 'win32') {
257
+ execSync(
258
+ `Get-ChildItem -Path "${resultsPath}\\*.*" -Recurse | Compress-Archive -DestinationPath "${zipName}"`,
259
+ { shell: 'powershell.exe' },
260
+ );
261
+ } else {
262
+ // Get zip command in Mac and Linux
263
+ const command = '/usr/bin/zip';
264
+ // Check if user specified absolute or relative path
265
+ const zipFilePath = path.isAbsolute(zipName) ? zipName : path.join(process.cwd(), zipName);
266
+
267
+ // To zip up files recursively (-r) in the results folder path and write it to user's specified path
268
+ const args = ['-r', zipFilePath, '.'];
269
+
270
+ // Change working directory only for the zip command
271
+ const options = {
272
+ cwd: resultsPath,
273
+ };
274
+
275
+ spawnSync(command, args, options);
276
+ }
277
+ };
278
+
279
+ // areLinksEqual compares 2 string URLs and ignores comparison of 'www.' and url protocol
280
+ // i.e. 'http://google.com' and 'https://www.google.com' returns true
281
+ export const areLinksEqual = (link1, link2) => {
282
+ try {
283
+ const format = link => {
284
+ return new URL(link.replace(/www\./, ''));
285
+ };
286
+ const l1 = format(link1);
287
+ const l2 = format(link2);
288
+
289
+ const areHostEqual = l1.host === l2.host;
290
+ const arePathEqual = l1.pathname === l2.pathname;
291
+
292
+ return areHostEqual && arePathEqual;
293
+ } catch {
294
+ return link1 === link2;
295
+ }
296
+ };
297
+
298
+ export const randomThreeDigitNumberString = () => {
299
+ // Generate a random decimal between 0 (inclusive) and 1 (exclusive)
300
+ const randomDecimal = Math.random();
301
+ // Multiply by 900 to get a decimal between 0 (inclusive) and 900 (exclusive)
302
+ const scaledDecimal = randomDecimal * 900;
303
+ // Add 100 to ensure the result is between 100 (inclusive) and 1000 (exclusive)
304
+ const threeDigitNumber = Math.floor(scaledDecimal) + 100;
305
+ return String(threeDigitNumber);
306
+ };
307
+
308
+ export const isFollowStrategy = (link1, link2, rule) => {
309
+ const parsedLink1 = new URL(link1);
310
+ const parsedLink2 = new URL(link2);
311
+ if (rule === 'same-domain') {
312
+ const link1Domain = parsedLink1.hostname.split('.').slice(-2).join('.');
313
+ const link2Domain = parsedLink2.hostname.split('.').slice(-2).join('.');
314
+ return link1Domain === link2Domain;
315
+ }
316
+ return parsedLink1.hostname === parsedLink2.hostname;
317
+ };
318
+
319
+ /* eslint-disable no-await-in-loop */
320
+ export const retryFunction = async (func, maxAttempt) => {
321
+ let attemptCount = 0;
322
+ while (attemptCount < maxAttempt) {
323
+ attemptCount += 1;
324
+ try {
325
+ const result = await func();
326
+ return result;
327
+ } catch (error) {
328
+ silentLogger.error(`(Attempt count: ${attemptCount} of ${maxAttempt}) ${error}`);
329
+ }
330
+ }
331
+ };
332
+ /* eslint-enable no-await-in-loop */
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "./dist",
4
+ "allowJs": true,
5
+ "target": "es2021",
6
+ "module": "nodenext",
7
+ "rootDir": "./src",
8
+ "skipLibCheck": true
9
+ },
10
+ "include": ["./src/**/*"],
11
+ "exclude": [
12
+ "node_modules",
13
+ "**/*.test.ts"
14
+ ]
15
+ }