@btravstack/core 0.18.0 → 0.20.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.cjs CHANGED
@@ -429,6 +429,51 @@ const installUncaughtHandlers = (onUncaught) => {
429
429
  };
430
430
  };
431
431
  //#endregion
432
+ //#region src/unit-overrides.ts
433
+ /**
434
+ * Providers substituted inside a runtime's unit modules, keyed by the kind
435
+ * the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
436
+ * `overridden(root, providers, { unit })` is the one contributor, and a
437
+ * production root never names it.
438
+ *
439
+ * Each provider must already carry di's override brand. The kernel applies
440
+ * them at boot, before the runtime starts: every kind named must be one the
441
+ * runtime binds, and every port must be one that kind's module provides, or
442
+ * the boot is a defect — the same drift gate a root-level override has.
443
+ */
444
+ var UnitOverrides = class extends _btravstack_di.Port.many("UnitOverrides") {};
445
+ /** Whether `module`'s tree carries a provider for `portId`. */
446
+ const provides = (module, portId) => module.provides.some((provider) => provider.port.portId === portId) || module.imports.some((imported) => provides(imported, portId));
447
+ /**
448
+ * Each overridden kind's module, mapped to the module the kernel forks in its
449
+ * place. Keyed by module IDENTITY, because `UnitHost.fork` is handed the
450
+ * module and never the kind — which is why a module bound under two kinds is
451
+ * refused rather than overridden under both.
452
+ */
453
+ const unitSubstitutes = (runtime, units, overrides) => {
454
+ const byKind = /* @__PURE__ */ new Map();
455
+ for (const member of overrides) for (const [kind, providers] of Object.entries(member)) byKind.set(kind, [...byKind.get(kind) ?? [], ...providers]);
456
+ const substitutes = /* @__PURE__ */ new Map();
457
+ for (const [kind, providers] of byKind) {
458
+ const module = units[kind];
459
+ if (module === void 0) throw new Error(`[core] unit override for kind "${kind}", which runtime "${runtime}" binds no module for`);
460
+ const shared = Object.keys(units).find((other) => other !== kind && units[other] === module);
461
+ if (shared !== void 0) throw new Error(`[core] unit override for kind "${kind}", whose module kind "${shared}" binds too — an override cannot reach one without the other`);
462
+ const seen = /* @__PURE__ */ new Set();
463
+ for (const provider of providers) {
464
+ if (seen.has(provider.port.portId)) throw new Error(`[core] two unit overrides registered for port ${JSON.stringify(provider.port.portId)} in kind "${kind}"`);
465
+ seen.add(provider.port.portId);
466
+ if (!provides(module, provider.port.portId)) throw new Error(`[core] unit override for port ${JSON.stringify(provider.port.portId)} in kind "${kind}" with nothing to override — its module no longer provides it`);
467
+ }
468
+ substitutes.set(module, (0, _btravstack_di.Module)("UnitOverridden")({
469
+ imports: [module],
470
+ provides: providers,
471
+ exports: [module]
472
+ }));
473
+ }
474
+ return substitutes;
475
+ };
476
+ //#endregion
432
477
  //#region src/units.ts
433
478
  const storage = new node_async_hooks.AsyncLocalStorage();
434
479
  const runWithUnit = (record, fn) => storage.run(record, fn);
