@naturalcycles/dev-lib 20.19.0 → 20.21.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.
@@ -326,99 +326,99 @@ export default {
326
326
  '@typescript-eslint/prefer-find': 2,
327
327
  'prefer-promise-reject-errors': 0,
328
328
  '@typescript-eslint/prefer-promise-reject-errors': 0, // ox
329
- 'unicorn/prefer-import-meta-properties': 2,
330
- 'unicorn/prefer-response-static-json': 0, // ox
331
- 'unicorn/prefer-keyboard-event-key': 0, // ox
332
- 'unicorn/prefer-default-parameters': 0, // ox
333
- 'unicorn/prefer-includes': 0, // ox
334
- 'unicorn/no-array-callback-reference': 0, // false positives
335
- 'unicorn/no-process-exit': 0,
336
- 'unicorn/prefer-single-call': 0,
337
- 'unicorn/no-abusive-eslint-disable': 0,
338
- 'unicorn/no-negated-condition': 0,
339
- 'unicorn/no-unnecessary-polyfills': 0, // little value, bottleneck performance
340
- 'unicorn/no-array-method-this-argument': 0, // bug: wrongly removes`readable.flatMap` concurrency option
341
- 'unicorn/prefer-array-flat': 0, // bug: messes up with `readable.flatMap`
342
- 'unicorn/number-literal-case': 0, // conflicts with prettier
343
- 'unicorn/prevent-abbreviations': 0,
344
- 'unicorn/prefer-module': 0,
345
- 'unicorn/no-null': 0,
346
- 'unicorn/filename-case': 0,
347
- 'unicorn/prefer-node-protocol': 0, // oxlint
348
- 'unicorn/prefer-set-has': 0,
349
- 'unicorn/explicit-length-check': 0,
350
- 'unicorn/no-array-for-each': 0,
351
- 'unicorn/prefer-at': 0, // iOS 15.4+, oxlint
352
- 'unicorn/import-style': 0, // todo: fix
353
- 'unicorn/prefer-spread': 0, // fails on joiSchema.concat() which is not an array!
354
- 'unicorn/prefer-structured-clone': 0, // no real advantage, plus in most of the cases we want JSON to remove undefined, etc.
355
- 'unicorn/better-regex': 0, // we still believe that [0-9] is clearer than [\d]
356
- 'unicorn/no-for-loop': 0, // oxlint has similar
357
- 'unicorn/no-array-reduce': 0, // oxlint
358
- 'unicorn/consistent-assert': 0, // oxlint
359
- 'unicorn/consistent-date-clone': 0, // oxlint
360
- 'unicorn/consistent-empty-array-spread': 0, // oxlint
361
- 'unicorn/consistent-existence-index-check': 0, // oxlint
362
- 'unicorn/escape-case': 0, // oxlint
363
- 'unicorn/no-console-spaces': 0, // oxlint
364
- 'unicorn/no-hex-escape': 0, // oxlint
365
- 'unicorn/no-instanceof-builtins': 0, // oxlint
366
- 'unicorn/no-lonely-if': 0, // oxlint
367
- 'unicorn/no-named-default': 0, // oxlint
368
- 'unicorn/no-negation-in-equality-check': 0, // oxlint
369
- 'unicorn/no-new-buffer': 0, // oxlint
370
- 'unicorn/no-accessor-recursion': 0, // oxlint
371
- 'unicorn/no-anonymous-default-export': 0, // oxlint
372
- 'unicorn/prefer-string-replace-all': 0, // oxlint
373
- 'unicorn/prefer-math-min-max': 0, // oxlint
374
- 'unicorn/prefer-code-point': 0, // oxlint
375
- 'unicorn/prefer-global-this': 0, // oxlint
376
- 'unicorn/error-message': 0, // oxlint
377
- 'unicorn/no-zero-fractions': 0, // oxlint
378
- 'unicorn/prefer-array-some': 0, // oxlint
379
- 'unicorn/prefer-math-trunc': 0, // oxlint
380
- 'unicorn/no-object-as-default-parameter': 0, // doesn't allow e.g method (opt = { skipValidation: true })
381
- 'unicorn/catch-error-name': [
382
- 0, // oxlint
383
- {
384
- name: 'err',
385
- ignore: [/^err\d*$/, /^_/],
386
- },
387
- ],
388
- 'unicorn/prefer-switch': 0,
389
- 'unicorn/no-useless-undefined': 0,
390
- 'unicorn/prefer-ternary': [0, 'only-single-line'], // single-line doesn't really work, hence disabled
391
- 'unicorn/numeric-separators-style': [2, { onlyIfContainsSeparator: true }],
392
- 'unicorn/consistent-destructuring': 0, // todo: enable later
393
- 'unicorn/no-nested-ternary': 0,
394
- 'unicorn/consistent-function-scoping': 0, // todo: consider enabling later
395
- 'unicorn/no-this-assignment': 0,
396
- 'unicorn/prefer-string-slice': 0, // todo: consider?
397
- 'unicorn/prefer-number-properties': 0,
398
- 'unicorn/prefer-negative-index': 0,
399
- 'unicorn/prefer-regexp-test': 0,
400
- 'unicorn/prefer-query-selector': 0,
401
- 'unicorn/prefer-prototype-methods': 0, // false-positive on node promisify() of callback functions
402
- 'unicorn/expiring-todo-comments': 1, // warning, instead of error
403
- 'unicorn/no-await-in-promise-methods': 0, // oxlint
404
- 'unicorn/no-document-cookie': 0, // oxlint
405
- 'unicorn/no-empty-file': 0, // oxlint
406
- 'unicorn/no-invalid-fetch-options': 0, // oxlint
407
- 'unicorn/no-invalid-remove-event-listener': 0, // oxlint
408
- 'unicorn/no-magic-array-flat-depth': 0, // oxlint
409
- 'unicorn/no-new-array': 0, // oxlint
410
- 'unicorn/no-single-promise-in-promise-methods': 0, // oxlint
411
- 'unicorn/no-unnecessary-await': 0, // oxlint
412
- 'unicorn/no-useless-fallback-in-spread': 0, // oxlint
413
- 'unicorn/no-useless-length-check': 0, // oxlint
414
- 'unicorn/no-useless-spread': 0, // oxlint
415
- 'unicorn/prefer-array-find': 0, // oxlint
416
- 'unicorn/prefer-modern-math-apis': 0, // oxlint
417
- 'unicorn/prefer-set-size': 0, // oxlint
418
- 'unicorn/prefer-string-starts-ends-with': 0, // oxlint
329
+ // 'unicorn/prefer-import-meta-properties': 2,
330
+ // 'unicorn/prefer-response-static-json': 0, // ox
331
+ // 'unicorn/prefer-keyboard-event-key': 0, // ox
332
+ // 'unicorn/prefer-default-parameters': 0, // ox
333
+ // 'unicorn/prefer-includes': 0, // ox
334
+ // 'unicorn/no-array-callback-reference': 0, // false positives
335
+ // 'unicorn/no-process-exit': 0,
336
+ // 'unicorn/prefer-single-call': 0,
337
+ // 'unicorn/no-abusive-eslint-disable': 0,
338
+ // 'unicorn/no-negated-condition': 0,
339
+ // 'unicorn/no-unnecessary-polyfills': 0, // little value, bottleneck performance
340
+ // 'unicorn/no-array-method-this-argument': 0, // bug: wrongly removes`readable.flatMap` concurrency option
341
+ // 'unicorn/prefer-array-flat': 0, // bug: messes up with `readable.flatMap`
342
+ // 'unicorn/number-literal-case': 0, // conflicts with prettier
343
+ // 'unicorn/prevent-abbreviations': 0,
344
+ // 'unicorn/prefer-module': 0,
345
+ // 'unicorn/no-null': 0,
346
+ // 'unicorn/filename-case': 0,
347
+ // 'unicorn/prefer-node-protocol': 0, // oxlint
348
+ // 'unicorn/prefer-set-has': 0,
349
+ // 'unicorn/explicit-length-check': 0,
350
+ // 'unicorn/no-array-for-each': 0,
351
+ // 'unicorn/prefer-at': 0, // iOS 15.4+, oxlint
352
+ // 'unicorn/import-style': 0, // todo: fix
353
+ // 'unicorn/prefer-spread': 0, // fails on joiSchema.concat() which is not an array!
354
+ // 'unicorn/prefer-structured-clone': 0, // no real advantage, plus in most of the cases we want JSON to remove undefined, etc.
355
+ // 'unicorn/better-regex': 0, // we still believe that [0-9] is clearer than [\d]
356
+ // 'unicorn/no-for-loop': 0, // oxlint has similar
357
+ // 'unicorn/no-array-reduce': 0, // oxlint
358
+ // 'unicorn/consistent-assert': 0, // oxlint
359
+ // 'unicorn/consistent-date-clone': 0, // oxlint
360
+ // 'unicorn/consistent-empty-array-spread': 0, // oxlint
361
+ // 'unicorn/consistent-existence-index-check': 0, // oxlint
362
+ // 'unicorn/escape-case': 0, // oxlint
363
+ // 'unicorn/no-console-spaces': 0, // oxlint
364
+ // 'unicorn/no-hex-escape': 0, // oxlint
365
+ // 'unicorn/no-instanceof-builtins': 0, // oxlint
366
+ // 'unicorn/no-lonely-if': 0, // oxlint
367
+ // 'unicorn/no-named-default': 0, // oxlint
368
+ // 'unicorn/no-negation-in-equality-check': 0, // oxlint
369
+ // 'unicorn/no-new-buffer': 0, // oxlint
370
+ // 'unicorn/no-accessor-recursion': 0, // oxlint
371
+ // 'unicorn/no-anonymous-default-export': 0, // oxlint
372
+ // 'unicorn/prefer-string-replace-all': 0, // oxlint
373
+ // 'unicorn/prefer-math-min-max': 0, // oxlint
374
+ // 'unicorn/prefer-code-point': 0, // oxlint
375
+ // 'unicorn/prefer-global-this': 0, // oxlint
376
+ // 'unicorn/error-message': 0, // oxlint
377
+ // 'unicorn/no-zero-fractions': 0, // oxlint
378
+ // 'unicorn/prefer-array-some': 0, // oxlint
379
+ // 'unicorn/prefer-math-trunc': 0, // oxlint
380
+ // 'unicorn/no-object-as-default-parameter': 0, // doesn't allow e.g method (opt = { skipValidation: true })
381
+ // 'unicorn/catch-error-name': [
382
+ // 0, // oxlint
383
+ // {
384
+ // name: 'err',
385
+ // ignore: [/^err\d*$/, /^_/],
386
+ // },
387
+ // ],
388
+ // 'unicorn/prefer-switch': 0,
389
+ // 'unicorn/no-useless-undefined': 0,
390
+ // 'unicorn/prefer-ternary': [0, 'only-single-line'], // single-line doesn't really work, hence disabled
391
+ // 'unicorn/numeric-separators-style': [2, { onlyIfContainsSeparator: true }],
392
+ // 'unicorn/consistent-destructuring': 0, // todo: enable later
393
+ // 'unicorn/no-nested-ternary': 0,
394
+ // 'unicorn/consistent-function-scoping': 0, // todo: consider enabling later
395
+ // 'unicorn/no-this-assignment': 0,
396
+ // 'unicorn/prefer-string-slice': 0, // todo: consider?
397
+ // 'unicorn/prefer-number-properties': 0,
398
+ // 'unicorn/prefer-negative-index': 0,
399
+ // 'unicorn/prefer-regexp-test': 0,
400
+ // 'unicorn/prefer-query-selector': 0,
401
+ // 'unicorn/prefer-prototype-methods': 0, // false-positive on node promisify() of callback functions
402
+ // 'unicorn/expiring-todo-comments': 1, // warning, instead of error
403
+ // 'unicorn/no-await-in-promise-methods': 0, // oxlint
404
+ // 'unicorn/no-document-cookie': 0, // oxlint
405
+ // 'unicorn/no-empty-file': 0, // oxlint
406
+ // 'unicorn/no-invalid-fetch-options': 0, // oxlint
407
+ // 'unicorn/no-invalid-remove-event-listener': 0, // oxlint
408
+ // 'unicorn/no-magic-array-flat-depth': 0, // oxlint
409
+ // 'unicorn/no-new-array': 0, // oxlint
410
+ // 'unicorn/no-single-promise-in-promise-methods': 0, // oxlint
411
+ // 'unicorn/no-unnecessary-await': 0, // oxlint
412
+ // 'unicorn/no-useless-fallback-in-spread': 0, // oxlint
413
+ // 'unicorn/no-useless-length-check': 0, // oxlint
414
+ // 'unicorn/no-useless-spread': 0, // oxlint
415
+ // 'unicorn/prefer-array-find': 0, // oxlint
416
+ // 'unicorn/prefer-modern-math-apis': 0, // oxlint
417
+ // 'unicorn/prefer-set-size': 0, // oxlint
418
+ // 'unicorn/prefer-string-starts-ends-with': 0, // oxlint
419
419
  '@typescript-eslint/return-await': [2, 'always'], // ox is not good yet
420
420
  '@typescript-eslint/require-await': 0, // ox
421
- 'unicorn/no-array-reverse': 0, // too early
421
+ // 'unicorn/no-array-reverse': 0, // too early
422
422
  '@typescript-eslint/no-misused-promises': 0, // ox, but not good rule
423
423
  '@typescript-eslint/no-unsafe-assignment': 0,
424
424
  '@typescript-eslint/no-unsafe-member-access': 0,
@@ -438,16 +438,16 @@ export default {
438
438
  '@typescript-eslint/restrict-plus-operands': 0, // ox
439
439
  '@typescript-eslint/unbound-method': 0,
440
440
  '@typescript-eslint/no-unsafe-argument': 0, // prevents "legit" use of `any`
441
- 'unicorn/prefer-export-from': 0, // breaks auto-imports in IntelliJ Idea
442
- 'unicorn/require-module-specifiers': 0, // oxlint
443
- 'unicorn/prefer-classlist-toggle': 0, // oxlint
444
- 'unicorn/no-unnecessary-array-splice-count': 0, // oxlint
445
- 'unicorn/no-useless-error-capture-stack-trace': 0, // oxlint
446
- 'unicorn/prefer-top-level-await': 0, // oxlint
447
- 'unicorn/prefer-class-fields': 0, // oxlint
448
- 'unicorn/no-await-expression-member': 0, // some cases are better as-is
449
- 'unicorn/no-array-sort': 0,
450
- 'unicorn/prefer-json-parse-buffer': 0, // typescript doesn't allow it
441
+ // 'unicorn/prefer-export-from': 0, // breaks auto-imports in IntelliJ Idea
442
+ // 'unicorn/require-module-specifiers': 0, // oxlint
443
+ // 'unicorn/prefer-classlist-toggle': 0, // oxlint
444
+ // 'unicorn/no-unnecessary-array-splice-count': 0, // oxlint
445
+ // 'unicorn/no-useless-error-capture-stack-trace': 0, // oxlint
446
+ // 'unicorn/prefer-top-level-await': 0, // oxlint
447
+ // 'unicorn/prefer-class-fields': 0, // oxlint
448
+ // 'unicorn/no-await-expression-member': 0, // some cases are better as-is
449
+ // 'unicorn/no-array-sort': 0,
450
+ // 'unicorn/prefer-json-parse-buffer': 0, // typescript doesn't allow it
451
451
  'no-constructor-return': 2,
452
452
  // 'no-promise-executor-return': 2,
453
453
  'no-self-compare': 2,
@@ -7,7 +7,6 @@
7
7
  import globals from 'globals'
8
8
  import eslint from '@eslint/js'
9
9
  import tseslint from 'typescript-eslint'
10
- import eslintPluginUnicorn from 'eslint-plugin-unicorn'
11
10
  import eslintPluginVue from 'eslint-plugin-vue'
12
11
  import eslintPluginOxlint from 'eslint-plugin-oxlint'
13
12
  import eslintPluginVitest from '@vitest/eslint-plugin'
@@ -54,11 +53,6 @@ function getEslintConfigForDir() {
54
53
  ...c,
55
54
  files: defaultFiles,
56
55
  })),
