@naturalcycles/dev-lib 20.24.1 → 20.26.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.
@@ -44,6 +44,8 @@
44
44
  "noUncheckedIndexedAccess": true,
45
45
  "noUncheckedSideEffectImports": true,
46
46
  "noPropertyAccessFromIndexSignature": true,
47
+ "noUnusedLocals": false, // doesn't support _prefixed vars
48
+ "noUnusedParameters": true,
47
49
 
48
50
  // Enabled should be faster, but will catch less errors
49
51
  // "skipLibCheck": true,
@@ -3,7 +3,7 @@ import { _by } from '@naturalcycles/js-lib/array/array.util.js';
3
3
  import { _assert } from '@naturalcycles/js-lib/error/assert.js';
4
4
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
5
5
  import { runScript } from '@naturalcycles/nodejs-lib/runScript';
6
- import { buildCopy, buildProd, eslintAll, lintStagedCommand, requireOxlintConfig, runBiome, runCheck, runOxlint, runPrettier, runTest, stylelintAll, typecheckWithTSC, } from '../check.util.js';
6
+ import { buildCopy, buildProd, eslintAll, lintStagedCommand, requireOxlintConfig, runBiome, runCheck, runOxlint, runPrettier, runTest, stylelintAll, typecheckWithTS, typecheckWithTSC, typecheckWithTSGO, } from '../check.util.js';
7
7
  import { runCommitlint } from '../commitlint.js';