@@ -498,7 +543,7 @@ const createUnitRegistry = () => {
498
543
  * and whose side effects then have to be guarded one by one.
499
544
  */
500
545
  const withdrawn = () => (0, unthrown.fromSafePromise)(new Promise(() => {}));
501
- const providesEnv = (module) => module.provides.some((provider) => provider.port.portId === _btravstack_config.Env.portId) || module.imports.some(providesEnv);
546
+ const providesEnv = (module) => provides(module, _btravstack_config.Env.portId);
502
547
  /**
503
548
  * What the kernel reads from the environment for itself. Each field is pinned
504
549
  * by the matching `StartOptions` field — explicit beats environment beats
@@ -545,7 +590,13 @@ const readKernelConfig = (options, env) => {
545
590
  const start = (module, options = {}) => {
546
591
  const clock = options.clock ?? systemClock;
547
592
  const env = options.env ?? process.env;
548
- const emit = safeSink(options.onEvent ?? stderrSink);
593
+ const sink = safeSink(options.onEvent ?? stderrSink);
594
+ let exitedEmitted = false;
595
+ const emit = (event) => {
596
+ if (exitedEmitted) return;
597
+ sink(event);
598
+ if (event.type === "exited") exitedEmitted = true;
599
+ };
549
600
  let runtimeName = "";
550
601
  let servingInfo = void 0;
551
602
  let probeBoundPort = void 0;
@@ -597,6 +648,7 @@ const start = (module, options = {}) => {
597
648
  }
598
649
  });
599
650
  const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
651
+ crashed = true;
600
652
  emit({
601
653
  type: "uncaught",
602
654
  cause
@@ -607,6 +659,8 @@ const start = (module, options = {}) => {
607
659
  abandonBuild("uncaught");
608
660
  });
609
661
  let disposeProbes = () => {};
662
+ let crashed = false;
663
+ let startupFailing = false;
610
664
  const disposeAll = () => {
611
665
  disposeSignals();
612
666
  disposeUncaught();
@@ -614,7 +668,7 @@ const start = (module, options = {}) => {
614
668
  disposeProbes();
615
669
  };
616
670
  const reportOf = (reason, drain, abandonedAt) => ({
617
- reason,
671
+ reason: crashed ? "uncaught" : reason,
618
672
  drain,
619
673
  teardownErrors,
620
674
  uptimeMs: clock.now() - startedAt,
@@ -629,16 +683,24 @@ const start = (module, options = {}) => {
629
683
  tracker.advanceTo("stopping");
630
684
  disposeAll();
631
685
  };
686
+ const leaveFailed = (failure) => {
687
+ if (tracker.current() !== "stopping") emit({
688
+ type: "startFailed",
689
+ cause: failure.tag === "Err" ? failure.error : failure.cause
690
+ });
691
+ runtimePublished.resolve(void 0);
692
+ tracker.advanceTo("stopping");
693
+ };
632
694
  const stopping = Promise.withResolvers();
633
- const stopAbandoned = () => (0, unthrown.fromSafePromise)(stopping.promise).flatMap(({ reason, drain }) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : (0, unthrown.OkAsync)(abandonStop(reason, drain))));
634
- const abandonStop = (reason, drain) => {
695
+ const stopAbandoned = () => (0, unthrown.fromSafePromise)(stopping.promise).flatMap((abandoned) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : abandonStop(abandoned)));
696
+ const abandonStop = (abandoned) => {
635
697
  emit({
636
698
  type: "stoppedWaiting",
637
699
  phase: "stop",
638
700
  afterMs: stopSettled.signal.aborted ? void 0 : stopTimeoutMs
639
701
  });
640
702
  disposeAll();
641
- return reportOf(reason, drain, "stop");
703
+ return abandoned().map(({ reason, drain }) => reportOf(reason, drain, "stop"));
642
704
  };
643
705
  const abandoningBuild = Promise.withResolvers();
644
706
  const buildAbandoned = () => (0, unthrown.fromSafePromise)(abandoningBuild.promise).map((reason) => {
@@ -653,7 +715,7 @@ const start = (module, options = {}) => {
653
715
  });
654
716
  const abandonBuild = (reason) => {
655
717
  const phase = tracker.current();
656
- if (phase === "building" || phase === "starting") abandoningBuild.resolve(reason);
718
+ if (phase === "building" || phase === "starting" || reason === "uncaught" && startupFailing) abandoningBuild.resolve(reason);
657
719
  };
658
720
  emit({ type: "building" });
659
721
  const probesOptions = kernelConfig.map(({ probePort, probeHost }) => options.probes === false ? false : {
@@ -714,22 +776,23 @@ const start = (module, options = {}) => {
714
776
  if (reason !== "signal") registry.abortAll();
715
777
  return (reason === "signal" ? runDrain(serving) : (0, unthrown.OkAsync)(void 0)).flatMap((report) => {
716
778
  tracker.advanceTo("stopping");
717
- stopping.resolve({
779
+ stopping.resolve(() => (0, unthrown.OkAsync)({
718
780
  reason,
719
781
  drain: report
720
- });
721
- return serving.stop().map(() => {
722
- disposeAll();
723
- return reportOf(reason, report);
724
- });
782
+ }));
783
+ return serving.stop().map(() => ({
784
+ reason,
785
+ drain: report
786
+ }));
725
787
  });
726
788
  };
727
- const built = () => _btravstack_di.Module.scoped(root, (ctx) => {
789
+ const serve = (ctx) => {
728
790
  tracker.advanceTo("starting");
729
791
  const runtime = ctx.get(RuntimePort);
730
792
  runtimeName = runtime.name;
731
793
  healthChecks = ctx.get(HealthChecks);
732
794
  const runtimeCtx = ctx;
795
+ const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
733
796
  const run = (meta, work) => registry.run(meta, (signal) => {
734
797
  const settled = Promise.withResolvers();
735
798
  let closing;
@@ -738,12 +801,12 @@ const start = (module, options = {}) => {
738
801
  if (hasSettled) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
739
802
  if (closing !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
740
803
  const ready = Promise.withResolvers();
741
- closing = _btravstack_di.Module.forkScope(ctx, module, (forked) => {
804
+ closing = _btravstack_di.Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
742
805
  ready.resolve(forked);
743
806
  return (0, unthrown.fromSafePromise)(settled.promise);
744
807
  }, {
745
808
  seed,
746
- onTeardownError: (port, cause) => emit({
809
+ onTeardownError: (port, cause) => sink({
747
810
  type: "teardownError",
748
811
  port,
749
812
  cause
@@ -781,28 +844,35 @@ const start = (module, options = {}) => {
781
844
  servingInfo = serving.info;
782
845
  tracker.advanceTo("serving");
783
846
  runtimePublished.resolve(serving.info);
784
- serving.stopped?.().map(() => {
785
- requestShutdown("runtimeStopped");
786
- });
847
+ serving.stopped?.().tap(() => requestShutdown("runtimeStopped")).tapFailure(() => requestShutdown("runtimeStopped"));
787
848
  return (0, unthrown.fromSafePromise)(shutdown.promise).flatMap((reason) => finish(serving, reason));
788
849
  });
789
- }, { onTeardownError: (port, cause) => {
790
- teardownErrors.push({
791
- port,
792
- cause
793
- });
794
- emit({
795
- type: "teardownError",
796
- port,
797
- cause
798
- });
799
- } }).tapFailure((failure) => {
800
- if (tracker.current() !== "stopping") emit({
801
- type: "startFailed",
802
- cause: failure.tag === "Err" ? failure.error : failure.cause
803
- });
804
- leaveStartup();
805
- }).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
850
+ };
851
+ const built = () => _btravstack_di.Module.scoped(root, (ctx) => (0, unthrown.OkAsync)(ctx).flatMap(serve).tapFailure((failure) => {
852
+ if (tracker.current() === "starting") startupFailing = true;
853
+ leaveFailed(failure);
854
+ stopping.resolve(() => failure.toAsync());
855
+ }), {
856
+ onConstructionFailure: (failure) => {
857
+ startupFailing = true;
858
+ leaveFailed(failure);
859
+ stopping.resolve(() => failure.toAsync());
860
+ },
861
+ onTeardownError: (port, cause) => {
862
+ teardownErrors.push({
863
+ port,
864
+ cause
865
+ });
866
+ emit({
867
+ type: "teardownError",
868
+ port,
869
+ cause
870
+ });
871
+ }
872
+ }).tapFailure((failure) => {
873
+ leaveFailed(failure);
874
+ disposeAll();
875
+ }).map(({ reason, drain }) => reportOf(reason, drain)).tap(disposeAll).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
806
876
  return {
807
877
  exited: probesStarted.flatMap(() => (0, unthrown.fromSafePromise)(Promise.race([
808
878
  built(),
@@ -979,6 +1049,7 @@ exports.RuntimePort = RuntimePort;
979
1049
  exports.RuntimeStartFailed = RuntimeStartFailed;
980
1050
  exports.SPAN_STATUS = SPAN_STATUS;
981
1051
  exports.Tracer = Tracer;
1052
+ exports.UnitOverrides = UnitOverrides;
982
1053
  exports.composeByPrefix = composeByPrefix;
983
1054
  exports.currentUnit = currentUnit;
984
1055
  exports.dispatchUnit = dispatchUnit;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AsyncResult, FailureView, Result } from "unthrown";
2
- import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
2
+ import { AnyPort, AnyProvider, Context, DependencyGate, Module, PortClass, PortInstance, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
5
5
  /**
@@ -395,6 +395,64 @@ type UnitRegistry = {
395
395
  readonly awaitIdle: () => AsyncResult<void, never>;
396
396
  };
397
397
  //#endregion
398
+ //#region src/unit-record.d.ts
399
+ /**
400
+ * A module a runtime's `unit` option may bind, as the upper bound a starter
401
+ * constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
402
+ * contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
403
+ * module's own (necessarily narrower) export type assignable to this bound:
404
+ * `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
405
+ * `(x: unknown) => void`.
406
+ */
407
+ type AnyUnitModule = Module<never, never, unknown>;
408
+ /**
409
+ * The needs a bound unit module still owes, or `never` when none is bound.
410
+ * `Scope` is excluded, since nothing can ever provide it — the same exemption
411
+ * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
412
+ * fork seed discharges.
413
+ */
414
+ type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
415
+ /** What a bound unit module exports — the port instances a piece of that kind may read. */
416
+ type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
417
+ /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
418
+ type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
419
+ /**
420
+ * A piece injecting a port the bound unit module does not export. `unknown`
421
+ * when every declared port is covered — including the empty case, so a worker
422
+ * whose pieces declare no `unit:` is gated on nothing.
423
+ */
424
+ type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
425
+ readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
426
+ };
427
+ /**
428
+ * One `(helpers, input)` implementation with its piece's declared record put on
429
+ * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
430
+ * Applied once per piece as di constructs it, so a unit costs one record and
431
+ * one context object. Each worker maps it over its own entry shape.
432
+ */
433
+ export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
434
+ readonly context?: Readonly<Record<string | symbol, unknown>>;
435
+ }, input: never) => unknown;
436
+ /**
437
+ * A worker middleware's body: observe `operation`, open one kernel unit, fork
438
+ * `unit` — when one is bound — seeded with `seed`, and call `next` with the
439
+ * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
440
+ * `next()` runs unchanged.
441
+ *
442
+ * Observed from whichever channel the unit settles on, a defect included: a
443
+ * defect is a failed unit too, and an errors count that omitted it would be the
444
+ * reassuring half.
445
+ */
446
+ export declare const dispatchUnit: <T, E>(dispatch: {
447
+ readonly host: RuntimeHost<never>;
448
+ readonly observers: readonly ((operation: Operation) => Settle)[];
449
+ readonly operation: Operation;
450
+ readonly meta: UnitMeta;
451
+ readonly unit: AnyUnitModule | undefined;
452
+ readonly seed: readonly [AnyPort, unknown];
453
+ readonly next: (overrides?: never) => AsyncResult<T, E>;
454
+ }) => AsyncResult<T, E>;
455
+ //#endregion
398
456
  //#region src/runtime.d.ts
