@platformatic/basic 3.4.1 → 3.5.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.
@@ -1,24 +1,44 @@
1
- import { ITC } from '@platformatic/itc'
2
- import { setupNodeHTTPTelemetry } from '@platformatic/telemetry'
3
- import { createPinoWritable, ensureLoggableError } from '@platformatic/utils'
4
- import { tracingChannel } from 'node:diagnostics_channel'
5
- import { once } from 'node:events'
1
+ import {
2
+ buildPinoFormatters,
3
+ buildPinoTimestamp,
4
+ disablePinoDirectWrite,
5
+ ensureLoggableError,
6
+ features
7
+ } from '@platformatic/foundation'
8
+ import { ITC } from '@platformatic/itc/lib/index.js'
9
+ import { client, collectMetrics } from '@platformatic/metrics'
10
+ import diagnosticChannel, { tracingChannel } from 'node:diagnostics_channel'
11
+ import { EventEmitter, once } from 'node:events'
6
12
  import { readFile } from 'node:fs/promises'
13
+ import { ServerResponse } from 'node:http'
7
14
  import { register } from 'node:module'
8
- import { platform, tmpdir } from 'node:os'
15
+ import { hostname, platform, tmpdir } from 'node:os'
9
16
  import { basename, resolve } from 'node:path'
10
- import { fileURLToPath } from 'node:url'
11
17
  import { isMainThread } from 'node:worker_threads'
12
18
  import pino from 'pino'
13
- import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
19
+ import { Agent, Pool, setGlobalDispatcher } from 'undici'
14
20
  import { WebSocket } from 'ws'
15
21
  import { exitCodes } from '../errors.js'
16
22
  import { importFile } from '../utils.js'
