thread-stream 3.1.0 → 4.1.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.
@@ -24,7 +24,7 @@ jobs:
24
24
  contents: read
25
25
  steps:
26
26
  - name: Check out repo
27
- uses: actions/checkout@v3
27
+ uses: actions/checkout@v6
28
28
  with:
29
29
  persist-credentials: false
30
30
 
@@ -38,20 +38,17 @@ jobs:
38
38
  contents: read
39
39
  strategy:
40
40
  matrix:
41
- node-version: [18, 20, 22]
41
+ node-version: [20, 22, 24, 26]
42
42
  os: [macos-latest, ubuntu-latest, windows-latest]
43
- exclude:
44
- - os: windows-latest
45
- node-version: 22
46
43
 
47
44
  steps:
48
45
  - name: Check out repo
49
- uses: actions/checkout@v3
46
+ uses: actions/checkout@v6
50
47
  with:
51
48
  persist-credentials: false
52
49
 
53
50
  - name: Setup Node ${{ matrix.node-version }}
54
- uses: actions/setup-node@v4
51
+ uses: actions/setup-node@v6
55
52
  with:
56
53
  node-version: ${{ matrix.node-version }}
57
54
 
@@ -61,23 +58,6 @@ jobs:
61
58
  - name: Run tests
62
59
  run: npm run test:ci
63
60
 
64
- - name: Coveralls Parallel
65
- uses: coverallsapp/github-action@v2.3.0
66
- with:
67
- github-token: ${{ secrets.github_token }}
68
- parallel: true
69
- flag-name: run-${{ matrix.node-version }}-${{ matrix.os }}
70
-
71
- coverage:
72
- needs: test
73
- runs-on: ubuntu-latest
74
- steps:
75
- - name: Coveralls Finished
76
- uses: coverallsapp/github-action@v2.3.0
77
- with:
78
- github-token: ${{ secrets.GITHUB_TOKEN }}
79
- parallel-finished: true
80
-
81
61
  automerge:
82
62
  name: Automerge Dependabot PRs
83
63
  if: >
package/README.md CHANGED
@@ -39,6 +39,8 @@ stream.flush(function () {
39
39
  })
40
40
  ```
41
41
 
42
+ `flush(cb)` waits for the worker destination flush when supported (`flush`, `flushSync`, or pending `drain`).
43
+
42
44
  In `worker.js`:
43
45
 
44
46
  ```js