399
457
  declare const RuntimeStartFailed_base: import("unthrown").TaggedErrorConstructor<"RuntimeStartFailed">;
400
458
  export declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
@@ -504,6 +562,14 @@ type Runtime<Resolves extends AnyPort = never, Info = never> = {
504
562
  readonly name: string;
505
563
  readonly resolves: readonly Resolves[];
506
564
  readonly start: (host: RuntimeHost<Resolves>) => AsyncResult<Serving<Info>, RuntimeStartFailed>;
565
+ /**
566
+ * The unit modules this runtime forks, by kind — `{ message: … }`, or one
567
+ * per HTTP scheme. Optional, and read for one thing only: a test's
568
+ * `UnitOverrides` name a kind, `fork` is handed a module, and this is
569
+ * what maps one onto the other. A runtime that omits it refuses every unit
570
+ * override at boot rather than ignoring it.
571
+ */
572
+ readonly units?: Readonly<Record<string, AnyUnitModule | undefined>>;
507
573
  };
508
574
  /**
509
575
  * The port the kernel resolves its runtime from. A runtime is a **service the
@@ -847,63 +913,20 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
847
913
  */
848
914
  export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
849
915
  //#endregion
850
- //#region src/unit-record.d.ts
916
+ //#region src/unit-overrides.d.ts
917
+ declare const UnitOverrides_base: import("@btravstack/di").ManyPortClass<"UnitOverrides">;
851
918
  /**
852
- * A module a runtime's `unit` option may bind, as the upper bound a starter
853
- * constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
854
- * contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
855
- * module's own (necessarily narrower) export type assignable to this bound:
856
- * `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
857
- * `(x: unknown) => void`.
858
- */
859
- type AnyUnitModule = Module<never, never, unknown>;
860
- /**
861
- * The needs a bound unit module still owes, or `never` when none is bound.
862
- * `Scope` is excluded, since nothing can ever provide it — the same exemption
863
- * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
864
- * fork seed discharges.
865
- */
866
- type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
867
- /** What a bound unit module exports — the port instances a piece of that kind may read. */
868
- type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
869
- /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
870
- type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
871
- /**
872
- * A piece injecting a port the bound unit module does not export. `unknown`
873
- * when every declared port is covered — including the empty case, so a worker
874
- * whose pieces declare no `unit:` is gated on nothing.
875
- */
876
- type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
877
- readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
878
- };
879
- /**
880
- * One `(helpers, input)` implementation with its piece's declared record put on
881
- * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
882
- * Applied once per piece as di constructs it, so a unit costs one record and
883
- * one context object. Each worker maps it over its own entry shape.
884
- */
885
- export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
886
- readonly context?: Readonly<Record<string | symbol, unknown>>;
887
- }, input: never) => unknown;
888
- /**
889
- * A worker middleware's body: observe `operation`, open one kernel unit, fork
890
- * `unit` — when one is bound — seeded with `seed`, and call `next` with the
891
- * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
892
- * `next()` runs unchanged.
919
+ * Providers substituted inside a runtime's unit modules, keyed by the kind
920
+ * the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
921
+ * `overridden(root, providers, { unit })` is the one contributor, and a
922
+ * production root never names it.
893
923
  *
894
- * Observed from whichever channel the unit settles on, a defect included: a
895
- * defect is a failed unit too, and an errors count that omitted it would be the
896
- * reassuring half.
924
+ * Each provider must already carry di's override brand. The kernel applies
925
+ * them at boot, before the runtime starts: every kind named must be one the
926
+ * runtime binds, and every port must be one that kind's module provides, or
927
+ * the boot is a defect — the same drift gate a root-level override has.
897
928
  */
