@btravstack/core 0.3.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/dist/index.mjs ADDED
@@ -0,0 +1,567 @@
1
+ import { Err, Ok, OkAsync, P, TaggedError, fromSafePromise } from "unthrown";
2
+ import { Module, Port, Provider } from "@btravstack/di";
3
+ import { Config, ConfigInvalid, Env } from "@btravstack/config";
4
+ import { createServer } from "node:http";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ //#region src/clock.ts
7
+ const systemClock = {
8
+ now: () => Date.now(),
9
+ sleep: (ms, signal) => fromSafePromise(new Promise((resolve) => {
10
+ if (signal?.aborted === true) {
11
+ resolve();
12
+ return;
13
+ }
14
+ const timer = setTimeout(() => {
15
+ signal?.removeEventListener("abort", onAbort);
16
+ resolve();
17
+ }, ms);
18
+ timer.unref?.();
19
+ function onAbort() {
20
+ clearTimeout(timer);
21
+ resolve();
22
+ }
23
+ signal?.addEventListener("abort", onAbort, { once: true });
24
+ }))
25
+ };
26
+ //#endregion
27
+ //#region src/events.ts
28
+ const withErrors = (_key, value) => value instanceof Error ? {
29
+ name: value.name,
30
+ message: value.message,
31
+ stack: value.stack,
32
+ cause: value.cause
33
+ } : value;
34
+ const render = (event) => {
35
+ try {
36
+ return JSON.stringify(event, withErrors);
37
+ } catch {
38
+ return JSON.stringify({
39
+ type: event.type,
40
+ cause: "[unserialisable]"
41
+ });
42
+ }
43
+ };
44
+ const stderrSink = (event) => {
45
+ process.stderr.write(`${render(event)}\n`);
46
+ };
47
+ const safeSink = (sink) => (event) => {
48
+ try {
49
+ sink(event);
50
+ } catch {}
51
+ };
52
+ //#endregion
53
+ //#region src/observability.ts
54
+ /** The levels in order, least severe first — what `isEnabled` compares through. */
55
+ const LEVELS = [
56
+ "trace",
57
+ "debug",
58
+ "info",
59
+ "warn",
60
+ "error",
61
+ "fatal"
62
+ ];
63
+ /**
64
+ * The application's logger, as a port — declared here and implemented in
65
+ * `@btravstack/observability`, because a contract every package may depend on
66
+ * has to be reachable without installing an implementation, and because the
67
+ * correlation an implementation stamps per line is the kernel's own ambient
68
+ * record.
69
+ *
70
+ * An implementation must not throw: a logger that throws turns an observability
71
+ * problem into an outage. Synchronous `void`, not an `AsyncResult` — thesis 6's
72
+ * one deliberate exemption, since a lost line is not a modeled error.
73
+ */
74
+ var Logger = class extends Port("Logger") {};
75
+ /** A span's outcome: unset until something says otherwise, then `ok` or `error`. */
76
+ const SPAN_STATUS = {
77
+ unset: 0,
78
+ ok: 1,
79
+ error: 2
80
+ };
81
+ /**
82
+ * The application's tracer, as a port — declared without naming OpenTelemetry,
83
+ * because a port typed as a vendor's type points the dependency arrow outwards
84
+ * and makes the whole family install that vendor.
85
+ */
86
+ var Tracer = class extends Port("Tracer") {};
87
+ /** The application's meter, as a port. See {@link Tracer} for why it does not name a vendor. */
88
+ var Meter = class extends Port("Meter") {};
89
+ //#endregion
90
+ //#region src/runtime.ts
91
+ var RuntimeStartFailed = class extends TaggedError("RuntimeStartFailed") {
92
+ message = `the ${this.runtime} runtime failed to start`;
93
+ };
94
+ /**
95
+ * The port the kernel resolves its runtime from. A runtime is a **service the
96
+ * module provides**, not an option handed to `start`: a runtime package
97
+ * declares its own port over this one — `class HttpRuntime extends
98
+ * RuntimePort<Runtime<never, HttpInfo>> {}` — so it is built by di like
99
+ * everything else, and the kernel owns nothing but the lifecycle.
100
+ *
101
+ * Left generic on purpose: every runtime port is one id at runtime, since a
102
+ * process boots exactly one, while each carries its own `Resolves`/`Info` in
103
+ * the type.
104
+ */
105
+ const RuntimePort = Port("Runtime");
106
+ /**
107
+ * `running`, but no later than the kernel's drain deadline — the primitive a
108
+ * `Serving.drain` needs when the work it awaits settles on somebody else's
109
+ * clock (Temporal's `shutdownForceTime`, a broker library's close) and so
110
+ * cannot honour `signal` itself.
111
+ *
112
+ * **The losing branch's `Result` is dropped**, which is the point rather than
113
+ * an oversight: once the deadline wins, the kernel has already moved on and
114
+ * the eventual outcome has no consumer left. What that costs is the runtime's
115
+ * own business — an un-acked AMQP delivery is redelivered, so abandoning one
116
+ * repeats work rather than losing it, while a Temporal activity is retried on
117
+ * another worker.
118
+ *
119
+ * Deliberately **`Clock`-agnostic**: there is no duration here, only a signal,
120
+ * so it behaves identically under `@btravstack/testing`'s fake clock. Racing
121
+ * work against a *timeout* is a different primitive and belongs on `Clock`
122
+ * (`drain.ts` uses `clock.sleep` for exactly that, so a fake clock can control
123
+ * it) — do not fold the two together.
124
+ */
125
+ const releasedBy = (signal, running) => fromSafePromise(Promise.race([running, whenAborted(signal)])).flatMap((settled) => settled);
126
+ const whenAborted = (signal) => signal.aborted ? OkAsync() : fromSafePromise(new Promise((resolve) => {
127
+ signal.addEventListener("abort", () => resolve(), { once: true });
128
+ }));
129
+ //#endregion
130
+ //#region src/deferred.ts
131
+ const createDeferred = () => {
132
+ let resolve;
133
+ return {
134
+ promise: new Promise((r) => {
135
+ resolve = r;
136
+ }),
137
+ resolve
138
+ };
139
+ };
140
+ //#endregion
141
+ //#region src/drain.ts
142
+ const drainApp = (args) => {
143
+ args.onUnready();
144
+ const inFlightAtStart = args.registry.inFlight();
145
+ const closedAtStart = args.registry.closed();
146
+ const deadline = new AbortController();
147
+ return args.clock.sleep(args.preDrainDelayMs, args.skip).flatMap(() => {
148
+ const drainStopped = args.serving.drain(deadline.signal);
149
+ return fromSafePromise(Promise.race([drainStopped.flatMap(() => args.registry.awaitIdle()), args.clock.sleep(args.drainTimeoutMs, args.skip)])).flatMap((raced) => {
150
+ deadline.abort();
151
+ return raced.map(() => {
152
+ const abandoned = args.registry.inFlight();
153
+ if (abandoned > 0) args.registry.abortAll();
154
+ return {
155
+ inFlightAtStart,
156
+ completed: args.registry.closed() - closedAtStart,
157
+ abandoned
158
+ };
159
+ });
160
+ });
161
+ });
162
+ };
163
+ //#endregion
164
+ //#region src/phase.ts
165
+ const ORDER = [
166
+ "building",
167
+ "starting",
168
+ "serving",
169
+ "draining",
170
+ "stopping",
171
+ "exited"
172
+ ];
173
+ const rank = (phase) => ORDER.indexOf(phase);
174
+ const createPhaseTracker = (onChange) => {
175
+ let phase = "building";
176
+ return {
177
+ current: () => phase,
178
+ advanceTo: (next) => {
179
+ if (rank(next) <= rank(phase)) return false;
180
+ phase = next;
181
+ onChange(next);
182
+ return true;
183
+ }
184
+ };
185
+ };
186
+ //#endregion
187
+ //#region src/probes.ts
188
+ const startProbeServer = (args) => fromSafePromise(new Promise((resolve) => {
189
+ const server = createServer((request, response) => {
190
+ if (request.url === "/livez") {
191
+ respond(response, args.live(), "ok");
192
+ return;
193
+ }
194
+ if (request.url === "/readyz") {
195
+ respond(response, args.ready(), "ready");
196
+ return;
197
+ }
198
+ response.writeHead(404).end();
199
+ });
200
+ const onBindError = (cause) => {
201
+ resolve(Err(new RuntimeStartFailed({
202
+ runtime: "probes",
203
+ cause
204
+ })));
205
+ };
206
+ const ignoreServingError = () => {};
207
+ server.once("error", onBindError);
208
+ try {
209
+ server.listen(args.port, "127.0.0.1", () => {
210
+ server.removeListener("error", onBindError);
211
+ server.on("error", ignoreServingError);
212
+ const address = server.address();
213
+ const port = typeof address === "object" && address !== null ? address.port : args.port;
214
+ resolve(Ok({
215
+ port,
216
+ close: () => fromSafePromise(new Promise((done) => server.close(() => done())))
217
+ }));
218
+ });
219
+ } catch (cause) {
220
+ onBindError(cause);
221
+ }
222
+ server.unref();
223
+ })).flatMap((result) => result);
224
+ const respond = (response, healthy, body) => {
225
+ if (healthy) {
226
+ response.writeHead(200).end(body);
227
+ return;
228
+ }
229
+ response.writeHead(503).end("unavailable");
230
+ };
231
+ //#endregion
232
+ //#region src/process-handlers.ts
233
+ const SIGNALS = ["SIGTERM", "SIGINT"];
234
+ const installSignalHandlers = (handlers) => {
235
+ let seen = 0;
236
+ const onSignal = () => {
237
+ seen += 1;
238
+ if (seen === 1) {
239
+ handlers.onFirst();
240
+ return;
241
+ }
242
+ handlers.onSecond();
243
+ };
244
+ for (const signal of SIGNALS) process.on(signal, onSignal);
245
+ return () => {
246
+ for (const signal of SIGNALS) process.off(signal, onSignal);
247
+ };
248
+ };
249
+ const installUncaughtHandlers = (onUncaught) => {
250
+ let reported = false;
251
+ const report = (cause) => {
252
+ if (reported) return;
253
+ reported = true;
254
+ onUncaught(cause);
255
+ };
256
+ const onException = (error) => report(error);
257
+ const onRejection = (reason) => report(reason);
258
+ process.on("uncaughtException", onException);
259
+ process.on("unhandledRejection", onRejection);
260
+ return () => {
261
+ process.off("uncaughtException", onException);
262
+ process.off("unhandledRejection", onRejection);
263
+ };
264
+ };
265
+ //#endregion
266
+ //#region src/units.ts
267
+ const storage = new AsyncLocalStorage();
268
+ const runWithUnit = (record, fn) => storage.run(record, fn);
269
+ const currentUnit = () => storage.getStore();
270
+ let counter = 0;
271
+ const nextId = () => {
272
+ counter += 1;
273
+ return `u${counter}`;
274
+ };
275
+ const createUnitRegistry = () => {
276
+ const open = /* @__PURE__ */ new Set();
277
+ const idleWaiters = /* @__PURE__ */ new Set();
278
+ let closedCount = 0;
279
+ const settleIfIdle = () => {
280
+ if (open.size > 0) return;
281
+ for (const waiter of idleWaiters) waiter();
282
+ idleWaiters.clear();
283
+ };
284
+ return {
285
+ run: (meta, work) => {
286
+ const controller = new AbortController();
287
+ open.add(controller);
288
+ const record = {
289
+ unitId: nextId(),
290
+ traceId: meta.traceId ?? meta.id,
291
+ tenantId: meta.tenantId,
292
+ deadline: meta.deadline,
293
+ signal: controller.signal
294
+ };
295
+ return fromSafePromise(runWithUnit(record, async () => {
296
+ try {
297
+ return await work(controller.signal);
298
+ } finally {
299
+ open.delete(controller);
300
+ closedCount += 1;
301
+ settleIfIdle();
302
+ }
303
+ })).flatMap((result) => result);
304
+ },
305
+ inFlight: () => open.size,
306
+ closed: () => closedCount,
307
+ abortAll: () => {
308
+ for (const controller of open) controller.abort();
309
+ },
310
+ awaitIdle: () => open.size === 0 ? OkAsync() : fromSafePromise(new Promise((resolve) => {
311
+ idleWaiters.add(resolve);
312
+ }))
313
+ };
314
+ };
315
+ //#endregion
316
+ //#region src/start.ts
317
+ const providesEnv = (module) => module.provides.some((provider) => provider.port.portId === Env.portId) || module.imports.some(providesEnv);
318
+ const start = (module, options = {}) => {
319
+ const clock = options.clock ?? systemClock;
320
+ const env = options.env ?? process.env;
321
+ const emit = safeSink(options.onEvent ?? stderrSink);
322
+ let runtimeName = "";
323
+ let servingInfo = void 0;
324
+ let probeBoundPort = void 0;
325
+ const tracker = createPhaseTracker((phase) => {
326
+ if (phase === "serving") emit({
327
+ type: "serving",
328
+ runtime: runtimeName,
329
+ info: servingInfo,
330
+ probePort: probeBoundPort
331
+ });
332
+ if (phase === "stopping") emit({ type: "stopping" });
333
+ if (phase === "exited") emit({ type: "exited" });
334
+ });
335
+ const registry = createUnitRegistry();
336
+ const shutdown = createDeferred();
337
+ const teardownErrors = [];
338
+ const startedAt = clock.now();
339
+ const preDrainDelayMs = options.preDrainDelayMs ?? 5e3;
340
+ const drainTimeoutMs = options.drainTimeoutMs ?? 2e4;
341
+ const skipDrain = new AbortController();
342
+ let shutdownRequestedAt = startedAt;
343
+ let shutdownRequested = false;
344
+ const sinceShutdownRequested = () => clock.now() - shutdownRequestedAt;
345
+ const requestShutdown = (reason) => {
346
+ if (!shutdownRequested) {
347
+ shutdownRequested = true;
348
+ shutdownRequestedAt = clock.now();
349
+ }
350
+ shutdown.resolve(reason);
351
+ };
352
+ let forcedUnready = false;
353
+ const onUnready = () => {
354
+ forcedUnready = true;
355
+ };
356
+ const live = () => tracker.current() !== "exited";
357
+ const ready = () => tracker.current() === "serving" && !forcedUnready;
358
+ const disposeSignals = options.signals === false ? () => {} : installSignalHandlers({
359
+ onFirst: () => requestShutdown("signal"),
360
+ onSecond: () => skipDrain.abort()
361
+ });
362
+ const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
363
+ emit({
364
+ type: "uncaught",
365
+ cause
366
+ });
367
+ onUnready();
368
+ skipDrain.abort();
369
+ requestShutdown("uncaught");
370
+ });
371
+ let disposeProbes = () => {};
372
+ const probeBound = createDeferred();
373
+ const runtimePublished = createDeferred();
374
+ emit({ type: "building" });
375
+ const probesOptions = options.probes === void 0 ? Config.port("PROBE_PORT", { default: 9e3 }).parse(env["PROBE_PORT"]).map((port) => ({ port })).mapErrCases((matcher) => matcher.with(P.tag("ConfigFieldInvalid"), ({ reason }) => new RuntimeStartFailed({
376
+ runtime: "probes",
377
+ cause: new ConfigInvalid({
378
+ port: "probes",
379
+ issues: [{
380
+ message: reason,
381
+ path: ["PROBE_PORT"]
382
+ }]
383
+ })
384
+ }))) : Ok(options.probes);
385
+ if (options.probes === false) probeBound.resolve(void 0);
386
+ const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? OkAsync() : startProbeServer({
387
+ port: probes.port,
388
+ live,
389
+ ready
390
+ }).tap((server) => {
391
+ probeBoundPort = server.port;
392
+ probeBound.resolve(server.port);
393
+ disposeProbes = () => {
394
+ server.close();
395
+ };
396
+ }).discard()).tapFailure((failure) => {
397
+ emit({
398
+ type: "startFailed",
399
+ cause: failure.tag === "Err" ? failure.error : failure.cause
400
+ });
401
+ probeBound.resolve(void 0);
402
+ runtimePublished.resolve(void 0);
403
+ tracker.advanceTo("stopping");
404
+ disposeSignals();
405
+ disposeUncaught();
406
+ tracker.advanceTo("exited");
407
+ });
408
+ const root = Module("Kernel")({
409
+ imports: providesEnv(module) ? [module] : [module, Module("Environment")({
410
+ provides: [Provider(Env)({ value: env })],
411
+ exports: [Env]
412
+ })],
413
+ exports: [module]
414
+ });
415
+ const runDrain = (serving) => {
416
+ tracker.advanceTo("draining");
417
+ emit({
418
+ type: "draining",
419
+ inFlight: registry.inFlight()
420
+ });
421
+ return drainApp({
422
+ serving,
423
+ registry,
424
+ clock,
425
+ preDrainDelayMs: Math.max(0, preDrainDelayMs - sinceShutdownRequested()),
426
+ drainTimeoutMs,
427
+ skip: skipDrain.signal,
428
+ onUnready
429
+ }).tap((report) => emit({
430
+ type: "drained",
431
+ report
432
+ }));
433
+ };
434
+ const finish = (serving, reason) => {
435
+ if (reason !== "signal") registry.abortAll();
436
+ return (reason === "signal" ? runDrain(serving) : OkAsync(void 0)).flatMap((report) => {
437
+ tracker.advanceTo("stopping");
438
+ return serving.stop().map(() => {
439
+ disposeSignals();
440
+ disposeUncaught();
441
+ tracker.advanceTo("exited");
442
+ disposeProbes();
443
+ return {
444
+ reason,
445
+ drain: report,
446
+ teardownErrors,
447
+ uptimeMs: clock.now() - startedAt
448
+ };
449
+ });
450
+ });
451
+ };
452
+ return {
453
+ exited: probesStarted.flatMap(() => Module.scoped(root, (ctx) => {
454
+ tracker.advanceTo("starting");
455
+ const runtime = ctx.get(RuntimePort);
456
+ runtimeName = runtime.name;
457
+ const runtimeCtx = ctx;
458
+ const unit = options.unit;
459
+ const run = (meta, work) => registry.run(meta, (signal) => {
460
+ if (unit === void 0) return work(runtimeCtx, signal);
461
+ const fork = Module.forkScope;
462
+ return fork(ctx, unit, (forked) => fromSafePromise((async () => await work(forked, signal))()).flatMap((result) => result), { onTeardownError: (port, cause) => emit({
463
+ type: "teardownError",
464
+ port,
465
+ cause
466
+ }) });
467
+ });
468
+ const host = {
469
+ ctx: runtimeCtx,
470
+ run
471
+ };
472
+ return runtime.start(host).flatMap((serving) => {
473
+ servingInfo = serving.info;
474
+ tracker.advanceTo("serving");
475
+ runtimePublished.resolve(serving.info);
476
+ return fromSafePromise(shutdown.promise).flatMap((reason) => finish(serving, reason));
477
+ });
478
+ }, { onTeardownError: (port, cause) => {
479
+ teardownErrors.push({
480
+ port,
481
+ cause
482
+ });
483
+ emit({
484
+ type: "teardownError",
485
+ port,
486
+ cause
487
+ });
488
+ } }).tapFailure((failure) => {
489
+ if (tracker.current() !== "stopping") emit({
490
+ type: "startFailed",
491
+ cause: failure.tag === "Err" ? failure.error : failure.cause
492
+ });
493
+ runtimePublished.resolve(void 0);
494
+ tracker.advanceTo("stopping");
495
+ disposeSignals();
496
+ disposeUncaught();
497
+ tracker.advanceTo("exited");
498
+ disposeProbes();
499
+ })),
500
+ stop: () => requestShutdown("runtimeStopped"),
501
+ requestDrain: () => requestShutdown("signal"),
502
+ phase: tracker.current,
503
+ ready,
504
+ probePort: () => fromSafePromise(probeBound.promise),
505
+ runtimeInfo: () => fromSafePromise(runtimePublished.promise)
506
+ };
507
+ };
508
+ //#endregion
509
+ //#region src/run-main.ts
510
+ const EX_SOFTWARE = 70;
511
+ const EX_CONFIG = 78;
512
+ const isConfig = (error) => error instanceof ConfigInvalid || error instanceof RuntimeStartFailed && error.cause instanceof ConfigInvalid;
513
+ const codeFor = (report) => {
514
+ if (report.reason === "uncaught") return EX_SOFTWARE;
515
+ return (report.drain?.abandoned ?? 0) > 0 || report.teardownErrors.length > 0 ? 2 : 0;
516
+ };
517
+ /**
518
+ * The exit-code half of `runMain`, on its own so the code table can be asserted
519
+ * against hand-built reports without booting a kernel. Exported for
520
+ * `run-main.spec.ts` only, not from `index.ts`.
521
+ */
522
+ const awaitExit = async (app, exit) => {
523
+ exit((await app.exited).match({
524
+ ok: codeFor,
525
+ errCases: (matcher) => matcher.with(P._, (error) => isConfig(error) ? EX_CONFIG : 1),
526
+ defect: () => EX_SOFTWARE
527
+ }));
528
+ };
529
+ /**
530
+ * Boot a module and turn its outcome into a process exit code — the front
531
+ * door, and the one sanctioned place this package decides a process's fate.
532
+ * `start` composed with the wait for `exited`: use `start` instead when the
533
+ * `RunningApp` itself is wanted (a test, an embedder, a dev runner booting
534
+ * two applications — none of which may claim `process.exitCode`).
535
+ *
536
+ * `exit` is injectable and defaults to setting `process.exitCode`: `runMain`
537
+ * never calls `process.exit()`, so pending output is flushed, an embedding
538
+ * host keeps control of its own lifetime, and a test can observe the code
539
+ * without ending the run.
540
+ *
541
+ * | outcome | code |
542
+ * | --- | --- |
543
+ * | exited cleanly | `0` |
544
+ * | startup failure (a modeled `Err`) | `1` |
545
+ * | a configuration port that could not be bound (`ConfigInvalid`, `PROBE_PORT` included) | `78` |
546
+ * | drained with work abandoned | `2` |
547
+ * | exited with teardown errors | `2` |
548
+ * | stopped by an uncaught exception or unhandled rejection | `70` |
549
+ * | a defect | `70` |
550
+ *
551
+ * The two `70`s are the same statement reached through the two channels a bug
552
+ * can take. A crash takes precedence over abandoned work.
553
+ *
554
+ * @example
555
+ * ```ts
556
+ * // `OrderApi` imports the application next to `http()` and exports
557
+ * // `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
558
+ * await runMain(OrderApi);
559
+ * ```
560
+ */
561
+ const runMain = async (module, options = {}, exit = (code) => {
562
+ process.exitCode = code;
563
+ }) => {
564
+ await awaitExit(start(module, options), exit);
565
+ };
566
+ //#endregion
567
+ export { LEVELS, Logger, Meter, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, currentUnit, releasedBy, runMain, start, stderrSink, systemClock };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@btravstack/core",
3
+ "version": "0.3.0",
4
+ "description": "The application kernel: boot a di module into a running process, and stop it without losing work",
5
+ "keywords": [
6
+ "application",
7
+ "bootstrap",
8
+ "dependency-injection",
9
+ "graceful-shutdown",
10
+ "kernel",
11
+ "lifecycle",
12
+ "typescript",
13
+ "unthrown"
14
+ ],
15
+ "homepage": "https://github.com/btravstack/btravstack#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/btravstack/btravstack/issues"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/btravstack/btravstack.git",
24
+ "directory": "packages/core"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.mjs",
33
+ "types": "./dist/index.d.cts",
34
+ "exports": {
35
+ ".": {
36
+ "import": {
37
+ "types": "./dist/index.d.mts",
38
+ "default": "./dist/index.mjs"
39
+ },
40
+ "require": {
41
+ "types": "./dist/index.d.cts",
42
+ "default": "./dist/index.cjs"
43
+ }
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "devDependencies": {
48
+ "@btravstack/tsconfig": "0.2.0",
49
+ "@types/node": "26.2.0",
50
+ "@unthrown/vitest": "5.7.0",
51
+ "@vitest/coverage-v8": "4.1.11",
52
+ "tsdown": "0.22.14",
53
+ "typescript": "7.0.2",
54
+ "unthrown": "5.7.0",
55
+ "vitest": "4.1.11",
56
+ "@btravstack/config": "0.3.0",
57
+ "@btravstack/di": "0.3.0"
58
+ },
59
+ "peerDependencies": {
60
+ "unthrown": "^5.0.0",
61
+ "@btravstack/config": "^0.3.0",
62
+ "@btravstack/di": "^0.3.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=20"
66
+ },
67
+ "scripts": {
68
+ "build": "tsdown src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.build.json",
69
+ "dev": "tsdown src/index.ts --format cjs,esm --dts --watch --tsconfig tsconfig.build.json",
70
+ "generate": "node ../../docs/scripts/extract-doc-samples.ts --group core --out src/generated/doc-samples",
71
+ "test": "vitest run --coverage",
72
+ "test:types": "tsc --noEmit -p tsconfig.test-d.json",
73
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
74
+ }
75
+ }