@nwire/endpoint 0.12.1 → 0.13.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/adapter.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * `endpoint("api").use(adopter).mount(source).run()` is the nwire path.
11
11
  */
12
+ import type { IncomingMessage, ServerResponse } from "node:http";
12
13
  import type { Wire } from "@nwire/wires";
13
14
  import type { Container } from "@nwire/container";
14
15
  import type { Logger } from "@nwire/logger";
@@ -82,6 +83,29 @@ export interface Adapter {
82
83
  * adopters or before boot.
83
84
  */
84
85
  port?(): number | undefined;
86
+ /**
87
+ * Optional — a host-mountable `(req, res)` handler, available after
88
+ * `boot()`. HTTP-class adapters return their composed handler (koa's
89
+ * `.callback()`, the Express app, …) so a single in-process host (the
90
+ * one-Vite dev host, supertest) can mount the wire as middleware without
91
+ * binding a port. Non-HTTP adapters (queue, cron, nats, mcp) return
92
+ * `undefined` — they just run in-process. Transport-agnostic: the
93
+ * endpoint composes these without knowing which transport produced them.
94
+ */
95
+ handler?(): ((req: IncomingMessage, res: ServerResponse) => void) | undefined;
96
+ /**
97
+ * Optional — does this adapter own the given URL path? Available after
98
+ * `boot()`. The one-Vite dev host calls this to route deterministically:
99
+ * paths the wire owns (its route prefix + well-known surfaces like
100
+ * `/openapi.json`, `/docs`, `/_nwire/*`) go to the wire's handler; every
101
+ * other path (`/`, Studio assets, Studio deep links) goes to Studio. This
102
+ * removes the response-sniffing heuristic for the common case — a wire with
103
+ * adopter-wide auth no longer 401s `/` and shadows Studio.
104
+ *
105
+ * Returns `undefined`/absent for adapters that don't compute ownership; the
106
+ * host then falls back to running the handler and detecting a pass-through.
107
+ */
108
+ owns?(pathname: string): boolean;
85
109
  }
86
110
  /** Type narrow — is this an Adapter? */
87
111
  export declare function isAdapter(x: unknown): x is Adapter;
@@ -19,6 +19,6 @@
19
19
  * (`http-terminator` + `lightship`). The high-level builder is what most
20
20
  * projects reach for; the low-level form is for adapters and tests.
21
21
  */
22
- export { endpoint, EndpointBuilder, isAppServable, isForeignServable, type AppServable, type EndpointConfig, type ProcessConfig, type ForeignServable, type RunningEndpoint, type Servable, } from "./endpoint.js";
22
+ export { endpoint, EndpointBuilder, isAppServable, isForeignServable, type AppServable, type EndpointConfig, type ProcessConfig, type ForeignServable, type RunningEndpoint, type Servable, type DevHostMount, type DevHostCollector, } from "./endpoint.js";
23
23
  export { attachLifecycle, defineCheck, type HealthCheck, type HealthConfig, type LifecycleManager, type ShutdownConfig, } from "./lifecycle.js";
24
24
  export { isAdapter, type Adapter, type AdapterBootContext } from "./adapter.js";
@@ -34,7 +34,7 @@
34
34
  * write the same K8s-readiness code on every project. It works alone —
35
35
  * install only this package and wrap any Node server.
36
36
  */
37
- import { type Server } from "node:http";
37
+ import { type Server, type IncomingMessage, type ServerResponse } from "node:http";
38
38
  import type { Container } from "@nwire/container";
39
39
  import type { Interface } from "@nwire/wires";
40
40
  import { type HealthConfig, type LifecycleManager, type ShutdownConfig } from "./lifecycle.js";
@@ -135,10 +135,38 @@ export type ProcessConfig = EndpointConfig;
135
135
  export interface RunningEndpoint {
136
136
  readonly name: string;
137
137
  readonly server?: Server;
138
- readonly lifecycle: LifecycleManager;
139
- /** Trigger shutdown manually. Idempotent. */
138
+ /**
139
+ * The lifecycle manager present for a normal `.run()`. Absent in dev-host
140
+ * mode, where the Vite host owns the process (no probes / signal handling).
141
+ */
142
+ readonly lifecycle?: LifecycleManager;
143
+ /** Trigger shutdown manually. Idempotent. Drains adapters, then apps. */
140
144
  shutdown(reason?: string): Promise<void>;
141
145
  }
146
+ /**
147
+ * What a dev host collects from each `.run()` it loads (one Vite host mode).
148
+ * The host registers a collector on `globalThis.__nwireDevHost`; the endpoint
149
+ * boots port-less and hands over the booted app(s) + each adapter's mounted
150
+ * `(req, res)` handler so the host can mount the wire as middleware and read
151
+ * the in-process runtime directly. See the dev-host branch in `run()`.
152
+ */
153
+ export interface DevHostMount {
154
+ readonly name: string;
155
+ readonly apps: readonly AppServable[];
156
+ readonly handlers: ReadonlyArray<(req: IncomingMessage, res: ServerResponse) => void>;
157
+ /**
158
+ * Does this endpoint's wire own the given URL path? OR of each HTTP adapter's
159
+ * `owns(pathname)`. Present only when at least one adapter computes ownership;
160
+ * the host routes owned paths to the wire and everything else to Studio. When
161
+ * absent, the host falls back to running handlers + detecting a pass-through.
162
+ */
163
+ readonly owns?: (pathname: string) => boolean;
164
+ /** Drain this endpoint's adapters (reverse) then its apps. Host calls on close/HMR. */
165
+ readonly shutdown: (reason?: string) => Promise<void>;
166
+ }
167
+ export interface DevHostCollector {
168
+ collect(mount: DevHostMount): void;
169
+ }
142
170
  /**
143
171
  * Build an endpoint. The returned builder is chainable: `.serve()` adds
144
172
  * apps + interfaces in declaration order; `.run()` boots and listens.
package/dist/endpoint.js CHANGED
@@ -34,7 +34,7 @@
34
34
  * write the same K8s-readiness code on every project. It works alone —
35
35
  * install only this package and wrap any Node server.
36
36
  */