898
- export declare const dispatchUnit: <T, E>(dispatch: {
899
- readonly host: RuntimeHost<never>;
900
- readonly observers: readonly ((operation: Operation) => Settle)[];
901
- readonly operation: Operation;
902
- readonly meta: UnitMeta;
903
- readonly unit: AnyUnitModule | undefined;
904
- readonly seed: readonly [AnyPort, unknown];
905
- readonly next: (overrides?: never) => AsyncResult<T, E>;
906
- }) => AsyncResult<T, E>;
929
+ export declare class UnitOverrides extends UnitOverrides_base<Readonly<Record<string, readonly AnyProvider[]>>> {}
907
930
  //#endregion
908
931
  //#region src/pieces.d.ts
909
932
  /**
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
1
+ import { AnyPort, AnyProvider, Context, DependencyGate, Module, PortClass, PortInstance, Provider, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
2
2
  import { AsyncResult, FailureView, Result } from "unthrown";
3
3
  import { Env, Environment } from "@btravstack/config";
4
4
  //#region src/instrumentation.d.ts
@@ -395,6 +395,64 @@ type UnitRegistry = {
395
395
  readonly awaitIdle: () => AsyncResult<void, never>;
396
396
  };
397
397
  //#endregion
398
+ //#region src/unit-record.d.ts
399
+ /**
400
+ * A module a runtime's `unit` option may bind, as the upper bound a starter
401
+ * constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
402
+ * contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
403
+ * module's own (necessarily narrower) export type assignable to this bound:
404
+ * `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
405
+ * `(x: unknown) => void`.
406
+ */
407
+ type AnyUnitModule = Module<never, never, unknown>;
408
+ /**
409
+ * The needs a bound unit module still owes, or `never` when none is bound.
410
+ * `Scope` is excluded, since nothing can ever provide it — the same exemption
411
+ * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
412
+ * fork seed discharges.
413
+ */
414
+ type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
415
+ /** What a bound unit module exports — the port instances a piece of that kind may read. */
416
+ type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
417
+ /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
418
+ type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
419
+ /**
420
+ * A piece injecting a port the bound unit module does not export. `unknown`
421
+ * when every declared port is covered — including the empty case, so a worker
422
+ * whose pieces declare no `unit:` is gated on nothing.
423
+ */
424
+ type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
425
+ readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
426
+ };
427
+ /**
428
+ * One `(helpers, input)` implementation with its piece's declared record put on
429
+ * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
430
+ * Applied once per piece as di constructs it, so a unit costs one record and
431
+ * one context object. Each worker maps it over its own entry shape.
432
+ */
433
+ export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
434
+ readonly context?: Readonly<Record<string | symbol, unknown>>;
435
+ }, input: never) => unknown;
436
+ /**
437
+ * A worker middleware's body: observe `operation`, open one kernel unit, fork
438
+ * `unit` — when one is bound — seeded with `seed`, and call `next` with the
439
+ * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
440
+ * `next()` runs unchanged.
441
+ *
442
+ * Observed from whichever channel the unit settles on, a defect included: a
443
+ * defect is a failed unit too, and an errors count that omitted it would be the
444
+ * reassuring half.
445
+ */
446
+ export declare const dispatchUnit: <T, E>(dispatch: {
447
+ readonly host: RuntimeHost<never>;
448
+ readonly observers: readonly ((operation: Operation) => Settle)[];
449
+ readonly operation: Operation;
450
+ readonly meta: UnitMeta;
451
+ readonly unit: AnyUnitModule | undefined;
452
+ readonly seed: readonly [AnyPort, unknown];
453
+ readonly next: (overrides?: never) => AsyncResult<T, E>;
454
+ }) => AsyncResult<T, E>;
455
+ //#endregion
398
456
  //#region src/runtime.d.ts