57
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/configs/recommended.js
58
- {
59
- ...eslintPluginUnicorn.configs.recommended,
60
- files: defaultFiles,
61
- },
62
56
  // https://eslint.vuejs.org/user-guide/#user-guide
63
57
  ...eslintPluginVue.configs['flat/recommended'].map(c => ({
64
58
  ...c,
@@ -0,0 +1,26 @@
1
+ import { _ms } from '@naturalcycles/js-lib/datetime'
2
+
3
+ /**
4
+ * A minimal reporter that only shows failures and the final summary.
5
+ * No per-test, no per-file output - just failures and final summary.
6
+ *
7
+ * Use via: VITEST_REPORTER=summary vitest run
8
+ */
9
+ export class SummaryOnlyReporter {
10
+ onTestRunEnd(testModules) {
11
+ let files = 0
12
+ let duration = 0
13
+
14
+ for (const mod of testModules) {
15
+ files++
16
+ duration += mod.diagnostic().duration
17
+ }
18
+
19
+ console.log()
20
+ console.log(` Files: ${files}`)
21
+ console.log(` Duration: ${_ms(duration)}`)
22
+ console.log()
23
+ }
24
+ }
25
+
26
+ export default SummaryOnlyReporter
@@ -21,4 +21,7 @@ export function defineVitestConfig(config?: Partial<ViteUserConfig>, cwd?: strin
21
21
  export function getSharedConfig(cwd?: string): InlineConfig
22
22
 
23
23
  export const CollectReporter: any
24
- export const SummaryReporter: any
24
+
25
+ export class SummaryReporter {}
26
+
27
+ export class SummaryOnlyReporter {}
@@ -2,8 +2,10 @@ import fs from 'node:fs'
2
2
  import { VitestAlphabeticSequencer } from './vitestAlphabeticSequencer.js'
3
3
  import { defineConfig } from 'vitest/config'
4
4
  import { SummaryReporter } from './summaryReporter.js'
5
+ import { SummaryOnlyReporter } from './summaryOnlyReporter.js'
5
6
  export { SummaryReporter } from './summaryReporter.js'
6
7
  export { CollectReporter } from './collectReporter.js'
8
+ export { SummaryOnlyReporter } from './summaryOnlyReporter.js'
7
9
 
8
10
  const runsInIDE = doesItRunInIDE()
9
11
  const testType = getTestType(runsInIDE)
@@ -90,17 +92,7 @@ export function getSharedConfig(cwd) {
90
92
  },
91
93
  include,
92
94
  exclude,
93
- reporters: [
94
- 'default',
95
- new SummaryReporter(),
96
- junitReporterEnabled && [
97
- 'junit',
98
- {
99
- suiteName: `${testType} tests`,
100
- // classNameTemplate: '{filename} - {classname}',
101
- },
102
- ],
103
- ].filter(Boolean),
95
+ reporters: getReporters(junitReporterEnabled, testType),
104
96
  // outputFile location is specified for compatibility with the previous jest config
105
97
  outputFile: junitReporterEnabled ? `./tmp/jest/${testType}.xml` : undefined,
106
98
  coverage: {
@@ -130,6 +122,30 @@ export function getSharedConfig(cwd) {
130
122
  }
131
123
  }
132
124
 
125
+ function getReporters(junitReporterEnabled, testType) {
126
+ // VITEST_REPORTER env var allows overriding the default reporter
127
+ // e.g., VITEST_REPORTER=summary for minimal output
128
+ const { VITEST_REPORTER, CLAUDE_CODE } = process.env
129
+
130
+ if (VITEST_REPORTER === 'summary' || CLAUDE_CODE) {
131
+ return [new SummaryOnlyReporter()]
132
+ }
133
+ if (VITEST_REPORTER === 'dot') {
134
+ return ['dot']
135
+ }
136
+
137
+ return [
138
+ 'default',
139
+ new SummaryReporter(),
140
+ junitReporterEnabled && [
141
+ 'junit',
142
+ {
143
+ suiteName: `${testType} tests`,
144
+ },
145
+ ],
146
+ ].filter(Boolean)
147
+ }
148
+
133
149
  function doesItRunInIDE() {
134
150
  // example command line below:
135
151
  // /usr/local/bin/node /Users/some/Idea/some/node_modules/vitest/vitest.mjs --run --reporter /Users/some/Library/Application Support/JetBrains/IntelliJIdea2025.2/plugins/javascript-plugin/helpers/vitest-intellij/node_modules/vitest-intellij-reporter-safe.js --testNamePattern=^ ?case 001: empty data$ /Users/some/Idea/some/src/some/some.integration.test.ts
@@ -9,6 +9,11 @@ import { eslintAll, lintAllCommand, lintStagedCommand, requireOxlintConfig, runB
9
9
  import { runTest } from '../test.util.js';
10
10
  const commands = [
11
11
  { name: 'check', fn: check, desc: '"Run all possible checks": lint, typecheck, then test.' },
12
+ {
13
+ name: 'quick-check',
14
+ fn: quickCheck,
15
+ desc: 'Like check, but without slow parts, to perform preliminary checks',
16
+ },
12
17
  { name: 'bt', fn: bt, desc: 'Build & Test: run "typecheck" (via oxlint) and then "test".' },
13
18
  {
14
19
  name: 'typecheck',
@@ -154,6 +159,10 @@ async function check() {
154
159
  await lintAllCommand();
155
160
  runTest();
156
161
  }
162
+ async function quickCheck() {
163
+ await lintAllCommand(false);
164
+ runTest();
165
+ }
157
166
  async function bt() {
158
167
  await typecheckWithOxlint();
159
168
  runTest();
@@ -1,8 +1,10 @@
1
1
  import type { SemVerString } from '@naturalcycles/js-lib/types';
2
2
  /**
3
3
  * Run all linters.
4
+ *
5
+ * If full=false - the "slow" linters are skipped.
4
6
  */
5
- export declare function lintAllCommand(): Promise<void>;
7
+ export declare function lintAllCommand(full?: boolean): Promise<void>;
6
8
  interface EslintAllOptions {
7
9
  ext?: string;
8
10
  fix?: boolean;
package/dist/lint.util.js CHANGED
@@ -16,8 +16,10 @@ import { cfgDir } from './paths.js';
16
16
  const { CI, ESLINT_CONCURRENCY } = process.env;
17
17
  /**
18
18
  * Run all linters.
19
+ *
20
+ * If full=false - the "slow" linters are skipped.
19
21
  */
20
- export async function lintAllCommand() {
22
+ export async function lintAllCommand(full = true) {
21
23
  const started = Date.now();
22
24
  // const { commitOnChanges, failOnChanges } = _yargs().options({
23
25
  // commitOnChanges: {
@@ -44,16 +46,18 @@ export async function lintAllCommand() {
44
46
  runBiome(fix);
45
47
  runOxlint(fix);
46
48
  // From this point we start the "slow" linters, with ESLint leading the way
47
- // We run eslint BEFORE Prettier, because eslint can delete e.g unused imports.
48
- eslintAll({
49
- fix,
50
- });
51
- if (existsSync(`node_modules/stylelint`) &&
52
- existsSync(`node_modules/stylelint-config-standard-scss`)) {
53
- stylelintAll(fix);
49
+ if (full) {
50
+ // We run eslint BEFORE Prettier, because eslint can delete e.g unused imports.
51
+ eslintAll({
52
+ fix,
53
+ });
54
+ if (existsSync(`node_modules/stylelint`) &&
55
+ existsSync(`node_modules/stylelint-config-standard-scss`)) {
56
+ stylelintAll(fix);
57
+ }
58
+ runPrettier({ fix });
59
+ await runKTLint(fix);
54
60
  }
55
- runPrettier({ fix });
56
- await runKTLint(fix);
57
61
  console.log(`${check(true)}${white(`lint-all`)} ${dimGrey(`took ` + _since(started))}`);
58
62
  // if (needToTrackChanges) {
59
63
  // const gitStatusAfter = gitStatus()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/dev-lib",
3
3
  "type": "module",
4
- "version": "20.19.0",
4
+ "version": "20.21.0",
5
5
  "dependencies": {
6
6
  "@biomejs/biome": "^2",
7
7
  "@eslint/js": "^9",
@@ -14,9 +14,8 @@
14
14
  "eslint-plugin-import-x": "^4",
15
15
  "eslint-plugin-oxlint": "^1",
16
16
  "eslint-plugin-simple-import-sort": "^12",
17
- "eslint-plugin-unicorn": "^62",
18
17
  "eslint-plugin-vue": "^10",
19
- "globals": "^16",
18
+ "globals": "^17",
20
19
  "lint-staged": "^16",
21
20
  "micromatch": "^4",
22
21
  "mitm": "^1",