8
8
  const commands = [
9
9
  { name: 'check', fn: runCheck, desc: '"Run all possible checks": lint, typecheck, then test.' },
@@ -12,17 +12,21 @@ const commands = [
12
12
  fn: quickCheck,
13
13
  desc: 'Like check, but without slow parts, to perform preliminary checks',
14
14
  },
15
- { name: 'bt', fn: bt, desc: 'Build & Test: run "typecheck" (via oxlint) and then "test".' },
15
+ { name: 'bt', fn: bt, desc: 'Build & Test: run "typecheck" and then "test".' },
16
16
  {
17
17
  name: 'typecheck',
18
- // fn: typecheckWithOxlint,
19
- fn: typecheckWithTSC, // todo: attempt tsgo
20
- desc: 'Run typecheck via oxlint --type-aware',
18
+ fn: typecheckWithTS,
19
+ desc: 'Run typecheck via tsgo (if available) or tsc',
21
20
  },
22
21
  {
23
22
  name: 'typecheck-with-tsc',
24
23
  fn: typecheckWithTSC,
25
- desc: 'Run typecheck (tsc) in folders (src, scripts, e2e) if there is tsconfig.json present. Deprecated, use oxlint type-checking instead.',
24
+ desc: 'Run typecheck (tsc) in folders (src, scripts, e2e) if there is tsconfig.json present.',
25
+ },
26
+ {
27
+ name: 'typecheck-with-tsgo',
28
+ fn: typecheckWithTSGO,
29
+ desc: 'Run typecheck (tsgo) in folders (src, scripts, e2e) if there is tsconfig.json present.',
26
30
  },
27
31
  {
28
32
  name: 'build',
@@ -63,7 +67,7 @@ const commands = [
63
67
  {
64
68
  name: 'lint',
65
69
  fn: () => runCheck({
66
- typecheckWithTSC: false,
70
+ typecheck: false,
67
71
  test: false,
68
72
  }),
69
73
  desc: 'Run all linters: eslint, prettier, stylelint, ktlint, actionlint.',
@@ -162,14 +166,13 @@ async function quickCheck() {
162
166
  eslint: false,
163
167
  prettier: false,
164
168
  stylelint: false,
165
- typecheckWithTSC: false,
169
+ typecheck: false,
166
170
  });
167
171
  }
168
172
  async function bt() {
169
- // Still using tsc, as oxlint is found to fail in certain cases
170
- // todo: attempt to use tsgo!
173
+ // Still using ts, as oxlint is found to fail in certain cases
171
174
  // await typecheckWithOxlint()
172
- await typecheckWithTSC();
175
+ await typecheckWithTS();
173
176
  runTest();
174
177
  }
175
178
  async function _typecheckWithOxlint() {
@@ -1,4 +1,4 @@
1
- import type { SemVerString } from '@naturalcycles/js-lib/types';
1
+ import { type SemVerString } from '@naturalcycles/js-lib/types';
2
2
  /**
3
3
  * Every boolean defaults to true, so, by default - everything is being run.
4
4
  * Pass false to skip it.
@@ -9,7 +9,13 @@ export interface CheckOptions {
9
9
  stylelint?: boolean;
10
10
  prettier?: boolean;
11
11
  ktlint?: boolean;
12
- typecheckWithTSC?: boolean;
12
+ /**
13
+ * true - run tsgo, otherwise tsc
14
+ * tsgo - run tsgo
15
+ * tsc - run tsc
16
+ * false - skip
17
+ */
18
+ typecheck?: boolean | 'tsc' | 'tsgo';
13
19
  test?: boolean;
14
20
  }
15
21
  /**
@@ -24,27 +30,50 @@ interface EslintAllOptions {
24
30
  }
25
31
  /**
26
32
  * Runs `eslint` command for all predefined paths (e.g /src, /scripts, etc).
33
+ *
34
+ * Returns true if it ran
35
+ */
36
+ export declare function eslintAll(opt?: EslintAllOptions): boolean;
37
+ /**
38
+ * Returns true if it ran.
27
39
  */
28
- export declare function eslintAll(opt?: EslintAllOptions): void;
29
- export declare function runOxlint(fix?: boolean): void;
40
+ export declare function runOxlint(fix?: boolean): boolean;
30
41
  export declare function requireOxlintConfig(): void;
31
42
  export declare function hasOxlintConfig(): boolean;
32
43
  interface RunPrettierOptions {
33
44
  experimentalCli?: boolean;
34
45
  fix?: boolean;
35
46
  }
36
- export declare function runPrettier(opt?: RunPrettierOptions): void;
37
- export declare function stylelintAll(fix?: boolean): void;
47
+ /**
48
+ * Returns true if it ran.
49
+ */
50
+ export declare function runPrettier(opt?: RunPrettierOptions): boolean;
51
+ /**
52
+ * Returns true if it ran.
53
+ */
54
+ export declare function stylelintAll(fix?: boolean): boolean;
38
55
  export declare function lintStagedCommand(): Promise<void>;
39
56
  export declare function requireActionlintVersion(): void;
40
57
  export declare function getActionLintVersion(): SemVerString | undefined;
41
- export declare function runBiome(fix?: boolean): void;
58
+ /**
59
+ * Returns true if it ran.
60
+ */
61
+ export declare function runBiome(fix?: boolean): boolean;
42
62
  export declare function buildProd(): Promise<void>;
63
+ /**
64
+ * Uses tsgo if it's installed, otherwise tsc
65
+ */
66
+ export declare function typecheckWithTS(): Promise<void>;
43
67
  export declare function typecheckWithTSC(): Promise<void>;
68
+ export declare function typecheckWithTSGO(): Promise<void>;
44
69
  /**
45
70
  * Use 'src' to indicate root.
46
71
  */
47
72
  export declare function runTSCInFolders(dirs: string[], args?: string[], parallel?: boolean): Promise<void>;
73
+ /**
74
+ * Use 'src' to indicate root.
75
+ */
76
+ export declare function runTSGOInFolders(dirs: string[], args?: string[], parallel?: boolean): Promise<void>;
48
77
  export declare function runTSCProd(args?: string[]): Promise<void>;
49
78
  export declare function buildCopy(): void;
50
79
  interface RunTestOptions {
@@ -52,6 +81,9 @@ interface RunTestOptions {
52
81
  manual?: boolean;
53
82
  leaks?: boolean;
54
83
  }
55
- export declare function runTest(opt?: RunTestOptions): void;
84
+ /**
85
+ * Returns true if it ran.
86
+ */
87
+ export declare function runTest(opt?: RunTestOptions): boolean;
56
88
  export declare function findPackageBinPath(pkg: string, cmd: string): string;
57
89
  export {};
@@ -4,10 +4,11 @@ import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
5
  import { _isTruthy } from '@naturalcycles/js-lib';
6
6
  import { _uniq } from '@naturalcycles/js-lib/array';
7
- import { _since } from '@naturalcycles/js-lib/datetime/time.util.js';
7
+ import { _ms, _since } from '@naturalcycles/js-lib/datetime/time.util.js';
8
8
  import { _assert } from '@naturalcycles/js-lib/error/assert.js';
9
9
  import { _filterFalsyValues } from '@naturalcycles/js-lib/object/object.util.js';
10
10
  import { semver2 } from '@naturalcycles/js-lib/semver';
11
+ import { _stringMapEntries, } from '@naturalcycles/js-lib/types';
11
12
  import { dimGrey, white } from '@naturalcycles/nodejs-lib/colors';
12
13
  import { exec2 } from '@naturalcycles/nodejs-lib/exec2';
13
14
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
@@ -22,8 +23,10 @@ const { CI, ESLINT_CONCURRENCY } = process.env;
22
23
  * If full=false - the "slow" linters are skipped.
23
24
  */
24
25
  export async function runCheck(opt = {}) {
25
- const { fastLinters = true, eslint = true, stylelint = true, prettier = true, ktlint = true, typecheckWithTSC: runTSC = true, test = true, } = opt;
26
+ const { fastLinters = true, eslint = true, stylelint = true, prettier = true, ktlint = true, typecheck = true, test = true, } = opt;
26
27
  const started = Date.now();
28
+ let s;
29
+ const timings = {};
27
30
  // const { commitOnChanges, failOnChanges } = _yargs().options({
28
31
  // commitOnChanges: {
29
32
  // type: 'boolean',
@@ -47,34 +50,68 @@ export async function runCheck(opt = {}) {
47
50
  if (fastLinters) {
48
51
  // Fast linters (that run in <1 second) go first
49
52
  runActionLint();
50
- runBiome(fix);
51
- runOxlint(fix);
53
+ s = Date.now();
54
+ if (runBiome(fix)) {
55
+ timings['biome'] = Date.now() - s;
56
+ }
57
+ s = Date.now();
58
+ if (runOxlint(fix)) {
59
+ timings['oxlint'] = Date.now() - s;
60
+ }
52
61
  }
53
62
  // From this point we start the "slow" linters, with ESLint leading the way
54
63
  if (eslint) {
55
64
  // We run eslint BEFORE Prettier, because eslint can delete e.g unused imports.
56
- eslintAll({
57
- fix,
58
- });
65
+ s = Date.now();
66
+ if (eslintAll({ fix })) {
67
+ timings['eslint'] = Date.now() - s;
68
+ }
59
69
  }
60
70
  if (stylelint &&
61
- existsSync(`node_modules/stylelint`) &&
62
- existsSync(`node_modules/stylelint-config-standard-scss`)) {
63
- stylelintAll(fix);
71
+ hasDependencyInNodeModules('stylelint') &&
72
+ hasDependencyInNodeModules('stylelint-config-standard-scss')) {
73
+ s = Date.now();
74
+ if (stylelintAll(fix)) {
75
+ timings['stylelint'] = Date.now() - s;
76
+ }
64
77
  }
65
78
  if (prettier) {
66
- runPrettier({ fix });
79
+ s = Date.now();
80
+ if (runPrettier({ fix })) {
81
+ timings['prettier'] = Date.now() - s;
82
+ }
67
83
  }
68
84
  if (ktlint) {
69
- await runKTLint(fix);
85
+ s = Date.now();
86
+ if (await runKTLint(fix)) {
87
+ timings['ktlint'] = Date.now() - s;
88
+ }
70
89
  }
71
- if (runTSC) {
72
- await typecheckWithTSC();
90
+ if (typecheck) {
91
+ s = Date.now();
92
+ if (typecheck === 'tsgo') {
93
+ await typecheckWithTSGO();
94
+ timings['tsgo'] = Date.now() - s;
95
+ }
96
+ else if (typecheck === 'tsc') {
97
+ await typecheckWithTSC();
98
+ timings['tsc'] = Date.now() - s;
99
+ }
100
+ else {
101
+ await typecheckWithTS();
102
+ timings['typecheck'] = Date.now() - s;
103
+ }
73
104
  }
74
105
  if (test) {
75
- runTest();
106
+ s = Date.now();
107
+ if (runTest()) {
108
+ timings['test'] = Date.now() - s;
109
+ }
76
110
  }
77
111
  console.log(`${check(true)}${white(`check`)} ${dimGrey(`took ` + _since(started))}`);
112
+ for (const [job, ms] of _stringMapEntries(timings)) {
113
+ console.log(`${job.padStart(12, ' ')}: ${String(_ms(ms)).padStart(10, ' ')}`);
114
+ }
78
115
  // if (needToTrackChanges) {
79
116
  // const gitStatusAfter = gitStatus()
80
117
  // const hasChanges = gitStatusAfter !== gitStatusAtStart
@@ -97,6 +134,8 @@ export async function runCheck(opt = {}) {
97
134
  }
98
135
  /**
99
136
  * Runs `eslint` command for all predefined paths (e.g /src, /scripts, etc).
137
+ *
138
+ * Returns true if it ran
100
139
  */
101
140
  export function eslintAll(opt) {
102
141
  const { argv } = _yargs().options({
@@ -114,13 +153,16 @@ export function eslintAll(opt) {
114
153
  ...opt,
115
154
  };
116
155
  const extensions = ext.split(',');
117
- runESLint(extensions, fix);
156
+ return runESLint(extensions, fix);
118
157
  }
158
+ /**
159
+ * Returns true if it ran.
160
+ */
119
161
  function runESLint(extensions = eslintExtensions.split(','), fix = true) {
120
162
  const eslintConfigPath = `eslint.config.js`;
121
163
  if (!existsSync(eslintConfigPath)) {
122
164
  // faster to bail-out like this
123
- return;
165
+ return false;
124
166
  }
125
167
  // const tsconfigRootDir = [cwd, configDir !== '.' && configDir].filter(Boolean).join('/')
126
168
  const eslintPath = findPackageBinPath('eslint', 'eslint');
@@ -149,11 +191,15 @@ function runESLint(extensions = eslintExtensions.split(','), fix = true) {
149
191
  TIMING: CI ? 'true' : '',
150
192
  }),
151
193
  });
194
+ return true;
152
195
  }
196
+ /**
197
+ * Returns true if it ran.
198
+ */
153
199
  export function runOxlint(fix = true) {
154
200
  if (!hasOxlintConfig()) {
155
201
  console.log('.oxlintrc.json is not found, skipping to run oxlint');
156
- return;
202
+ return false;
157
203
  }
158
204
  const oxlintPath = findPackageBinPath('oxlint', 'oxlint');
159
205
  exec2.spawn(oxlintPath, {
@@ -169,6 +215,7 @@ export function runOxlint(fix = true) {
169
215
  ].filter(_isTruthy),
170
216
  shell: false,
171
217
  });
218
+ return true;
172
219
  }
173
220
  export function requireOxlintConfig() {
174
221
  _assert(hasOxlintConfig(), '.oxlintrc.json config is not found');
@@ -185,11 +232,15 @@ const prettierPaths = [
185
232
  // Exclude
186
233
  ...lintExclude.map((s) => `!${s}`),
187
234
  ];
235
+ /**
236
+ * Returns true if it ran.
237
+ */
188
238
  export function runPrettier(opt = {}) {
189
239
  let { experimentalCli = true, fix = true } = opt;
190
240
  const prettierConfigPath = [`./prettier.config.js`].find(f => existsSync(f));
191
- if (!prettierConfigPath)
192
- return;
241
+ if (!prettierConfigPath) {
242
+ return false;
243
+ }
193
244
  const prettierPath = findPackageBinPath('prettier', 'prettier');
194
245
  const cacheLocation = 'node_modules/.cache/prettier';
195
246
  const cacheFound = existsSync(cacheLocation);
@@ -214,6 +265,7 @@ export function runPrettier(opt = {}) {
214
265
  ].filter(_isTruthy),
215
266
  shell: false,
216
267
  });
268
+ return true;
217
269
  }
218
270
  const stylelintPaths = [
219
271
  // Everything inside these folders
@@ -221,6 +273,9 @@ const stylelintPaths = [
221
273
  // Exclude
222
274
  ...lintExclude.map((s) => `!${s}`),
223
275
  ];
276
+ /**
277
+ * Returns true if it ran.
278
+ */
224
279
  export function stylelintAll(fix) {
225
280
  const argv = _yargs().options({
226
281
  fix: {
@@ -230,14 +285,16 @@ export function stylelintAll(fix) {
230
285
  }).argv;
231
286
  fix ??= argv.fix;
232
287
  const config = [`./stylelint.config.js`].find(f => existsSync(f));
233
- if (!config)
234
- return;
288
+ if (!config) {
289
+ return false;
290
+ }
235
291
  // stylelint is never hoisted from dev-lib, so, no need to search for its path
236
292
  exec2.spawn('stylelint', {
237
293
  name: fix ? 'stylelint' : 'stylelint --no-fix',
238
294
  args: [fix ? `--fix` : '', `--allow-empty-input`, `--config`, config, ...stylelintPaths].filter(Boolean),
239
295
  shell: false,
240
296
  });
297
+ return true;
241
298
  }
242
299
  export async function lintStagedCommand() {
243
300
  const localConfig = `./lint-staged.config.js`;
@@ -250,24 +307,33 @@ export async function lintStagedCommand() {
250
307
  if (!success)
251
308
  process.exit(3);
252
309
  }
310
+ /**
311
+ * Returns true if it ran.
312
+ */
253
313
  async function runKTLint(fix = true) {
254
- if (!existsSync(`node_modules/@naturalcycles/ktlint`))
255
- return;
314
+ if (!existsSync(`node_modules/@naturalcycles/ktlint`)) {
315
+ return false;
316
+ }
256
317
  // @ts-expect-error ktlint is not installed (due to size in node_modules), but it's ok
257
318
  const { ktlintAll } = await import('@naturalcycles/ktlint');
258
319
  await ktlintAll(fix ? ['-F'] : []);
320
+ return true;
259
321
  }
322
+ /**
323
+ * Returns true if it ran.
324
+ */
260
325
  function runActionLint() {
261
326
  // Only run if there is a folder of `.github/workflows`, otherwise actionlint will fail
262
- if (!existsSync('.github/workflows'))
263
- return;
327
+ if (!existsSync('.github/workflows')) {
328
+ return false;
329
+ }
264
330
  if (canRunBinary('actionlint')) {
265
331
  requireActionlintVersion();
266
332
  exec2.spawn(`actionlint`);
333
+ return true;
267
334
  }
268
- else {
269
- console.log(`actionlint is not installed and won't be run.\nThis is how to install it: https://github.com/rhysd/actionlint/blob/main/docs/install.md`);
270
- }
335
+ console.log(`actionlint is not installed and won't be run.\nThis is how to install it: https://github.com/rhysd/actionlint/blob/main/docs/install.md`);
336
+ return false;
271
337
  }
272
338
  export function requireActionlintVersion() {
273
339
  const version = getActionLintVersion();
@@ -281,11 +347,14 @@ export function getActionLintVersion() {
281
347
  return;
282
348
  return exec2.exec('actionlint --version').split('\n')[0];
283
349
  }
350
+ /**
351
+ * Returns true if it ran.
352
+ */
284
353
  export function runBiome(fix = true) {
285
354
  const configPath = `biome.jsonc`;
286
355
  if (!existsSync(configPath)) {
287
356
  console.log(`biome is skipped, because ./biome.jsonc is not present`);
288
- return;
357
+ return false;
289
358
  }
290
359
  const biomePath = findPackageBinPath('@biomejs/biome', 'biome');
291
360
  const dirs = [`src`, `scripts`, `e2e`].filter(d => existsSync(d));
@@ -294,16 +363,28 @@ export function runBiome(fix = true) {
294
363
  args: [`lint`, fix && '--write', fix && '--unsafe', '--no-errors-on-unmatched', ...dirs].filter(_isTruthy),
295
364
  shell: false,
296
365
  });
366
+ return true;
297
367
  }
298
368
  export async function buildProd() {
299
369
  // fs2.emptyDir('./dist') // it doesn't delete the dir itself, to prevent IDE jumping
300
370
  buildCopy();
301
371
  await runTSCProd();
302
372
  }
373
+ /**
374
+ * Uses tsgo if it's installed, otherwise tsc
375
+ */
376
+ export async function typecheckWithTS() {
377
+ if (hasDependencyInNodeModules('@typescript/native-preview')) {
378
+ await typecheckWithTSGO();
379
+ }
380
+ await typecheckWithTSC();
381
+ }
303
382
  export async function typecheckWithTSC() {
304
- // todo: try tsgo
305
383
  await runTSCInFolders(['src', 'scripts', 'e2e'], ['--noEmit']);
306
384
  }
385
+ export async function typecheckWithTSGO() {
386
+ await runTSGOInFolders(['src', 'scripts', 'e2e'], ['--noEmit', '--incremental', 'false']);
387
+ }
307
388
  /**
308
389
  * Use 'src' to indicate root.
309
390
  */
@@ -339,6 +420,38 @@ async function runTSCInFolder(dir, args = []) {
339
420
  shell: false,
340
421
  });
341
422
  }
423
+ /**
424
+ * Use 'src' to indicate root.
425
+ */
426
+ export async function runTSGOInFolders(dirs, args = [], parallel = true) {
427
+ if (parallel) {
428
+ await Promise.all(dirs.map(dir => runTSGOInFolder(dir, args)));
429
+ }
430
+ else {
431
+ for (const dir of dirs) {
432
+ await runTSGOInFolder(dir, args);
433
+ }
434
+ }
435
+ }
436
+ /**
437
+ * Pass 'src' to run in root.
438
+ */
439
+ async function runTSGOInFolder(dir, args = []) {
440
+ let configDir = dir;
441
+ if (dir === 'src') {
442
+ configDir = '';
443
+ }
444
+ const tsconfigPath = [configDir, 'tsconfig.json'].filter(Boolean).join('/');
445
+ if (!fs2.pathExists(tsconfigPath) || !fs2.pathExists(dir)) {
446
+ // console.log(`Skipping to run tsgo for ${tsconfigPath}, as it doesn't exist`)
447
+ return;
448
+ }
449
+ const tsgoPath = findPackageBinPath('@typescript/native-preview', 'tsgo');
450
+ await exec2.spawnAsync(tsgoPath, {
451
+ args: ['-P', tsconfigPath, ...args],
452
+ shell: false,
453
+ });
454
+ }
342
455
  export async function runTSCProd(args = []) {
343
456
  const tsconfigPath = [`./tsconfig.prod.json`].find(p => fs2.pathExists(p)) || 'tsconfig.json';
344
457
  const tscPath = findPackageBinPath('typescript', 'tsc');
@@ -365,13 +478,17 @@ export function buildCopy() {
365
478
  dotfiles: true,
366
479
  });
367
480
  }
481
+ /**
482
+ * Returns true if it ran.
483
+ */
368
484
  export function runTest(opt = {}) {
369
485
  // if (nodeModuleExists('vitest')) {
370
486
  if (fs2.pathExists('vitest.config.ts')) {
371
487
  runVitest(opt);
372
- return;
488
+ return true;
373
489
  }
374
490
  console.log(dimGrey(`vitest.config.ts not found, skipping tests`));
491
+ return false;
375
492
  }
376
493
  function runVitest(opt) {
377
494
  const { integration, manual } = opt;
@@ -411,6 +528,9 @@ function canRunBinary(name) {
411
528
  return false;
412
529
  }
413
530
  }
531
+ function hasDependencyInNodeModules(name) {
532
+ return existsSync(`node_modules/${name}`);
533
+ }
414
534
  // function gitStatus(): string | undefined {
415
535
  // try {
416
536
  // return execSync('git status -s', {
@@ -1,6 +1,6 @@
1
1
  import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
2
2
  import { red } from '@naturalcycles/nodejs-lib/colors';
3
- import createMitm from 'mitm';
3
+ import { createMitm } from '../vendor/mitm.js';
4
4
  const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1']);
5
5
  const detectLeaks = process.argv.some(a => a.includes('detectLeaks'));
6
6
  let mitm;
@@ -0,0 +1,19 @@
1
+ import type * as NetType from 'node:net';
2
+ export interface SocketOptions {
3
+ port: number;
4
+ host?: string;
5
+ localAddress?: string;
6
+ localPort?: string;
7
+ family?: number;
8
+ allowHalfOpen?: boolean;
9
+ }
10
+ export interface BypassableSocket extends NetType.Socket {
11
+ bypass: () => void;
12
+ bypassed?: boolean;
13
+ }
14
+ export type ConnectCallback = (socket: BypassableSocket, opts: SocketOptions) => void;
15
+ export interface Mitm {
16
+ on: (event: 'connect', callback: ConnectCallback) => void;
17
+ disable: () => void;
18
+ }
19
+ export declare function createMitm(): Mitm;
@@ -0,0 +1,98 @@
1
+ import { createRequire } from 'node:module';
2
+ // Use require() to get mutable module objects (ESM namespace objects are frozen)
3
+ const require = createRequire(import.meta.url);
4
+ /* eslint-disable @typescript-eslint/naming-convention -- vendored code uses PascalCase for modules */
5
+ const Net = require('node:net');
6
+ const Tls = require('node:tls');
7
+ const Http = require('node:http');
8
+ const Https = require('node:https');
9
+ export function createMitm() {
10
+ const stubs = [];
11
+ const listeners = [];
12
+ function stub(obj, prop, value) {
13
+ stubs.push([obj, prop, obj[prop]]);
14
+ obj[prop] = value;
15
+ }
16
+ function restore() {
17
+ let s;
18
+ while ((s = stubs.pop())) {
19
+ s[0][s[1]] = s[2];
20
+ }
21
+ }
22
+ function connect(orig, opts, done) {
23
+ // Create a bypassable socket that we'll pass to listeners
24
+ const socket = new Net.Socket();
25
+ socket.bypassed = false;
26
+ socket.bypass = function () {
27
+ this.bypassed = true;
28
+ };
29
+ // Emit connect event to all listeners
30
+ for (const listener of listeners) {
31
+ listener(socket, opts);
32
+ }
33
+ // If bypassed, call the original connect function
34
+ if (socket.bypassed) {
35
+ return orig.call(Net, opts, done);
36
+ }
37
+ // Not bypassed - return the socket that will never actually connect.
38
+ // testOffline throws an error before we get here, so this is just a fallback.
39
+ return socket;
40
+ }
41
+ function normalizeArgs(args) {
42
+ // Handle the various signatures of Net.connect:
43
+ // connect(port, host?, callback?)
44
+ // connect(options, callback?)
45
+ // connect(path, callback?) - Unix socket
46
+ if (typeof args[0] === 'number') {
47
+ const opts = { port: args[0] };
48
+ if (typeof args[1] === 'string') {
49
+ opts.host = args[1];
50
+ return [opts, args[2]];
51
+ }
52
+ return [opts, args[1]];
53
+ }
54
+ if (typeof args[0] === 'object' && args[0] !== null) {
55
+ return [args[0], args[1]];
56
+ }
57
+ // Fallback for other cases (e.g., Unix socket path)
58
+ return [{ port: 0 }, args[1]];
59
+ }
60
+ function createNetConnect(orig) {
61
+ return function (...args) {
62
+ const [opts, done] = normalizeArgs(args);
63
+ return connect(orig, opts, done);
64
+ };
65
+ }
66
+ function createTlsConnect(orig) {
67
+ return function (...args) {
68
+ const [opts, done] = normalizeArgs(args);
69
+ return connect(orig, opts, done);
70
+ };
71
+ }
72
+ // Stub network functions
73
+ const netConnect = createNetConnect(Net.connect);
74
+ const tlsConnect = createTlsConnect(Tls.connect);
75
+ stub(Net, 'connect', netConnect);
76
+ stub(Net, 'createConnection', netConnect);
77
+ stub(Http.Agent.prototype, 'createConnection', netConnect);
78
+ stub(Tls, 'connect', tlsConnect);
79
+ // Disable keep-alive on global agents to force new connections
80
+ const httpAgent = Http.globalAgent;
81
+ const httpsAgent = Https.globalAgent;
82
+ if (httpAgent['keepAlive']) {
83
+ stub(httpAgent, 'keepAlive', false);
84
+ }
85
+ if (httpsAgent['keepAlive']) {
86
+ stub(httpsAgent, 'keepAlive', false);
87
+ }
88
+ return {
89
+ on(event, callback) {
90
+ if (event === 'connect') {
91
+ listeners.push(callback);
92
+ }
93
+ },
94
+ disable() {
95
+ restore();
96
+ },
97
+ };
98
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/dev-lib",
3
3
  "type": "module",
4
- "version": "20.24.1",
4
+ "version": "20.26.0",
5
5
  "dependencies": {
6
6
  "@biomejs/biome": "^2",
7
7
  "@eslint/js": "^9",
@@ -16,19 +16,22 @@
16
16
  "globals": "^17",
17
17
  "lint-staged": "^16",
18
18
  "micromatch": "^4",
19
- "mitm": "^1",
20
19
  "oxlint": "^1",
21
20
  "oxlint-tsgolint": "*",
22
21
  "prettier": "^3",
23
22
  "typescript-eslint": "^8"
24
23
  },
25
24
  "peerDependencies": {
25
+ "@typescript/native-preview": "^7.0.0-0",
26
26
  "husky": "^9",
27
27
  "stylelint": "^16",
28
28
  "stylelint-config-standard-scss": "^16",
29
29
  "typescript": "^5"
30
30
  },
31
31
  "peerDependenciesMeta": {
32
+ "@typescript/native-preview": {
33
+ "optional": true
34
+ },
32
35
  "stylelint": {
33
36
  "optional": true
34
37
  },
@@ -37,7 +40,6 @@
37
40
  }
38
41
  },
39
42
  "devDependencies": {
40
- "@types/mitm": "^1",
41
43
  "@types/node": "^25",
42
44
  "@types/prompts": "^2"
43
45
  },