399
457
  declare const RuntimeStartFailed_base: import("unthrown").TaggedErrorConstructor<"RuntimeStartFailed">;
400
458
  export declare class RuntimeStartFailed extends RuntimeStartFailed_base<{
@@ -504,6 +562,14 @@ type Runtime<Resolves extends AnyPort = never, Info = never> = {
504
562
  readonly name: string;
505
563
  readonly resolves: readonly Resolves[];
506
564
  readonly start: (host: RuntimeHost<Resolves>) => AsyncResult<Serving<Info>, RuntimeStartFailed>;
565
+ /**
566
+ * The unit modules this runtime forks, by kind — `{ message: … }`, or one
567
+ * per HTTP scheme. Optional, and read for one thing only: a test's
568
+ * `UnitOverrides` name a kind, `fork` is handed a module, and this is
569
+ * what maps one onto the other. A runtime that omits it refuses every unit
570
+ * override at boot rather than ignoring it.
571
+ */
572
+ readonly units?: Readonly<Record<string, AnyUnitModule | undefined>>;
507
573
  };
508
574
  /**
509
575
  * The port the kernel resolves its runtime from. A runtime is a **service the
@@ -847,63 +913,20 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
847
913
  */
848
914
  export declare const runMain: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>, options?: StartOptions, exit?: (code: number) => void) => Promise<void>;
849
915
  //#endregion
850
- //#region src/unit-record.d.ts
916
+ //#region src/unit-overrides.d.ts
917
+ declare const UnitOverrides_base: import("@btravstack/di").ManyPortClass<"UnitOverrides">;
851
918
  /**
852
- * A module a runtime's `unit` option may bind, as the upper bound a starter
853
- * constrains its own `Unit` type parameter to. `Module`'s `_exports` channel is
854
- * contravariant, so `Exports = never` — never `unknown` — is what makes a REAL
855
- * module's own (necessarily narrower) export type assignable to this bound:
856
- * `(x: Concrete) => void` is assignable to `(x: never) => void`, not to
857
- * `(x: unknown) => void`.
858
- */
859
- type AnyUnitModule = Module<never, never, unknown>;
860
- /**
861
- * The needs a bound unit module still owes, or `never` when none is bound.
862
- * `Scope` is excluded, since nothing can ever provide it — the same exemption
863
- * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
864
- * fork seed discharges.
865
- */
866
- type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
867
- /** What a bound unit module exports — the port instances a piece of that kind may read. */
868
- type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
869
- /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
870
- type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
871
- /**
872
- * A piece injecting a port the bound unit module does not export. `unknown`
873
- * when every declared port is covered — including the empty case, so a worker
874
- * whose pieces declare no `unit:` is gated on nothing.
875
- */
876
- type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
877
- readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
878
- };
879
- /**
880
- * One `(helpers, input)` implementation with its piece's declared record put on
881
- * `helpers.context.unit`, resolved out of the fork {@link dispatchUnit} opened.
882
- * Applied once per piece as di constructs it, so a unit costs one record and
883
- * one context object. Each worker maps it over its own entry shape.
884
- */
885
- export declare const withUnitRecord: (record: Readonly<Record<string, AnyPort>>, implementation: (helpers: never, input: never) => unknown) => (helpers: {
886
- readonly context?: Readonly<Record<string | symbol, unknown>>;
887
- }, input: never) => unknown;
888
- /**
889
- * A worker middleware's body: observe `operation`, open one kernel unit, fork
890
- * `unit` — when one is bound — seeded with `seed`, and call `next` with the
891
- * forked context where {@link withUnitRecord} finds it. With no `unit` bound,
892
- * `next()` runs unchanged.
919
+ * Providers substituted inside a runtime's unit modules, keyed by the kind
920
+ * the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
921
+ * `overridden(root, providers, { unit })` is the one contributor, and a
922
+ * production root never names it.
893
923
  *
894
- * Observed from whichever channel the unit settles on, a defect included: a
895
- * defect is a failed unit too, and an errors count that omitted it would be the
896
- * reassuring half.
924
+ * Each provider must already carry di's override brand. The kernel applies
925
+ * them at boot, before the runtime starts: every kind named must be one the
926
+ * runtime binds, and every port must be one that kind's module provides, or
927
+ * the boot is a defect — the same drift gate a root-level override has.
897
928
  */
