borp 0.3.0 → 0.4.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/README.md +17 -4
- package/borp.js +19 -10
- package/fixtures/only-src/src/add.test.ts +7 -0
- package/fixtures/only-src/src/add.ts +4 -0
- package/fixtures/only-src/src/add2.test.ts +7 -0
- package/fixtures/only-src/tsconfig.json +24 -0
- package/fixtures/src-to-dist/src/lib/add.ts +4 -0
- package/fixtures/src-to-dist/src/test/add.test.ts +7 -0
- package/fixtures/src-to-dist/src/test/add2.test.ts +7 -0
- package/fixtures/src-to-dist/tsconfig.json +24 -0
- package/lib/run.js +28 -37
- package/package.json +3 -3
- package/test/basic.test.js +120 -0
- package/test/watch.test.js +6 -2
package/README.md
CHANGED
|
@@ -21,7 +21,18 @@ Borp will autumatically run all tests files matching `*.test.{js|ts}`.
|
|
|
21
21
|
|
|
22
22
|
### Example project setup
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
```
|
|
25
|
+
.
|
|
26
|
+
├── src
|
|
27
|
+
│ ├── lib
|
|
28
|
+
│ │ └── add.ts
|
|
29
|
+
│ └── test
|
|
30
|
+
│ └── add.test.ts
|
|
31
|
+
└── tsconfig.json
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
As an example, consider having a `src/lib/add.ts` file
|
|
25
36
|
|
|
26
37
|
```typescript
|
|
27
38
|
export function add (x: number, y: number): number {
|
|
@@ -29,11 +40,11 @@ export function add (x: number, y: number): number {
|
|
|
29
40
|
}
|
|
30
41
|
```
|
|
31
42
|
|
|
32
|
-
and a `test/add.test.ts` file:
|
|
43
|
+
and a `src/test/add.test.ts` file:
|
|
33
44
|
|
|
34
45
|
```typescript
|
|
35
46
|
import { test } from 'node:test'
|
|
36
|
-
import { add } from '../
|
|
47
|
+
import { add } from '../lib/add.js'
|
|
37
48
|
import { strictEqual } from 'node:assert'
|
|
38
49
|
|
|
39
50
|
test('add', () => {
|
|
@@ -41,7 +52,7 @@ test('add', () => {
|
|
|
41
52
|
})
|
|
42
53
|
```
|
|
43
54
|
|
|
44
|
-
and the following `tsconfig`:
|
|
55
|
+
and the following `tsconfig.json`:
|
|
45
56
|
|
|
46
57
|
```json
|
|
47
58
|
{
|
|
@@ -79,6 +90,8 @@ Note the use of `incremental: true`, which speed up compilation massively.
|
|
|
79
90
|
* `--watch` or `-w`, re-run tests on changes
|
|
80
91
|
* `--timeout` or `-t`, timeouts the tests after a given time; default is 30000 ms
|
|
81
92
|
* `--coverage-exclude` or `-X`, a list of comma-separated patterns to exclude from the coverage report. All tests files are ignored by default.
|
|
93
|
+
* `--ignore` or `-i`, ignore a glob pattern, and not look for tests there
|
|
94
|
+
* `--pattern` or `-p`, run tests matching the given glob pattern
|
|
82
95
|
|
|
83
96
|
## License
|
|
84
97
|
|
package/borp.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from 'node:util'
|
|
4
4
|
import { tap, spec } from 'node:test/reporters'
|
|
5
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
5
|
+
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
|
6
6
|
import { finished } from 'node:stream/promises'
|
|
7
7
|
import { join, relative } from 'node:path'
|
|
8
8
|
import posix from 'node:path/posix'
|
|
9
9
|
import runWithTypeScript from './lib/run.js'
|
|
10
10
|
import { Report } from 'c8'
|
|
11
|
+
import os from 'node:os'
|
|
11
12
|
|
|
12
13
|
let reporter
|
|
13
14
|
/* c8 ignore next 4 */
|
|
@@ -24,14 +25,22 @@ const args = parseArgs({
|
|
|
24
25
|
only: { type: 'boolean', short: 'o' },
|
|
25
26
|
watch: { type: 'boolean', short: 'w' },
|
|
26
27
|
pattern: { type: 'string', short: 'p' },
|
|
27
|
-
concurrency: { type: 'string', short: 'c' },
|
|
28
|
+
concurrency: { type: 'string', short: 'c', default: os.availableParallelism() - 1 + '' },
|
|
28
29
|
coverage: { type: 'boolean', short: 'C' },
|
|
29
30
|
timeout: { type: 'string', short: 't', default: '30000' },
|
|
30
|
-
'coverage-exclude': { type: 'string', short: 'X' }
|
|
31
|
+
'coverage-exclude': { type: 'string', short: 'X', multiple: true },
|
|
32
|
+
ignore: { type: 'string', short: 'i', multiple: true },
|
|
33
|
+
help: { type: 'boolean', short: 'h' }
|
|
31
34
|
},
|
|
32
35
|
allowPositionals: true
|
|
33
36
|
})
|
|
34
37
|
|
|
38
|
+
/* c8 ignore next 4 */
|
|
39
|
+
if (args.values.help) {
|
|
40
|
+
console.log(await readFile(new URL('./README.md', import.meta.url), 'utf8'))
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
if (args.values.concurrency) {
|
|
36
45
|
args.values.concurrency = parseInt(args.values.concurrency)
|
|
37
46
|
}
|
|
@@ -42,7 +51,7 @@ if (args.values.timeout) {
|
|
|
42
51
|
|
|
43
52
|
let covDir
|
|
44
53
|
if (args.values.coverage) {
|
|
45
|
-
covDir = await mkdtemp(join(
|
|
54
|
+
covDir = await mkdtemp(join(os.tmpdir(), 'coverage-'))
|
|
46
55
|
process.env.NODE_V8_COVERAGE = covDir
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -65,15 +74,12 @@ try {
|
|
|
65
74
|
await finished(stream)
|
|
66
75
|
|
|
67
76
|
if (covDir) {
|
|
68
|
-
let exclude =
|
|
77
|
+
let exclude = args.values['coverage-exclude']
|
|
69
78
|
|
|
70
|
-
if (exclude
|
|
71
|
-
exclude = undefined
|
|
72
|
-
} else if (config.prefix) {
|
|
79
|
+
if (exclude && config.prefix) {
|
|
73
80
|
const localPrefix = relative(process.cwd(), config.prefix)
|
|
74
81
|
exclude = exclude.map((file) => posix.join(localPrefix, file))
|
|
75
82
|
}
|
|
76
|
-
console.log('>> Excluding from coverage:', exclude)
|
|
77
83
|
const report = Report({
|
|
78
84
|
reporter: ['text'],
|
|
79
85
|
tempDirectory: covDir,
|
|
@@ -87,6 +93,9 @@ try {
|
|
|
87
93
|
console.error(err)
|
|
88
94
|
} finally {
|
|
89
95
|
if (covDir) {
|
|
90
|
-
|
|
96
|
+
try {
|
|
97
|
+
await rm(covDir, { recursive: true, maxRetries: 10, retryDelay: 100 })
|
|
98
|
+
/* c8 ignore next 2 */
|
|
99
|
+
} catch {}
|
|
91
100
|
}
|
|
92
101
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"sourceMap": true,
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "NodeNext",
|
|
8
|
+
"moduleResolution": "NodeNext",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"newLine": "lf",
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"lib": [
|
|
20
|
+
"ESNext"
|
|
21
|
+
],
|
|
22
|
+
"incremental": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"sourceMap": true,
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "NodeNext",
|
|
8
|
+
"moduleResolution": "NodeNext",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"newLine": "lf",
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"lib": [
|
|
20
|
+
"ESNext"
|
|
21
|
+
],
|
|
22
|
+
"incremental": true
|
|
23
|
+
}
|
|
24
|
+
}
|
package/lib/run.js
CHANGED
|
@@ -2,20 +2,10 @@ import { run } from 'node:test'
|
|
|
2
2
|
import { glob } from 'glob'
|
|
3
3
|
import { findUp } from 'find-up'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
|
-
import {
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
6
|
import { access, readFile } from 'node:fs/promises'
|
|
7
7
|
import { execa } from 'execa'
|
|
8
8
|
|
|
9
|
-
async function isFileAccessible (filename, directory) {
|
|
10
|
-
try {
|
|
11
|
-
const filePath = directory ? resolve(directory, filename) : filename
|
|
12
|
-
await access(filePath)
|
|
13
|
-
return true
|
|
14
|
-
} catch (err) {
|
|
15
|
-
return false
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
function deferred () {
|
|
20
10
|
let resolve
|
|
21
11
|
let reject
|
|
@@ -28,7 +18,7 @@ function deferred () {
|
|
|
28
18
|
|
|
29
19
|
export default async function runWithTypeScript (config) {
|
|
30
20
|
const { cwd } = config
|
|
31
|
-
|
|
21
|
+
let pushable = []
|
|
32
22
|
const tsconfigPath = await findUp('tsconfig.json', { cwd })
|
|
33
23
|
|
|
34
24
|
let prefix = ''
|
|
@@ -39,22 +29,20 @@ export default async function runWithTypeScript (config) {
|
|
|
39
29
|
const typescriptPathCWD = _require.resolve('typescript')
|
|
40
30
|
tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc')
|
|
41
31
|
if (tscPath) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
throw new Error('Could not find tsc')
|
|
32
|
+
// This will throw if we cannot find the `tsc` binary
|
|
33
|
+
await access(tscPath)
|
|
34
|
+
|
|
35
|
+
// Watch is handled aftterwards
|
|
36
|
+
if (!config.watch) {
|
|
37
|
+
const start = Date.now()
|
|
38
|
+
await execa('node', [tscPath], { cwd: dirname(tsconfigPath) })
|
|
39
|
+
pushable.push({
|
|
40
|
+
type: 'test:diagnostic',
|
|
41
|
+
data: {
|
|
42
|
+
nesting: 0,
|
|
43
|
+
message: `TypeScript compilation complete (${Date.now() - start}ms)`
|
|
44
|
+
}
|
|
45
|
+
})
|
|
58
46
|
}
|
|
59
47
|
}
|
|
60
48
|
const tsconfig = JSON.parse(await readFile(tsconfigPath))
|
|
@@ -65,9 +53,10 @@ export default async function runWithTypeScript (config) {
|
|
|
65
53
|
}
|
|
66
54
|
config.prefix = prefix
|
|
67
55
|
config.setup = (test) => {
|
|
68
|
-
for (const chunk of
|
|
56
|
+
for (const chunk of pushable) {
|
|
69
57
|
test.reporter.push(chunk)
|
|
70
58
|
}
|
|
59
|
+
pushable = test.reporter
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
let tscChild
|
|
@@ -82,7 +71,7 @@ export default async function runWithTypeScript (config) {
|
|
|
82
71
|
tscChild.stdout.setEncoding('utf8')
|
|
83
72
|
tscChild.stdout.on('data', (data) => {
|
|
84
73
|
if (data.includes('Watching for file changes')) {
|
|
85
|
-
|
|
74
|
+
pushable.push({
|
|
86
75
|
type: 'test:diagnostic',
|
|
87
76
|
data: {
|
|
88
77
|
nesting: 0,
|
|
@@ -93,8 +82,7 @@ export default async function runWithTypeScript (config) {
|
|
|
93
82
|
p.resolve()
|
|
94
83
|
}
|
|
95
84
|
if (data.includes('error TS')) {
|
|
96
|
-
|
|
97
|
-
toPush.push({
|
|
85
|
+
pushable.push({
|
|
98
86
|
type: 'test:fail',
|
|
99
87
|
data: {
|
|
100
88
|
nesting: 0,
|
|
@@ -115,20 +103,23 @@ export default async function runWithTypeScript (config) {
|
|
|
115
103
|
}
|
|
116
104
|
|
|
117
105
|
let files = config.files || []
|
|
118
|
-
const ignore =
|
|
106
|
+
const ignore = config.ignore || []
|
|
107
|
+
ignore.unshift('node_modules/**/*')
|
|
119
108
|
if (files.length > 0) {
|
|
120
109
|
if (prefix) {
|
|
121
110
|
files = files.map((file) => join(prefix, file.replace(/ts$/, 'js')))
|
|
122
111
|
}
|
|
123
112
|
} else if (config.pattern) {
|
|
113
|
+
let pattern = config.pattern
|
|
124
114
|
if (prefix) {
|
|
125
|
-
|
|
115
|
+
pattern = join(prefix, pattern)
|
|
116
|
+
pattern = pattern.replace(/ts$/, 'js')
|
|
126
117
|
}
|
|
127
|
-
files = await glob(
|
|
118
|
+
files = await glob(pattern, { ignore, cwd, windowsPathsNoEscape: true })
|
|
128
119
|
} else if (prefix) {
|
|
129
|
-
files = await glob(join(prefix, join('
|
|
120
|
+
files = await glob(join(prefix, join('**', '*.test.{cjs,mjs,js}')), { ignore, cwd, windowsPathsNoEscape: true })
|
|
130
121
|
} else {
|
|
131
|
-
files = await glob(join('
|
|
122
|
+
files = await glob(join('**', '*.test.{cjs,mjs,js}'), { ignore, cwd, windowsPathsNoEscape: true })
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
config.files = files
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "node:test wrapper with TypeScript support",
|
|
6
6
|
"main": "borp.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"clean": "rm -rf fixtures/*/dist .test-*
|
|
8
|
+
"clean": "rm -rf fixtures/*/dist .test-*",
|
|
9
9
|
"lint": "standard | snazzy",
|
|
10
|
-
"unit": "node borp.js --
|
|
10
|
+
"unit": "node borp.js --ignore \"fixtures/**/*\" --coverage --coverage-exclude \"fixtures/**/*\" --coverage-exclude \"test/**/*\"",
|
|
11
11
|
"test": "npm run clean ; npm run lint && npm run unit"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [],
|
package/test/basic.test.js
CHANGED
|
@@ -45,3 +45,123 @@ test('ts-cjs', async (t) => {
|
|
|
45
45
|
|
|
46
46
|
await completed
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
test('ts-esm with named failes', async (t) => {
|
|
50
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 3 })
|
|
51
|
+
const config = {
|
|
52
|
+
files: ['test/add.test.ts'],
|
|
53
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stream = await runWithTypeScript(config)
|
|
57
|
+
|
|
58
|
+
const names = new Set(['add'])
|
|
59
|
+
|
|
60
|
+
stream.once('data', (test) => {
|
|
61
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
62
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
stream.on('test:pass', (test) => {
|
|
66
|
+
strictEqual(names.has(test.name), true)
|
|
67
|
+
names.delete(test.name)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
await completed
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('pattern', async (t) => {
|
|
74
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 3 })
|
|
75
|
+
const config = {
|
|
76
|
+
files: [],
|
|
77
|
+
pattern: 'test/*2.test.ts',
|
|
78
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const stream = await runWithTypeScript(config)
|
|
82
|
+
|
|
83
|
+
const names = new Set(['add2'])
|
|
84
|
+
|
|
85
|
+
stream.once('data', (test) => {
|
|
86
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
87
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
stream.on('test:pass', (test) => {
|
|
91
|
+
strictEqual(names.has(test.name), true)
|
|
92
|
+
names.delete(test.name)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
await completed
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('no files', async (t) => {
|
|
99
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 4 })
|
|
100
|
+
const config = {
|
|
101
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stream = await runWithTypeScript(config)
|
|
105
|
+
|
|
106
|
+
const names = new Set(['add', 'add2'])
|
|
107
|
+
|
|
108
|
+
stream.once('data', (test) => {
|
|
109
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
110
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
stream.on('test:pass', (test) => {
|
|
114
|
+
strictEqual(names.has(test.name), true)
|
|
115
|
+
names.delete(test.name)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await completed
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('src-to-dist', async (t) => {
|
|
122
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 4 })
|
|
123
|
+
const config = {
|
|
124
|
+
files: [],
|
|
125
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'src-to-dist')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stream = await runWithTypeScript(config)
|
|
129
|
+
|
|
130
|
+
const names = new Set(['add', 'add2'])
|
|
131
|
+
|
|
132
|
+
stream.once('data', (test) => {
|
|
133
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
134
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
stream.on('test:pass', (test) => {
|
|
138
|
+
strictEqual(names.has(test.name), true)
|
|
139
|
+
names.delete(test.name)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
await completed
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('only-src', async (t) => {
|
|
146
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 4 })
|
|
147
|
+
const config = {
|
|
148
|
+
files: [],
|
|
149
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'only-src')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const stream = await runWithTypeScript(config)
|
|
153
|
+
|
|
154
|
+
const names = new Set(['add', 'add2'])
|
|
155
|
+
|
|
156
|
+
stream.once('data', (test) => {
|
|
157
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
158
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
stream.on('test:pass', (test) => {
|
|
162
|
+
strictEqual(names.has(test.name), true)
|
|
163
|
+
names.delete(test.name)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await completed
|
|
167
|
+
})
|
package/test/watch.test.js
CHANGED
|
@@ -17,7 +17,9 @@ test('watch', async (t) => {
|
|
|
17
17
|
const controller = new AbortController()
|
|
18
18
|
t.after(async () => {
|
|
19
19
|
controller.abort()
|
|
20
|
-
|
|
20
|
+
try {
|
|
21
|
+
await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 })
|
|
22
|
+
} catch {}
|
|
21
23
|
})
|
|
22
24
|
|
|
23
25
|
const config = {
|
|
@@ -67,7 +69,9 @@ test('watch file syntax error', async (t) => {
|
|
|
67
69
|
const controller = new AbortController()
|
|
68
70
|
t.after(async () => {
|
|
69
71
|
controller.abort()
|
|
70
|
-
|
|
72
|
+
try {
|
|
73
|
+
await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 })
|
|
74
|
+
} catch {}
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
const config = {
|