37
- import { createServer as createHttpServer } from "node:http";
37
+ import { createServer as createHttpServer, } from "node:http";
38
38
  import { hook } from "@nwire/hooks";
39
39
  import { ConsoleLogger } from "@nwire/logger";
40
40
  import { attachLifecycle, } from "./lifecycle.js";
@@ -235,8 +235,15 @@ export class EndpointBuilder {
235
235
  phase: "serve",
236
236
  startedAt: new Date().toISOString(),
237
237
  });
238
+ // Dev-host mode (one Vite host): when a collector is registered on the
239
+ // global, the endpoint boots + mounts its adapters but binds NO port —
240
+ // httpKoa mounts its koa without listening (it only listens when a port
241
+ // is present), so the host can grab `adapter.koa().callback()` and mount
242
+ // the wire as middleware on its own single port. Foreign listeners and
243
+ // probes are skipped too. `main.ts` is unchanged: it still calls `.run()`.
244
+ const devHost = globalThis.__nwireDevHost;
238
245
  let server;
239
- if (this.config.port !== undefined && this.foreignServables.length) {
246
+ if (!devHost && this.config.port !== undefined && this.foreignServables.length) {
240
247
  server = await startListener({
241
248
  port: this.config.port,
242
249
  host: this.config.host,
@@ -286,12 +293,48 @@ export class EndpointBuilder {
286
293
  containerOf,
287
294
  logger: adapterLogger,
288
295
  addCheck: (c) => accumulatedChecks.push(c),
289
- port: this.config.port,
296
+ // Dev-host: no port → adapters mount without binding.
297
+ port: devHost ? undefined : this.config.port,
290
298
  host: this.config.host,
291
299
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
300
  installSinkStage: installSinkStage,
293
301
  });
294
302
  }
303
+ // Dev-host: hand the booted app + each adapter's mounted handler to the
304
+ // collector and return without binding a port or a probe server — the
305
+ // Vite host mounts the handlers + reads the in-process runtime directly.
306
+ if (devHost) {
307
+ // Transport-agnostic: ask each adapter for its mountable handler via the
308
+ // contract. HTTP adapters (koa, express, …) return one; non-HTTP adapters
309
+ // return undefined and keep running in-process. The endpoint never names
310
+ // a transport.
311
+ const handlers = [];
312
+ const ownsFns = [];
313
+ for (const adopter of this.adapters) {
314
+ const h = adopter.handler?.();
315
+ if (h)
316
+ handlers.push(h);
317
+ if (adopter.owns)
318
+ ownsFns.push((p) => adopter.owns(p));
319
+ }
320
+ // OR each adapter's ownership so the host can route deterministically:
321
+ // owned paths → wire, everything else → Studio. Undefined when no adapter
322
+ // computes ownership — the host then runs handlers + sniffs a 404.
323
+ const owns = ownsFns.length > 0 ? (pathname) => ownsFns.some((f) => f(pathname)) : undefined;
324
+ // Drain like prod: adapters in reverse boot order (so non-HTTP transports
325
+ // — queue/cron/nats — that started in-process get torn down), then stop
326
+ // the apps. The host calls this on close / HMR reload.
327
+ const reverseAdapters = [...this.adapters].reverse();
328
+ const shutdown = async (reason) => {
329
+ for (const adopter of reverseAdapters)
330
+ await adopter.shutdown(reason);
331
+ for (const app of this.apps)
332
+ await app.stop?.();
333
+ };
334
+ devHost.collect({ name: this.name, apps: this.apps, handlers, owns, shutdown });
335
+ // No lifecycle manager in dev-host mode — the Vite host owns the process.
336
+ return { name: this.name, shutdown };
337
+ }
295
338
  // 3. Wrap the server (if any) with graceful shutdown + probes.
296
339
  const lifecycle = await attachLifecycle({
297
340
  server: server ?? (await makeProbeOnlyServer()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwire/endpoint",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "Nwire — production process lifecycle. Wraps any Node server (Express, Fastify, Koa, Nest, Nwire interfaces) with K8s-grade graceful shutdown, http-terminator drain, and lightship readiness/liveness probes. Standalone — no framework dependency beyond @nwire/container.",
5
5
  "keywords": [
6
6
  "endpoint",
@@ -32,11 +32,11 @@
32
32
  "dependencies": {
33
33
  "http-terminator": "^3.2.0",
34
34
  "lightship": "^9.0.4",
35
- "@nwire/hooks": "0.12.1",
36
- "@nwire/logger": "0.12.1",
37
- "@nwire/runtime": "0.12.1",
38
- "@nwire/container": "0.12.1",
39
- "@nwire/wires": "0.12.1"
35
+ "@nwire/hooks": "0.13.0",
36
+ "@nwire/logger": "0.13.0",
37
+ "@nwire/runtime": "0.13.0",
38
+ "@nwire/wires": "0.13.0",
39
+ "@nwire/container": "0.13.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^22.19.9",