898
- export declare const dispatchUnit: <T, E>(dispatch: {
899
- readonly host: RuntimeHost<never>;
900
- readonly observers: readonly ((operation: Operation) => Settle)[];
901
- readonly operation: Operation;
902
- readonly meta: UnitMeta;
903
- readonly unit: AnyUnitModule | undefined;
904
- readonly seed: readonly [AnyPort, unknown];
905
- readonly next: (overrides?: never) => AsyncResult<T, E>;
906
- }) => AsyncResult<T, E>;
929
+ export declare class UnitOverrides extends UnitOverrides_base<Readonly<Record<string, readonly AnyProvider[]>>> {}
907
930
  //#endregion
908
931
  //#region src/pieces.d.ts
909
932
  /**
package/dist/index.mjs CHANGED
@@ -428,6 +428,51 @@ const installUncaughtHandlers = (onUncaught) => {
428
428
  };
429
429
  };
430
430
  //#endregion
431
+ //#region src/unit-overrides.ts
432
+ /**
433
+ * Providers substituted inside a runtime's unit modules, keyed by the kind
434
+ * the runtime declares on `Runtime.units` — test-facing: `@btravstack/testing`'s
435
+ * `overridden(root, providers, { unit })` is the one contributor, and a
436
+ * production root never names it.
437
+ *
438
+ * Each provider must already carry di's override brand. The kernel applies
439
+ * them at boot, before the runtime starts: every kind named must be one the
440
+ * runtime binds, and every port must be one that kind's module provides, or
441
+ * the boot is a defect — the same drift gate a root-level override has.
442
+ */
443
+ var UnitOverrides = class extends Port.many("UnitOverrides") {};
444
+ /** Whether `module`'s tree carries a provider for `portId`. */
445
+ const provides = (module, portId) => module.provides.some((provider) => provider.port.portId === portId) || module.imports.some((imported) => provides(imported, portId));
446
+ /**
447
+ * Each overridden kind's module, mapped to the module the kernel forks in its
448
+ * place. Keyed by module IDENTITY, because `UnitHost.fork` is handed the
449
+ * module and never the kind — which is why a module bound under two kinds is
450
+ * refused rather than overridden under both.
451
+ */
452
+ const unitSubstitutes = (runtime, units, overrides) => {
453
+ const byKind = /* @__PURE__ */ new Map();
454
+ for (const member of overrides) for (const [kind, providers] of Object.entries(member)) byKind.set(kind, [...byKind.get(kind) ?? [], ...providers]);
455
+ const substitutes = /* @__PURE__ */ new Map();
456
+ for (const [kind, providers] of byKind) {
457
+ const module = units[kind];
458
+ if (module === void 0) throw new Error(`[core] unit override for kind "${kind}", which runtime "${runtime}" binds no module for`);
459
+ const shared = Object.keys(units).find((other) => other !== kind && units[other] === module);
460
+ if (shared !== void 0) throw new Error(`[core] unit override for kind "${kind}", whose module kind "${shared}" binds too — an override cannot reach one without the other`);
461
+ const seen = /* @__PURE__ */ new Set();
462
+ for (const provider of providers) {
463
+ if (seen.has(provider.port.portId)) throw new Error(`[core] two unit overrides registered for port ${JSON.stringify(provider.port.portId)} in kind "${kind}"`);
464
+ seen.add(provider.port.portId);
465
+ if (!provides(module, provider.port.portId)) throw new Error(`[core] unit override for port ${JSON.stringify(provider.port.portId)} in kind "${kind}" with nothing to override — its module no longer provides it`);
466
+ }
467
+ substitutes.set(module, Module("UnitOverridden")({
468
+ imports: [module],
469
+ provides: providers,
470
+ exports: [module]
471
+ }));
472
+ }
473
+ return substitutes;
474
+ };
475
+ //#endregion
431
476
  //#region src/units.ts
432
477
  const storage = new AsyncLocalStorage();
433
478
  const runWithUnit = (record, fn) => storage.run(record, fn);