17
- import { getSocketPath, isWindows } from './child-manager.js'
23
+ import { getSocketPath } from './child-manager.js'
24
+
25
+ const windowsNpmExecutables = ['npm-prefix.js', 'npm-cli.js']
26
+
27
+ function createInterceptor () {
28
+ const pool = new Pool(
29
+ {
30
+ hostname: 'localhost',
31
+ protocol: 'http:'
32
+ },
33
+ {
34
+ socketPath: getSocketPath(process.env.PLT_MANAGER_ID),
35
+ keepAliveTimeout: 10,
36
+ keepAliveMaxTimeout: 10
37
+ }
38
+ )
18
39
 
19
- function createInterceptor (itc) {
20
40
  return function (dispatch) {
21
- return async (opts, handler) => {
41
+ return (opts, handler) => {
22
42
  let url = opts.origin
23
43
  if (!(url instanceof URL)) {
24
44
  url = new URL(opts.path, url)
@@ -29,49 +49,17 @@ function createInterceptor (itc) {
29
49
  return dispatch(opts, handler)
30
50
  }
31
51
 
32
- const headers = {
33
- ...opts?.headers
34
- }
35
-
36
- delete headers.connection
37
- delete headers['transfer-encoding']
38
- headers.host = url.host
39
-
40
- const requestOpts = {
41
- ...opts,
42
- headers
43
- }
44
- delete requestOpts.dispatcher
45
-
46
- itc
47
- .send('fetch', requestOpts)
48
- .then(res => {
49
- if (res.rawPayload && !Buffer.isBuffer(res.rawPayload)) {
50
- res.rawPayload = Buffer.from(res.rawPayload.data)
51
- }
52
-
53
- const headers = []
54
- for (const [key, value] of Object.entries(res.headers)) {
55
- if (Array.isArray(value)) {
56
- for (const v of value) {
57
- headers.push(key)
58
- headers.push(v)
59
- }
60
- } else {
61
- headers.push(key)
62
- headers.push(value)
63
- }
52
+ // Route the call via the UNIX socket
53
+ return pool.dispatch(
54
+ {
55
+ ...opts,
56
+ headers: {
57
+ host: url.host,
58
+ ...opts?.headers
64
59
  }
65
-
66
- handler.onHeaders(res.statusCode, headers, () => {}, res.statusMessage)
67
- handler.onData(res.rawPayload)
68
- handler.onComplete([])
69
- })
70
- .catch(e => {
71
- handler.onError(new Error(e.message))
72
- })
73
-
74
- return true
60
+ },
61
+ handler
62
+ )
75
63
  }
76
64
  }
77
65
  }
@@ -79,30 +67,90 @@ function createInterceptor (itc) {
79
67
  export class ChildProcess extends ITC {
80
68
  #listener
81
69
  #socket
82
- #child
83
70
  #logger
71
+ #metricsRegistry
84
72
  #pendingMessages
85
73
 
86
74
  constructor () {
87
- super({ throwOnMissingHandler: false, name: `${process.env.PLT_MANAGER_ID}-child-process` })
75
+ super({
76
+ throwOnMissingHandler: false,
77
+ name: `${process.env.PLT_MANAGER_ID}-child-process`,
78
+ handlers: {
79
+ collectMetrics: (...args) => {
80
+ return this.#collectMetrics(...args)
81
+ },
82
+ getMetrics: (...args) => {
83
+ return this.#getMetrics(...args)
84
+ },
85
+ close: signal => {
86
+ let handled = false
87
+
88
+ try {
89
+ handled = globalThis.platformatic.events.emit('close', signal)
90
+ } catch (error) {
91
+ this.#logger.error({ err: ensureLoggableError(error) }, 'Error while handling close event.')
92
+ process.exitCode = 1
93
+ }
94
+
95
+ if (!handled) {
96
+ // No user event, just exit without errors
97
+ setImmediate(() => {
98
+ process.exit(process.exitCode ?? 0)
99
+ })
100
+ }
101
+
102
+ return handled
103
+ }
104
+ }
105
+ })
88
106
 
89
107
  /* c8 ignore next */
90
108
  const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
91
109
  this.#socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
92
110
  this.#pendingMessages = []
111
+ this.#metricsRegistry = new client.Registry()
93
112
 
94
113
  this.listen()
95
114
  this.#setupLogger()
96
- this.#setupTelemetry()
97
- this.#setupHandlers()
115
+
116
+ if (globalThis.platformatic.exitOnUnhandledErrors) {
117
+ this.#setupHandlers()
118
+ }
119
+
98
120
  this.#setupServer()
99
121
  this.#setupInterceptors()
100
122
 
101
- this.on('close', signal => {
102
- process.kill(process.pid, signal)
123
+ this.registerGlobals({
124
+ logger: this.#logger,
125
+ setOpenapiSchema: this.setOpenapiSchema.bind(this),
126
+ setGraphqlSchema: this.setGraphqlSchema.bind(this),
127
+ setConnectionString: this.setConnectionString.bind(this),
128
+ setBasePath: this.setBasePath.bind(this),
129
+ prometheus: { client, registry: this.#metricsRegistry },
130
+ notifyConfig: this.#notifyConfig.bind(this)
103
131
  })
104
132
  }
105
133
 
134
+ registerGlobals (globals) {
135
+ globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, globals)
136
+ }
137
+
138
+ setOpenapiSchema (schema) {
139
+ this.notify('openapiSchema', schema)
140
+ }
141
+
142
+ setGraphqlSchema (schema) {
143
+ this.notify('graphqlSchema', schema)
144
+ }
145
+
146
+ setConnectionString (connectionString) {
147
+ this.notify('connectionString', connectionString)
148
+ }
149
+
150
+ setBasePath (basePath) {
151
+ this.notify('basePath', basePath)
152
+ }
153
+
106
154
  _setupListener (listener) {
107
155
  this.#listener = listener
108
156
 
@@ -152,31 +200,138 @@ export class ChildProcess extends ITC {
152
200
  this.#socket.close()
153
201
  }
154
202
 
155
- #setupLogger () {
156
- // Since this is executed by user code, make sure we only override this in the main thread
157
- // The rest will be intercepted by the BaseStackable.
158
- if (isMainThread) {
159
- this.#logger = pino({
160
- level: 'info',
161
- name: globalThis.platformatic.id,
162
- transport: {
163
- target: new URL('./child-transport.js', import.meta.url).toString(),
164
- options: { id: globalThis.platformatic.id }
165
- }
166
- })
203
+ async #collectMetrics ({ applicationId, workerId, metricsConfig }) {
204
+ await collectMetrics(applicationId, workerId, metricsConfig, this.#metricsRegistry)
205
+ this.#setHttpCacheMetrics()
206
+ }
207
+
208
+ #setHttpCacheMetrics () {
209
+ const { client, registry } = globalThis.platformatic.prometheus
210
+
211
+ const cacheHitMetric = new client.Counter({
212
+ name: 'http_cache_hit_count',
213
+ help: 'Number of http cache hits',
214
+ registers: [registry]
215
+ })
216
+
217
+ const cacheMissMetric = new client.Counter({
218
+ name: 'http_cache_miss_count',
219
+ help: 'Number of http cache misses',
220
+ registers: [registry]
221
+ })
222
+
223
+ globalThis.platformatic.onHttpCacheHit = () => {
224
+ cacheHitMetric.inc()
225
+ }
226
+ globalThis.platformatic.onHttpCacheMiss = () => {
227
+ cacheMissMetric.inc()
228
+ }
229
+
230
+ const httpStatsFreeMetric = new client.Gauge({
231
+ name: 'http_client_stats_free',
232
+ help: 'Number of free (idle) http clients (sockets)',
233
+ labelNames: ['dispatcher_stats_url'],
234
+ registers: [registry]
235
+ })
236
+ globalThis.platformatic.onHttpStatsFree = (url, val) => {
237
+ httpStatsFreeMetric.set({ dispatcher_stats_url: url }, val)
238
+ }
239
+
240
+ const httpStatsConnectedMetric = new client.Gauge({
241
+ name: 'http_client_stats_connected',
242
+ help: 'Number of open socket connections',
243
+ labelNames: ['dispatcher_stats_url'],
244
+ registers: [registry]
245
+ })
246
+ globalThis.platformatic.onHttpStatsConnected = (url, val) => {
247
+ httpStatsConnectedMetric.set({ dispatcher_stats_url: url }, val)
248
+ }
167
249
 
168
- Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(this.#logger, 'info') })
169
- Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(this.#logger, 'error', true) })
170
- } else {
171
- this.#logger = pino({ level: 'info', name: globalThis.platformatic.id })
250
+ const httpStatsPendingMetric = new client.Gauge({
251
+ name: 'http_client_stats_pending',
252
+ help: 'Number of pending requests across all clients',
253
+ labelNames: ['dispatcher_stats_url'],
254
+ registers: [registry]
255
+ })
256
+ globalThis.platformatic.onHttpStatsPending = (url, val) => {
257
+ httpStatsPendingMetric.set({ dispatcher_stats_url: url }, val)
258
+ }
259
+
260
+ const httpStatsQueuedMetric = new client.Gauge({
261
+ name: 'http_client_stats_queued',
262
+ help: 'Number of queued requests across all clients',
263
+ labelNames: ['dispatcher_stats_url'],
264
+ registers: [registry]
265
+ })
266
+ globalThis.platformatic.onHttpStatsQueued = (url, val) => {
267
+ httpStatsQueuedMetric.set({ dispatcher_stats_url: url }, val)
268
+ }
269
+
270
+ const httpStatsRunningMetric = new client.Gauge({
271
+ name: 'http_client_stats_running',
272
+ help: 'Number of currently active requests across all clients',
273
+ labelNames: ['dispatcher_stats_url'],
274
+ registers: [registry]
275
+ })
276
+ globalThis.platformatic.onHttpStatsRunning = (url, val) => {
277
+ httpStatsRunningMetric.set({ dispatcher_stats_url: url }, val)
172
278
  }
279
+
280
+ const httpStatsSizeMetric = new client.Gauge({
281
+ name: 'http_client_stats_size',
282
+ help: 'Number of active, pending, or queued requests across all clients',
283
+ labelNames: ['dispatcher_stats_url'],
284
+ registers: [registry]
285
+ })
286
+ globalThis.platformatic.onHttpStatsSize = (url, val) => {
287
+ httpStatsSizeMetric.set({ dispatcher_stats_url: url }, val)
288
+ }
289
+
290
+ const activeResourcesEventLoopMetric = new client.Gauge({
291
+ name: 'active_resources_event_loop',
292
+ help: 'Number of active resources keeping the event loop alive',
293
+ registers: [registry]
294
+ })
295
+ globalThis.platformatic.onActiveResourcesEventLoop = val => activeResourcesEventLoopMetric.set(val)
296
+ }
297
+
298
+ async #getMetrics ({ format } = {}) {
299
+ const res =
300
+ format === 'json' ? await this.#metricsRegistry.getMetricsAsJSON() : await this.#metricsRegistry.metrics()
301
+
302
+ return res
173
303
  }
174
304
 
175
- /* c8 ignore next 5 */
176
- #setupTelemetry () {
177
- if (globalThis.platformatic.telemetry) {
178
- setupNodeHTTPTelemetry(globalThis.platformatic.telemetry, this.#logger)
305
+ #setupLogger () {
306
+ disablePinoDirectWrite()
307
+
308
+ // Since this is executed by user code, make sure we only override this in the main thread
309
+ // The rest will be intercepted by the BaseCapability.
310
+ const loggerOptions = globalThis.platformatic?.config?.logger ?? {}
311
+ const pinoOptions = {
312
+ ...loggerOptions,
313
+ level: loggerOptions.level ?? 'info',
314
+ name: globalThis.platformatic.applicationId
315
+ }
316
+ if (loggerOptions.formatters) {
317
+ pinoOptions.formatters = buildPinoFormatters(loggerOptions.formatters)
318
+ }
319
+ if (loggerOptions.timestamp) {
320
+ pinoOptions.timestamp = buildPinoTimestamp(loggerOptions.timestamp)
321
+ }
322
+
323
+ if (loggerOptions.base !== null && typeof globalThis.platformatic.workerId !== 'undefined') {
324
+ pinoOptions.base = {
325
+ ...(pinoOptions.base ?? {}),
326
+ pid: process.pid,
327
+ hostname: hostname(),
328
+ worker: parseInt(globalThis.platformatic.workerId)
329
+ }
330
+ } else if (loggerOptions.base === null) {
331
+ pinoOptions.base = undefined
179
332
  }
333
+
334
+ this.#logger = pino(pinoOptions)
180
335
  }
181
336
 
182
337
  #setupServer () {
@@ -191,8 +346,14 @@ export class ChildProcess extends ITC {
191
346
  const host = globalThis.platformatic.host
192
347
 
193
348
  if (port !== false) {
194
- options.port = typeof port === 'number' ? port : 0
349
+ const hasFixedPort = typeof port === 'number'
350
+ options.port = hasFixedPort ? port : 0
351
+
352
+ if (hasFixedPort && features.node.reusePort) {
353
+ options.reusePort = true
354
+ }
195
355
  }
356
+
196
357
  if (typeof host === 'string') {
197
358
  options.host = host
198
359
  }
@@ -220,18 +381,26 @@ export class ChildProcess extends ITC {
220
381
  }
221
382
 
222
383
  tracingChannel('net.server.listen').subscribe(subscribers)
384
+
385
+ const { isEntrypoint, runtimeBasePath, wantsAbsoluteUrls } = globalThis.platformatic
386
+ if (isEntrypoint && runtimeBasePath && !wantsAbsoluteUrls) {
387
+ stripBasePath(runtimeBasePath)
388
+ }
223
389
  }
224
390
 
225
391
  #setupInterceptors () {
226
- setGlobalDispatcher(getGlobalDispatcher().compose(createInterceptor(this)))
392
+ const globalDispatcher = new Agent().compose(createInterceptor(this))
393
+ setGlobalDispatcher(globalDispatcher)
227
394
  }
228
395
 
229
396
  #setupHandlers () {
397
+ const errorLabel =
398
+ typeof globalThis.platformatic.workerId !== 'undefined'
399
+ ? `worker ${globalThis.platformatic.workerId} of the application "${globalThis.platformatic.applicationId}"`
400
+ : `application "${globalThis.platformatic.applicationId}"`
401
+
230
402
  function handleUnhandled (type, err) {
231
- this.#logger.error(
232
- { err: ensureLoggableError(err) },
233
- `Child process for service ${globalThis.platformatic.id} threw an ${type}.`
234
- )
403
+ this.#logger.error({ err: ensureLoggableError(err) }, `Child process for the ${errorLabel} threw an ${type}.`)
235
404
 
236
405
  // Give some time to the logger and ITC notifications to land before shutting down
237
406
  setTimeout(() => process.exit(exitCodes.PROCESS_UNHANDLED_ERROR), 100)
@@ -239,18 +408,79 @@ export class ChildProcess extends ITC {
239
408
 
240
409
  process.on('uncaughtException', handleUnhandled.bind(this, 'uncaught exception'))
241
410
  process.on('unhandledRejection', handleUnhandled.bind(this, 'unhandled rejection'))
411
+
412
+ process.on('newListener', event => {
413
+ if (event === 'uncaughtException' || event === 'unhandledRejection') {
414
+ this.#logger.warn(
415
+ `A listener has been added for the "process.${event}" event. This listener will be never triggered as Watt default behavior will kill the process before.\n To disable this behavior, set "exitOnUnhandledErrors" to false in the runtime config.`
416
+ )
417
+ }
418
+ })
419
+ }
420
+
421
+ #notifyConfig (config) {
422
+ this.notify('config', config)
423
+ }
424
+ }
425
+
426
+ function stripBasePath (basePath) {
427
+ const kBasePath = Symbol('kBasePath')
428
+
429
+ diagnosticChannel.subscribe('http.server.request.start', ({ request, response }) => {
430
+ if (request.url.startsWith(basePath)) {
431
+ request.url = request.url.slice(basePath.length)
432
+
433
+ if (request.url.charAt(0) !== '/') {
434
+ request.url = '/' + request.url
435
+ }
436
+
437
+ response[kBasePath] = basePath
438
+ }
439
+ })
440
+
441
+ const originWriteHead = ServerResponse.prototype.writeHead
442
+ const originSetHeader = ServerResponse.prototype.setHeader
443
+
444
+ ServerResponse.prototype.writeHead = function (statusCode, statusMessage, headers) {
445
+ if (this[kBasePath] !== undefined) {
446
+ if (headers === undefined && typeof statusMessage === 'object') {
447
+ headers = statusMessage
448
+ statusMessage = undefined
449
+ }
450
+
451
+ if (headers) {
452
+ for (const key in headers) {
453
+ if (key.toLowerCase() === 'location' && !headers[key].startsWith(basePath)) {
454
+ headers[key] = basePath + headers[key]
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ return originWriteHead.call(this, statusCode, statusMessage, headers)
461
+ }
462
+
463
+ ServerResponse.prototype.setHeader = function (name, value) {
464
+ if (this[kBasePath]) {
465
+ if (name.toLowerCase() === 'location' && !value.startsWith(basePath)) {
466
+ value = basePath + value
467
+ }
468
+ }
469
+ originSetHeader.call(this, name, value)
242
470
  }
243
471
  }
244
472
 
245
473
  async function main () {
474
+ const executable = basename(process.argv[1] ?? '')
475
+ if (!isMainThread || windowsNpmExecutables.includes(executable)) {
476
+ return
477
+ }
478
+
246
479
  const dataPath = resolve(tmpdir(), 'platformatic', 'runtimes', `${process.env.PLT_MANAGER_ID}.json`)
247
480
  const { data, loader, scripts } = JSON.parse(await readFile(dataPath))
248
481
 
249
482
  globalThis.platformatic = data
250
-
251
- if (data.root && isMainThread) {
252
- process.chdir(fileURLToPath(data.root))
253
- }
483
+ globalThis.platformatic.events = new EventEmitter()
254
484
 
255
485
  if (loader) {
256
486
  register(loader, { data })
@@ -263,7 +493,4 @@ async function main () {
263
493
  globalThis[Symbol.for('plt.children.itc')] = new ChildProcess()
264
494
  }
265
495
 
266
- /* c8 ignore next 3 */
267
- if (!isWindows || basename(process.argv.at(-1)) !== 'npm-prefix.js') {
268
- await main()
269
- }
496
+ await main()
@@ -1,8 +1,8 @@
1
- import { withResolvers } from '@platformatic/utils'
1
+ import { features } from '@platformatic/foundation'
2
2
  import { subscribe, tracingChannel, unsubscribe } from 'node:diagnostics_channel'
3
3
 
4
4
  export function createServerListener (overridePort = true, overrideHost) {
5
- const { promise, resolve, reject } = withResolvers()
5
+ const { promise, resolve, reject } = Promise.withResolvers()
6
6
 
7
7
  const subscribers = {
8
8
  asyncStart ({ options }) {
@@ -12,8 +12,14 @@ export function createServerListener (overridePort = true, overrideHost) {
12
12
  }
13
13
 
14
14
  if (overridePort !== false) {
15
- options.port = typeof overridePort === 'number' ? overridePort : 0
15
+ const hasFixedPort = typeof overridePort === 'number'
16
+ options.port = hasFixedPort ? overridePort : 0
17
+
18
+ if (hasFixedPort && features.node.reusePort) {
19
+ options.reusePort = true
20
+ }
16
21
  }
22
+
17
23
  if (typeof overrideHost === 'string') {
18
24
  options.host = overrideHost
19
25
  }
@@ -42,7 +48,7 @@ export function createServerListener (overridePort = true, overrideHost) {
42
48
  }
43
49
 
44
50
  export function createChildProcessListener () {
45
- const { promise, resolve } = withResolvers()
51
+ const { promise, resolve } = Promise.withResolvers()
46
52
 
47
53
  const handler = ({ process: child }) => {
48
54
  unsubscribe('child_process', handler)
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@platformatic/basic",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
6
7
  "type": "module",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/platformatic/platformatic.git"
10
11
  },
11
- "author": "Paolo Insogna <paolo@cowtech.it>",
12
+ "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
12
13
  "license": "Apache-2.0",
13
14
  "bugs": {
14
15
  "url": "https://github.com/platformatic/platformatic/issues"
@@ -17,36 +18,36 @@
17
18
  "dependencies": {
18
19
  "@fastify/error": "^4.0.0",
19
20
  "execa": "^9.3.1",
20
- "pino": "^9.3.2",
21
+ "fast-json-patch": "^3.1.1",
22
+ "pino": "^9.9.0",
21
23
  "pino-abstract-transport": "^2.0.0",
22
24
  "semver": "^7.6.3",
23
25
  "split2": "^4.2.0",
24
- "undici": "^6.19.5",
26
+ "undici": "^7.0.0",
25
27
  "ws": "^8.18.0",
26
- "@platformatic/config": "3.4.1",
27
- "@platformatic/itc": "3.4.1",
28
- "@platformatic/telemetry": "3.4.1",
29
- "@platformatic/utils": "3.4.1"
28
+ "@platformatic/foundation": "3.5.0",
29
+ "@platformatic/itc": "3.5.0",
30
+ "@platformatic/metrics": "3.5.0",
31
+ "@platformatic/telemetry": "3.5.0"
30
32
  },
31
33
  "devDependencies": {
32
- "borp": "^0.17.0",
34
+ "cleaner-spec-reporter": "^0.5.0",
33
35
  "eslint": "9",
34
36
  "express": "^4.19.2",
35
37
  "fastify": "^5.0.0",
38
+ "get-port": "^7.1.0",
36
39
  "json-schema-to-typescript": "^15.0.0",
37
- "neostandard": "^0.11.1",
38
- "next": "^14.2.5",
39
- "react": "^18.3.1",
40
- "react-dom": "^18.3.1",
41
- "typescript": "^5.5.4",
42
- "vite": "^5.4.0"
40
+ "neostandard": "^0.12.0",
41
+ "typescript": "^5.5.4"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.19.0"
43
45
  },
44
46
  "scripts": {
45
- "test": "npm run lint && borp --concurrency=1 --no-timeout",
46
- "coverage": "npm run lint && borp -C -X test -X test/fixtures --concurrency=1 --no-timeout",
47
+ "test": "node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js test/**/*.test.js",
47
48
  "gen-schema": "node lib/schema.js > schema.json",
48
49
  "gen-types": "json2ts > config.d.ts < schema.json",
49
- "build": "pnpm run gen-schema && pnpm run gen-types",
50
+ "build": "npm run gen-schema && npm run gen-types",
50
51
  "lint": "eslint"
51
52
  }
52
53
  }