@@ -87,7 +89,8 @@ This module works with `yarn` in PnP (plug'n play) mode too!
87
89
  ### Emit events
88
90
 
89
91
  You can emit events on the ThreadStream from your worker using [`worker.parentPort.postMessage()`](https://nodejs.org/api/worker_threads.html#workerparentport).
90
- The message (JSON object) must have the following data structure:
92
+ Messages that do not carry a thread-stream protocol `code` are ignored.
93
+ For custom events, the message (JSON object) must have the following data structure:
91
94
 
92
95
  ```js
93
96
  parentPort.postMessage({
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const neostandard = require('neostandard')
4
+
5
+ module.exports = neostandard({
6
+ ignores: [
7
+ 'test/ts/**/*',
8
+ 'test/syntax-error.mjs'
9
+ ]
10
+ })
package/index.d.ts CHANGED
@@ -59,6 +59,13 @@ declare class ThreadStream extends EventEmitter {
59
59
  * Calling the {@link write()} method after calling {@link end()} will emit an error.
60
60
  */
61
61
  end(): void
62
+ /**
63
+ * Flush the stream asynchronously.
64
+ *
65
+ * The callback is invoked once data has been consumed by the worker and the
66
+ * worker destination has acknowledged the flush.
67
+ */
68
+ flush(cb?: (err?: Error) => void): void
62
69
  /**
63
70
  * Flush the stream synchronously.
64
71
  * This method should be called in the shutdown phase to make sure that all data has been flushed.
package/index.js CHANGED
@@ -18,6 +18,8 @@ const kImpl = Symbol('kImpl')
18
18
  // V8 limit for string size
19
19
  const MAX_STRING = buffer.constants.MAX_STRING_LENGTH
20
20
 
21
+ function noop () {}
22
+
21
23
  class FakeWeakRef {
22
24
  constructor (value) {
23
25
  this._value = value
@@ -54,6 +56,7 @@ function createWorker (stream, opts) {
54
56
 
55
57
  const worker = new Worker(toExecute, {
56
58
  ...opts.workerOpts,
59
+ name: opts.workerOpts?.name || 'thread-stream',
57
60
  trackUnmanagedFds: false,
58
61
  workerData: {
59
62
  filename: filename.indexOf('file://') === 0
@@ -114,14 +117,15 @@ function nextFlush (stream) {
114
117
  write(stream, toWrite, nextFlush.bind(null, stream))
115
118
  } else {
116
119
  // multi-byte utf-8
117
- stream.flush(() => {
118
- // err is already handled in flush()
120
+ waitForRead(stream, () => {
121
+ // err is already handled in waitForRead()
119
122
  if (stream.destroyed) {
120
123
  return
121
124
  }
122
125
 
123
126
  Atomics.store(stream[kImpl].state, READ_INDEX, 0)
124
127
  Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
128
+ Atomics.notify(stream[kImpl].state, READ_INDEX)
125
129
 
126
130
  // Find a toWrite length that fits the buffer
127
131
  // it must exists as the buffer is at least 4 bytes length
@@ -140,9 +144,10 @@ function nextFlush (stream) {
140
144
  // we had a flushSync in the meanwhile
141
145
  return
142
146
  }
143
- stream.flush(() => {
147
+ waitForRead(stream, () => {
144
148
  Atomics.store(stream[kImpl].state, READ_INDEX, 0)
145
149
  Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
150
+ Atomics.notify(stream[kImpl].state, READ_INDEX)
146
151
  nextFlush(stream)
147
152
  })
148
153
  } else {
@@ -160,13 +165,19 @@ function onWorkerMessage (msg) {
160
165
  return
161
166
  }
162
167
 
168
+ // Node.js watch mode may send internal worker messages that do not
169
+ // participate in thread-stream's worker protocol.
170
+ if (msg?.code == null) {
171
+ return
172
+ }
173
+
163
174
  switch (msg.code) {
164
175
  case 'READY':
165
176
  // Replace the FakeWeakRef with a
166
177
  // proper one.
167
178
  this.stream = new WeakRef(stream)
168
179
 
169
- stream.flush(() => {
180
+ waitForRead(stream, () => {
170
181
  stream[kImpl].ready = true
171
182
  stream.emit('ready')
172
183
  })
@@ -181,6 +192,19 @@ function onWorkerMessage (msg) {
181
192
  stream.emit(msg.name, msg.args)
182
193
  }
183
194
  break
195
+ case 'FLUSHED': {
196
+ if (msg.context !== 'thread-stream') {
197
+ destroy(stream, new Error('this should not happen: ' + msg.code))
198
+ break
199
+ }
200
+
201
+ const cb = stream[kImpl].flushCallbacks.get(msg.id)
202
+ if (cb) {
203
+ stream[kImpl].flushCallbacks.delete(msg.id)
204
+ process.nextTick(cb)
205
+ }
206
+ break
207
+ }
184
208
  case 'WARNING':
185
209
  process.emitWarning(msg.err)
186
210
  break
@@ -225,6 +249,8 @@ class ThreadStream extends EventEmitter {
225
249
  this[kImpl].errored = null
226
250
  this[kImpl].closed = false
227
251
  this[kImpl].buf = ''
252
+ this[kImpl].flushCallbacks = new Map()
253
+ this[kImpl].nextFlushId = 0
228
254
 
229
255
  // TODO (fix): Make private?
230
256
  this.worker = createWorker(this, opts) // TODO (fix): make private
@@ -285,28 +311,15 @@ class ThreadStream extends EventEmitter {
285
311
  }
286
312
 
287
313
  flush (cb) {
288
- if (this[kImpl].destroyed) {
289
- if (typeof cb === 'function') {
290
- process.nextTick(cb, new Error('the worker has exited'))
291
- }
292
- return
293
- }
314
+ cb = typeof cb === 'function' ? cb : noop
294
315
 
295
- // TODO write all .buf
296
- const writeIndex = Atomics.load(this[kImpl].state, WRITE_INDEX)
297
- // process._rawDebug(`(flush) readIndex (${Atomics.load(this.state, READ_INDEX)}) writeIndex (${Atomics.load(this.state, WRITE_INDEX)})`)
298
- wait(this[kImpl].state, READ_INDEX, writeIndex, Infinity, (err, res) => {
316
+ flushBuffer(this, (err) => {
299
317
  if (err) {
300
- destroy(this, err)
301
318
  process.nextTick(cb, err)
302
319
  return
303
320
  }
304
- if (res === 'not-equal') {
305
- // TODO handle deadlock
306
- this.flush(cb)
307
- return
308
- }
309
- process.nextTick(cb)
321
+
322
+ requestWorkerFlush(this, cb)
310
323
  })
311
324
  }
312
325
 
@@ -364,6 +377,93 @@ class ThreadStream extends EventEmitter {
364
377
  }
365
378
  }
366
379
 
380
+ function flushBuffer (stream, cb) {
381
+ if (stream[kImpl].destroyed) {
382
+ process.nextTick(cb, new Error('the worker has exited'))
383
+ return
384
+ }
385
+
386
+ if (!stream[kImpl].sync && (stream[kImpl].flushing || stream[kImpl].buf.length > 0)) {
387
+ setImmediate(flushBuffer, stream, cb)
388
+ return
389
+ }
390
+
391
+ waitForRead(stream, cb)
392
+ }
393
+
394
+ function waitForRead (stream, cb) {
395
+ const writeIndex = Atomics.load(stream[kImpl].state, WRITE_INDEX)
396
+ wait(stream[kImpl].state, READ_INDEX, writeIndex, Infinity, (err, res) => {
397
+ if (err) {
398
+ destroy(stream, err)
399
+ cb(err)
400
+ return
401
+ }
402
+
403
+ if (res !== 'ok') {
404
+ waitForRead(stream, cb)
405
+ return
406
+ }
407
+
408
+ cb()
409
+ })
410
+ }
411
+
412
+ function requestWorkerFlush (stream, cb) {
413
+ if (stream[kImpl].destroyed) {
414
+ process.nextTick(cb, new Error('the worker has exited'))
415
+ return
416
+ }
417
+
418
+ if (!stream[kImpl].ready) {
419
+ const onReady = () => {
420
+ cleanup()
421
+ requestWorkerFlush(stream, cb)
422
+ }
423
+ const onClose = () => {
424
+ cleanup()
425
+ process.nextTick(cb, new Error('the worker has exited'))
426
+ }
427
+ const cleanup = () => {
428
+ stream.off('ready', onReady)
429
+ stream.off('close', onClose)
430
+ }
431
+
432
+ stream.once('ready', onReady)
433
+ stream.once('close', onClose)
434
+ return
435
+ }
436
+
437
+ const id = ++stream[kImpl].nextFlushId
438
+ stream[kImpl].flushCallbacks.set(id, cb)
439
+
440
+ try {
441
+ stream.worker.postMessage({
442
+ code: 'FLUSH',
443
+ context: 'thread-stream',
444
+ id
445
+ })
446
+ } catch (err) {
447
+ stream[kImpl].flushCallbacks.delete(id)
448
+ destroy(stream, err)
449
+ process.nextTick(cb, err)
450
+ }
451
+ }
452
+
453
+ function failPendingFlushCallbacks (stream, err) {
454
+ const callbacks = stream[kImpl].flushCallbacks
455
+ if (callbacks.size === 0) {
456
+ return
457
+ }
458
+
459
+ const flushErr = err || new Error('the worker has exited')
460
+
461
+ for (const cb of callbacks.values()) {
462
+ process.nextTick(cb, flushErr)
463
+ }
464
+ callbacks.clear()
465
+ }
466
+
367
467
  function error (stream, err) {
368
468
  setImmediate(() => {
369
469
  stream.emit('error', err)
@@ -375,6 +475,7 @@ function destroy (stream, err) {
375
475
  return
376
476
  }
377
477
  stream[kImpl].destroyed = true
478
+ failPendingFlushCallbacks(stream, err)
378
479
 
379
480
  if (err) {
380
481
  stream[kImpl].errored = err
@@ -468,6 +569,7 @@ function writeSync (stream) {
468
569
  flushSync(stream)
469
570
  Atomics.store(stream[kImpl].state, READ_INDEX, 0)
470
571
  Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
572
+ Atomics.notify(stream[kImpl].state, READ_INDEX)
471
573
  continue
472
574
  } else if (leftover < 0) {
473
575
  // stream should never happen
@@ -485,6 +587,7 @@ function writeSync (stream) {
485
587
  flushSync(stream)
486
588
  Atomics.store(stream[kImpl].state, READ_INDEX, 0)
487
589
  Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
590
+ Atomics.notify(stream[kImpl].state, READ_INDEX)
488
591
 
489
592
  // Find a toWrite length that fits the buffer
490
593
  // it must exists as the buffer is at least 4 bytes length
package/lib/wait.js CHANGED
@@ -1,61 +1,68 @@
1
1
  'use strict'
2
2
 
3
- const MAX_TIMEOUT = 1000
3
+ // Maximum wait time for a single waitAsync call
4
+ // Used as a fallback poll interval in case notifications are missed
5
+ // Keep this low enough for good throughput but high enough to not busy-loop
6
+ const WAIT_MS = 10000
4
7
 
5
8
  function wait (state, index, expected, timeout, done) {
6
- const max = Date.now() + timeout
7
- let current = Atomics.load(state, index)
8
- if (current === expected) {
9
- done(null, 'ok')
10
- return
11
- }
12
- let prior = current
13
- const check = (backoff) => {
14
- if (Date.now() > max) {
9
+ const max = timeout === Infinity ? Infinity : Date.now() + timeout
10
+
11
+ const check = () => {
12
+ const current = Atomics.load(state, index)
13
+ if (current === expected) {
14
+ done(null, 'ok')
15
+ return
16
+ }
17
+
18
+ if (max !== Infinity && Date.now() > max) {
15
19
  done(null, 'timed-out')
20
+ return
21
+ }
22
+
23
+ // Wait for any change from current value
24
+ const remaining = max === Infinity ? WAIT_MS : Math.min(WAIT_MS, Math.max(1, max - Date.now()))
25
+ const result = Atomics.waitAsync(state, index, current, remaining)
26
+
27
+ if (result.async) {
28
+ result.value.then(check)
16
29
  } else {
17
- setTimeout(() => {
18
- prior = current
19
- current = Atomics.load(state, index)
20
- if (current === prior) {
21
- check(backoff >= MAX_TIMEOUT ? MAX_TIMEOUT : backoff * 2)
22
- } else {
23
- if (current === expected) done(null, 'ok')
24
- else done(null, 'not-equal')
25
- }
26
- }, backoff)
30
+ // Value already changed (not-equal) - recheck on next tick
31
+ setImmediate(check)
27
32
  }
28
33
  }
29
- check(1)
34
+
35
+ check()
30
36
  }
31
37
 
32
- // let waitDiffCount = 0
33
38
  function waitDiff (state, index, expected, timeout, done) {
34
- // const id = waitDiffCount++
35
- // process._rawDebug(`>>> waitDiff ${id}`)
36
- const max = Date.now() + timeout
37
- let current = Atomics.load(state, index)
38
- if (current !== expected) {
39
- done(null, 'ok')
40
- return
41
- }
42
- const check = (backoff) => {
43
- // process._rawDebug(`${id} ${index} current ${current} expected ${expected}`)
44
- // process._rawDebug('' + backoff)
45
- if (Date.now() > max) {
39
+ const max = timeout === Infinity ? Infinity : Date.now() + timeout
40
+
41
+ const check = () => {
42
+ const current = Atomics.load(state, index)
43
+ if (current !== expected) {
44
+ done(null, 'ok')
45
+ return
46
+ }
47
+
48
+ if (max !== Infinity && Date.now() > max) {
46
49
  done(null, 'timed-out')
50
+ return
51
+ }
52
+
53
+ // Wait for value to change from expected
54
+ const remaining = max === Infinity ? WAIT_MS : Math.min(WAIT_MS, Math.max(1, max - Date.now()))
55
+ const result = Atomics.waitAsync(state, index, expected, remaining)
56
+
57
+ if (result.async) {
58
+ result.value.then(check)
47
59
  } else {
48
- setTimeout(() => {
49
- current = Atomics.load(state, index)
50
- if (current !== expected) {
51
- done(null, 'ok')
52
- } else {
53
- check(backoff >= MAX_TIMEOUT ? MAX_TIMEOUT : backoff * 2)
54
- }
55
- }, backoff)
60
+ // Value already changed (not-equal) - recheck on next tick
61
+ setImmediate(check)
56
62
  }
57
63
  }
58
- check(1)
64
+
65
+ check()
59
66
  }
60
67
 
61
68
  module.exports = { wait, waitDiff }
package/lib/worker.js CHANGED
@@ -12,26 +12,114 @@ const {
12
12
  } = workerData
13
13
 
14
14
  let destination
15
+ const flushQueue = []
16
+ let flushing = false
15
17
 
16
18
  const state = new Int32Array(stateBuf)
17
19
  const data = Buffer.from(dataBuf)
18
20
 
21
+ // Keep the event loop alive - Atomics.waitAsync promises don't prevent worker exit
22
+ const keepAlive = setInterval(() => {}, 60 * 60 * 1000)
23
+
24
+ function onParentPortMessage (msg) {
25
+ if (!msg || msg.code !== 'FLUSH' || msg.context !== 'thread-stream') {
26
+ return
27
+ }
28
+
29
+ flushQueue.push(msg.id)
30
+ processFlushQueue()
31
+ }
32
+
33
+ function processFlushQueue () {
34
+ if (flushing || !destination) {
35
+ return
36
+ }
37
+
38
+ const id = flushQueue.shift()
39
+ if (id === undefined) {
40
+ return
41
+ }
42
+
43
+ flushing = true
44
+ flushDestination((err) => {
45
+ flushing = false
46
+
47
+ if (err) {
48
+ parentPort.postMessage({
49
+ code: 'ERROR',
50
+ err
51
+ })
52
+ return
53
+ }
54
+
55
+ parentPort.postMessage({
56
+ code: 'FLUSHED',
57
+ context: 'thread-stream',
58
+ id
59
+ })
60
+
61
+ processFlushQueue()
62
+ })
63
+ }
64
+
65
+ function flushDestination (cb) {
66
+ if (typeof destination?.flush === 'function') {
67
+ if (destination.flush.length === 0) {
68
+ try {
69
+ const result = destination.flush()
70
+ if (result && typeof result.then === 'function') {
71
+ result.then(() => cb(), cb)
72
+ } else {
73
+ cb()
74
+ }
75
+ } catch (err) {
76
+ cb(err)
77
+ }
78
+ return
79
+ }
80
+
81
+ let done = false
82
+ const onDone = (err) => {
83
+ if (done) {
84
+ return
85
+ }
86
+ done = true
87
+ cb(err)
88
+ }
89
+
90
+ try {
91
+ const result = destination.flush(onDone)
92
+ if (result && typeof result.then === 'function') {
93
+ result.then(() => onDone(), onDone)
94
+ }
95
+ } catch (err) {
96
+ onDone(err)
97
+ }
98
+ return
99
+ }
100
+
101
+ if (typeof destination?.flushSync === 'function') {
102
+ try {
103
+ destination.flushSync()
104
+ cb()
105
+ } catch (err) {
106
+ cb(err)
107
+ }
108
+ return
109
+ }
110
+
111
+ if (destination?.writableNeedDrain && !destination?.writableEnded) {
112
+ destination.once('drain', cb)
113
+ return
114
+ }
115
+
116
+ cb()
117
+ }
118
+
19
119
  async function start () {
20
120
  let worker
21
121
  try {
22
- if (filename.endsWith('.ts') || filename.endsWith('.cts')) {
23
- // TODO: add support for the TSM modules loader ( https://github.com/lukeed/tsm ).
24
- if (!process[Symbol.for('ts-node.register.instance')]) {
25
- realRequire('ts-node/register')
26
- } else if (process.env.TS_NODE_DEV) {
27
- realRequire('ts-node-dev')
28
- }
29
- // TODO: Support ES imports once tsc, tap & ts-node provide better compatibility guarantees.
30
- // Remove extra forwardslash on Windows
31
- worker = realRequire(decodeURIComponent(filename.replace(process.platform === 'win32' ? 'file:///' : 'file://', '')))
32
- } else {
33
- worker = (await realImport(filename))
34
- }
122
+ worker = (await realImport(filename))
35
123
  } catch (error) {
36
124
  // A yarn user that tries to start a ThreadStream for an external module
37
125
  // provides a filename pointing to a zip file.
@@ -53,6 +141,19 @@ async function start () {
53
141
  } catch {
54
142
  throw error
55
143
  }
144
+ } else if (filename.endsWith('.ts') || filename.endsWith('.cts')) {
145
+ // Native TypeScript import failed (type stripping not enabled).
146
+ // Fall back to ts-node for TypeScript files.
147
+ try {
148
+ if (!process[Symbol.for('ts-node.register.instance')]) {
149
+ realRequire('ts-node/register')
150
+ } else if (process.env.TS_NODE_DEV) {
151
+ realRequire('ts-node-dev')
152
+ }
153
+ worker = realRequire(decodeURIComponent(filename.replace(process.platform === 'win32' ? 'file:///' : 'file://', '')))
154
+ } catch {
155
+ throw error
156
+ }
56
157
  } else {
57
158
  throw error
58
159
  }
@@ -84,16 +185,21 @@ async function start () {
84
185
  const end = Atomics.load(state, WRITE_INDEX)
85
186
  Atomics.store(state, READ_INDEX, end)
86
187
  Atomics.notify(state, READ_INDEX)
188
+ clearInterval(keepAlive)
87
189
  setImmediate(() => {
88
190
  process.exit(0)
89
191
  })
90
192
  })
193
+
194
+ processFlushQueue()
91
195
  }
92
196
 
93
197
  // No .catch() handler,
94
198
  // in case there is an error it goes
95
199
  // to unhandledRejection
96
200
  start().then(function () {
201
+ parentPort.on('message', onParentPortMessage)
202
+
97
203
  parentPort.postMessage({
98
204
  code: 'READY'
99
205
  })
package/package.json CHANGED
@@ -1,42 +1,35 @@
1
1
  {
2
2
  "name": "thread-stream",
3
- "version": "3.1.0",
3
+ "version": "4.1.0",
4
4
  "description": "A streaming way to send data to a Node.js Worker Thread",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
7
10
  "dependencies": {
8
- "real-require": "^0.2.0"
11
+ "real-require": "^1.0.0"
9
12
  },
10
13
  "devDependencies": {
11
- "@types/node": "^20.1.0",
12
- "@types/tap": "^15.0.0",
13
- "@yao-pkg/pkg": "^5.11.5",
14
+ "@types/node": "^25.0.2",
15
+ "@yao-pkg/pkg": "^6.0.0",
16
+ "borp": "^1.0.0",
14
17
  "desm": "^1.3.0",
18
+ "eslint": "^9.39.1",
15
19
  "fastbench": "^1.0.1",
16
- "husky": "^9.0.6",
20
+ "neostandard": "^0.13.0",
17
21
  "pino-elasticsearch": "^8.0.0",
18
- "sonic-boom": "^4.0.1",
19
- "standard": "^17.0.0",
20
- "tap": "^16.2.0",
22
+ "sonic-boom": "^5.0.0",
21
23
  "ts-node": "^10.8.0",
22
- "typescript": "^5.3.2",
23
- "why-is-node-running": "^2.2.2"
24
+ "typescript": "~5.7.3"
24
25
  },
25
26
  "scripts": {
26
27
  "build": "tsc --noEmit",
27
- "test": "standard && npm run build && npm run transpile && tap \"test/**/*.test.*js\" && tap --ts test/*.test.*ts",
28
- "test:ci": "standard && npm run transpile && npm run test:ci:js && npm run test:ci:ts",
29
- "test:ci:js": "tap --no-check-coverage --timeout=120 --coverage-report=lcovonly \"test/**/*.test.*js\"",
30
- "test:ci:ts": "tap --ts --no-check-coverage --coverage-report=lcovonly \"test/**/*.test.*ts\"",
31
- "test:yarn": "npm run transpile && tap \"test/**/*.test.js\" --no-check-coverage",
32
- "transpile": "sh ./test/ts/transpile.sh",
33
- "prepare": "husky install"
34
- },
35
- "standard": {
36
- "ignore": [
37
- "test/ts/**/*",
38
- "test/syntax-error.mjs"
39
- ]
28
+ "lint": "eslint",
29
+ "test": "npm run lint && npm run build && npm run transpile && borp --pattern \"test/*.test.{js,mjs}\"",
30
+ "test:ci": "npm run lint && npm run transpile && borp --pattern \"test/*.test.{js,mjs}\"",
31
+ "test:yarn": "npm run transpile && borp --pattern \"test/*.test.js\"",
32
+ "transpile": "sh ./test/ts/transpile.sh"
40
33
  },
41
34
  "repository": {
42
35
  "type": "git",