@@ -497,7 +542,7 @@ const createUnitRegistry = () => {
497
542
  * and whose side effects then have to be guarded one by one.
498
543
  */
499
544
  const withdrawn = () => fromSafePromise(new Promise(() => {}));
500
- const providesEnv = (module) => module.provides.some((provider) => provider.port.portId === Env.portId) || module.imports.some(providesEnv);
545
+ const providesEnv = (module) => provides(module, Env.portId);
501
546
  /**
502
547
  * What the kernel reads from the environment for itself. Each field is pinned
503
548
  * by the matching `StartOptions` field — explicit beats environment beats
@@ -544,7 +589,13 @@ const readKernelConfig = (options, env) => {
544
589
  const start = (module, options = {}) => {
545
590
  const clock = options.clock ?? systemClock;
546
591
  const env = options.env ?? process.env;
547
- const emit = safeSink(options.onEvent ?? stderrSink);
592
+ const sink = safeSink(options.onEvent ?? stderrSink);
593
+ let exitedEmitted = false;
594
+ const emit = (event) => {
595
+ if (exitedEmitted) return;
596
+ sink(event);
597
+ if (event.type === "exited") exitedEmitted = true;
598
+ };
548
599
  let runtimeName = "";
549
600
  let servingInfo = void 0;
550
601
  let probeBoundPort = void 0;
@@ -596,6 +647,7 @@ const start = (module, options = {}) => {
596
647
  }
597
648
  });
598
649
  const disposeUncaught = options.signals === false ? () => {} : installUncaughtHandlers((cause) => {
650
+ crashed = true;
599
651
  emit({
600
652
  type: "uncaught",
601
653
  cause
@@ -606,6 +658,8 @@ const start = (module, options = {}) => {
606
658
  abandonBuild("uncaught");
607
659
  });
608
660
  let disposeProbes = () => {};
661
+ let crashed = false;
662
+ let startupFailing = false;
609
663
  const disposeAll = () => {
610
664
  disposeSignals();
611
665
  disposeUncaught();
@@ -613,7 +667,7 @@ const start = (module, options = {}) => {
613
667
  disposeProbes();
614
668
  };
615
669
  const reportOf = (reason, drain, abandonedAt) => ({
616
- reason,
670
+ reason: crashed ? "uncaught" : reason,
617
671
  drain,
618
672
  teardownErrors,
619
673
  uptimeMs: clock.now() - startedAt,
@@ -628,16 +682,24 @@ const start = (module, options = {}) => {
628
682
  tracker.advanceTo("stopping");
629
683
  disposeAll();
630
684
  };
685
+ const leaveFailed = (failure) => {
686
+ if (tracker.current() !== "stopping") emit({
687
+ type: "startFailed",
688
+ cause: failure.tag === "Err" ? failure.error : failure.cause
689
+ });
690
+ runtimePublished.resolve(void 0);
691
+ tracker.advanceTo("stopping");
692
+ };
631
693
  const stopping = Promise.withResolvers();
632
- const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap(({ reason, drain }) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : OkAsync(abandonStop(reason, drain))));
633
- const abandonStop = (reason, drain) => {
694
+ const stopAbandoned = () => fromSafePromise(stopping.promise).flatMap((abandoned) => clock.sleep(stopTimeoutMs, stopSettled.signal).flatMap(() => lifecycleSettled ? withdrawn() : abandonStop(abandoned)));
695
+ const abandonStop = (abandoned) => {
634
696
  emit({
635
697
  type: "stoppedWaiting",
636
698
  phase: "stop",
637
699
  afterMs: stopSettled.signal.aborted ? void 0 : stopTimeoutMs
638
700
  });
639
701
  disposeAll();
640
- return reportOf(reason, drain, "stop");
702
+ return abandoned().map(({ reason, drain }) => reportOf(reason, drain, "stop"));
641
703
  };
642
704
  const abandoningBuild = Promise.withResolvers();
643
705
  const buildAbandoned = () => fromSafePromise(abandoningBuild.promise).map((reason) => {
@@ -652,7 +714,7 @@ const start = (module, options = {}) => {
652
714
  });
653
715
  const abandonBuild = (reason) => {
654
716
  const phase = tracker.current();
655
- if (phase === "building" || phase === "starting") abandoningBuild.resolve(reason);
717
+ if (phase === "building" || phase === "starting" || reason === "uncaught" && startupFailing) abandoningBuild.resolve(reason);
656
718
  };
657
719
  emit({ type: "building" });
658
720
  const probesOptions = kernelConfig.map(({ probePort, probeHost }) => options.probes === false ? false : {
@@ -713,22 +775,23 @@ const start = (module, options = {}) => {
713
775
  if (reason !== "signal") registry.abortAll();
714
776
  return (reason === "signal" ? runDrain(serving) : OkAsync(void 0)).flatMap((report) => {
715
777
  tracker.advanceTo("stopping");
716
- stopping.resolve({
778
+ stopping.resolve(() => OkAsync({
717
779
  reason,
718
780
  drain: report
719
- });
720
- return serving.stop().map(() => {
721
- disposeAll();
722
- return reportOf(reason, report);
723
- });
781
+ }));
782
+ return serving.stop().map(() => ({
783
+ reason,
784
+ drain: report
785
+ }));
724
786
  });
725
787
  };
726
- const built = () => Module.scoped(root, (ctx) => {
788
+ const serve = (ctx) => {
727
789
  tracker.advanceTo("starting");
728
790
  const runtime = ctx.get(RuntimePort);
729
791
  runtimeName = runtime.name;
730
792
  healthChecks = ctx.get(HealthChecks);
731
793
  const runtimeCtx = ctx;
794
+ const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
732
795
  const run = (meta, work) => registry.run(meta, (signal) => {
733
796
  const settled = Promise.withResolvers();
734
797
  let closing;
@@ -737,12 +800,12 @@ const start = (module, options = {}) => {
737
800
  if (hasSettled) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
738
801
  if (closing !== void 0) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
739
802
  const ready = Promise.withResolvers();
740
- closing = Module.forkScope(ctx, module, (forked) => {
803
+ closing = Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
741
804
  ready.resolve(forked);
742
805
  return fromSafePromise(settled.promise);
743
806
  }, {
744
807
  seed,
745
- onTeardownError: (port, cause) => emit({
808
+ onTeardownError: (port, cause) => sink({
746
809
  type: "teardownError",
747
810
  port,
748
811
  cause
@@ -780,28 +843,35 @@ const start = (module, options = {}) => {
780
843
  servingInfo = serving.info;
781
844
  tracker.advanceTo("serving");
782
845
  runtimePublished.resolve(serving.info);
783
- serving.stopped?.().map(() => {
784
- requestShutdown("runtimeStopped");
785
- });
846
+ serving.stopped?.().tap(() => requestShutdown("runtimeStopped")).tapFailure(() => requestShutdown("runtimeStopped"));
786
847
  return fromSafePromise(shutdown.promise).flatMap((reason) => finish(serving, reason));
787
848
  });
788
- }, { onTeardownError: (port, cause) => {
789
- teardownErrors.push({
790
- port,
791
- cause
792
- });
793
- emit({
794
- type: "teardownError",
795
- port,
796
- cause
797
- });
798
- } }).tapFailure((failure) => {
799
- if (tracker.current() !== "stopping") emit({
800
- type: "startFailed",
801
- cause: failure.tag === "Err" ? failure.error : failure.cause
802
- });
803
- leaveStartup();
804
- }).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
849
+ };
850
+ const built = () => Module.scoped(root, (ctx) => OkAsync(ctx).flatMap(serve).tapFailure((failure) => {
851
+ if (tracker.current() === "starting") startupFailing = true;
852
+ leaveFailed(failure);
853
+ stopping.resolve(() => failure.toAsync());
854
+ }), {
855
+ onConstructionFailure: (failure) => {
856
+ startupFailing = true;
857
+ leaveFailed(failure);
858
+ stopping.resolve(() => failure.toAsync());
859
+ },
860
+ onTeardownError: (port, cause) => {
861
+ teardownErrors.push({
862
+ port,
863
+ cause
864
+ });
865
+ emit({
866
+ type: "teardownError",
867
+ port,
868
+ cause
869
+ });
870
+ }
871
+ }).tapFailure((failure) => {
872
+ leaveFailed(failure);
873
+ disposeAll();
874
+ }).map(({ reason, drain }) => reportOf(reason, drain)).tap(disposeAll).tap(onLifecycleSettled).tapFailure(onLifecycleSettled);
805
875
  return {
806
876
  exited: probesStarted.flatMap(() => fromSafePromise(Promise.race([
807
877
  built(),
@@ -967,4 +1037,4 @@ const noObserverMember = Provider.member(Observers)({
967
1037
  value: noObserver
968
1038
  });
969
1039
  //#endregion
970
- export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, composeByPrefix, currentUnit, dispatchUnit, mintPiece, noObserver, noObserverMember, observe, observed, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock, traceIdOfTraceparent, unitOutcome, withUnitRecord };
1040
+ export { HealthCheckFailed, HealthChecks, Instrumentations, LEVELS, Logger, Meter, Observers, RuntimePort, RuntimeStartFailed, SPAN_STATUS, Tracer, UnitOverrides, composeByPrefix, currentUnit, dispatchUnit, mintPiece, noObserver, noObserverMember, observe, observed, releasedBy, runHealthChecks, runMain, start, stderrSink, systemClock, traceIdOfTraceparent, unitOutcome, withUnitRecord };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/core",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "The application kernel: boot a di module into a running process, and stop it without losing work",
5
5
  "keywords": [
6
6
  "application",
@@ -45,9 +45,11 @@
45
45
  "./package.json": "./package.json"
46
46
  },
47
47
  "devDependencies": {
48
- "@btravstack/config": "0.18.0",
49
- "@btravstack/di": "0.18.0",
48
+ "@btravstack/config": "0.20.0",
49
+ "@btravstack/di": "0.20.0",
50
50
  "@btravstack/tsconfig": "0.4.0",
51
+ "@stryker-mutator/core": "10.0.0",
52
+ "@stryker-mutator/vitest-runner": "10.0.0",
51
53
  "@types/node": "26.6.4",
52
54
  "@unthrown/vitest": "5.12.0",
53
55
  "@vitest/coverage-v8": "5.0.3",
@@ -58,8 +60,8 @@
58
60
  "vitest": "5.0.3"
59
61
  },
60
62
  "peerDependencies": {
61
- "@btravstack/config": "^0.18.0",
62
- "@btravstack/di": "^0.18.0",
63
+ "@btravstack/config": "^0.20.0",
64
+ "@btravstack/di": "^0.20.0",
63
65
  "unthrown": "^5.0.0"
64
66
  },
65
67
  "engines": {
@@ -69,6 +71,7 @@
69
71
  "build": "tsdown src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.build.json",
70
72
  "dev": "tsdown src/index.ts --format cjs,esm --dts --watch --tsconfig tsconfig.build.json",
71
73
  "generate": "node ../../docs/scripts/extract-doc-samples.ts --group core --out src/generated/doc-samples",
74
+ "mutate": "stryker run",
72
75
  "test": "vitest run --coverage",
73
76
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json && node ./node_modules/typescript-consumer/bin/tsc --noEmit -p tsconfig.test-d.json"
74
77
  }