@nifrajs/web 2.1.0 → 2.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.
Files changed (194) hide show
  1. package/README.md +3 -3
  2. package/dist/build-vite.d.ts +68 -0
  3. package/dist/build-vite.d.ts.map +1 -0
  4. package/dist/build-vite.js +433 -0
  5. package/dist/build-vite.js.map +1 -0
  6. package/dist/build.d.ts +174 -65
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +376 -135
  9. package/dist/build.js.map +1 -1
  10. package/dist/bun-dev-entry.d.ts +83 -0
  11. package/dist/bun-dev-entry.d.ts.map +1 -0
  12. package/dist/bun-dev-entry.js +136 -0
  13. package/dist/bun-dev-entry.js.map +1 -0
  14. package/dist/chunk-names.js +1 -1
  15. package/dist/client.d.ts +3 -9
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +209 -27
  18. package/dist/client.js.map +1 -1
  19. package/dist/conformance.d.ts +2 -2
  20. package/dist/conformance.js +3 -3
  21. package/dist/deferred.d.ts +11 -11
  22. package/dist/deferred.d.ts.map +1 -1
  23. package/dist/deferred.js +16 -16
  24. package/dist/dev-error.d.ts +2 -2
  25. package/dist/dev-error.js +3 -3
  26. package/dist/dev-port.d.ts +36 -0
  27. package/dist/dev-port.d.ts.map +1 -0
  28. package/dist/dev-port.js +48 -0
  29. package/dist/dev-port.js.map +1 -0
  30. package/dist/dev.d.ts +62 -5
  31. package/dist/dev.d.ts.map +1 -1
  32. package/dist/dev.js +419 -60
  33. package/dist/dev.js.map +1 -1
  34. package/dist/draft.d.ts +44 -7
  35. package/dist/draft.d.ts.map +1 -1
  36. package/dist/draft.js +92 -9
  37. package/dist/draft.js.map +1 -1
  38. package/dist/fn-state.d.ts +56 -0
  39. package/dist/fn-state.d.ts.map +1 -0
  40. package/dist/fn-state.js +77 -0
  41. package/dist/fn-state.js.map +1 -0
  42. package/dist/fn.d.ts +111 -0
  43. package/dist/fn.d.ts.map +1 -0
  44. package/dist/fn.js +125 -0
  45. package/dist/fn.js.map +1 -0
  46. package/dist/fonts-google.d.ts +9 -9
  47. package/dist/fonts-google.js +10 -10
  48. package/dist/fonts.d.ts +7 -7
  49. package/dist/fonts.js +1 -1
  50. package/dist/forms.d.ts +8 -8
  51. package/dist/fs.d.ts +15 -2
  52. package/dist/fs.d.ts.map +1 -1
  53. package/dist/fs.js +8 -2
  54. package/dist/fs.js.map +1 -1
  55. package/dist/index.d.ts +266 -74
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +869 -179
  58. package/dist/index.js.map +1 -1
  59. package/dist/internal/head-attributes.d.ts +15 -0
  60. package/dist/internal/head-attributes.d.ts.map +1 -0
  61. package/dist/internal/head-attributes.js +99 -0
  62. package/dist/internal/head-attributes.js.map +1 -0
  63. package/dist/internal/script-types.d.ts +14 -0
  64. package/dist/internal/script-types.d.ts.map +1 -0
  65. package/dist/internal/script-types.js +17 -0
  66. package/dist/internal/script-types.js.map +1 -0
  67. package/dist/internal/server-boundary.d.ts +60 -0
  68. package/dist/internal/server-boundary.d.ts.map +1 -0
  69. package/dist/internal/server-boundary.js +174 -0
  70. package/dist/internal/server-boundary.js.map +1 -0
  71. package/dist/internal/timing-safe-equal.d.ts +16 -0
  72. package/dist/internal/timing-safe-equal.d.ts.map +1 -0
  73. package/dist/internal/timing-safe-equal.js +26 -0
  74. package/dist/internal/timing-safe-equal.js.map +1 -0
  75. package/dist/internal/vite-import.d.ts +63 -0
  76. package/dist/internal/vite-import.d.ts.map +1 -0
  77. package/dist/internal/vite-import.js +94 -0
  78. package/dist/internal/vite-import.js.map +1 -0
  79. package/dist/islands.d.ts +5 -5
  80. package/dist/islands.js +5 -5
  81. package/dist/isr.d.ts +15 -15
  82. package/dist/isr.d.ts.map +1 -1
  83. package/dist/isr.js +8 -21
  84. package/dist/isr.js.map +1 -1
  85. package/dist/llms-txt.d.ts.map +1 -1
  86. package/dist/llms-txt.js +12 -6
  87. package/dist/llms-txt.js.map +1 -1
  88. package/dist/manifest.d.ts +121 -40
  89. package/dist/manifest.d.ts.map +1 -1
  90. package/dist/manifest.js +136 -26
  91. package/dist/manifest.js.map +1 -1
  92. package/dist/module-graph.d.ts +91 -0
  93. package/dist/module-graph.d.ts.map +1 -0
  94. package/dist/module-graph.js +92 -0
  95. package/dist/module-graph.js.map +1 -0
  96. package/dist/navigation.d.ts +120 -5
  97. package/dist/navigation.d.ts.map +1 -1
  98. package/dist/navigation.js +37 -5
  99. package/dist/navigation.js.map +1 -1
  100. package/dist/plugins/css-modules.d.ts +3 -3
  101. package/dist/plugins/css-modules.d.ts.map +1 -1
  102. package/dist/plugins/css-modules.js +0 -0
  103. package/dist/plugins/css-modules.js.map +1 -1
  104. package/dist/plugins/kit.d.ts +8 -8
  105. package/dist/plugins/kit.js +9 -9
  106. package/dist/plugins/scss.js +3 -3
  107. package/dist/plugins/vite-leak-guard.d.ts +45 -0
  108. package/dist/plugins/vite-leak-guard.d.ts.map +1 -0
  109. package/dist/plugins/vite-leak-guard.js +67 -0
  110. package/dist/plugins/vite-leak-guard.js.map +1 -0
  111. package/dist/plugins/vite-server-fn.d.ts +23 -0
  112. package/dist/plugins/vite-server-fn.d.ts.map +1 -0
  113. package/dist/plugins/vite-server-fn.js +51 -0
  114. package/dist/plugins/vite-server-fn.js.map +1 -0
  115. package/dist/plugins/vite-server-only.d.ts +49 -0
  116. package/dist/plugins/vite-server-only.d.ts.map +1 -0
  117. package/dist/plugins/vite-server-only.js +57 -0
  118. package/dist/plugins/vite-server-only.js.map +1 -0
  119. package/dist/prerender.d.ts +5 -5
  120. package/dist/prerender.js +7 -7
  121. package/dist/public-dir.d.ts +36 -0
  122. package/dist/public-dir.d.ts.map +1 -0
  123. package/dist/public-dir.js +89 -0
  124. package/dist/public-dir.js.map +1 -0
  125. package/dist/query.d.ts +10 -10
  126. package/dist/query.js +15 -15
  127. package/dist/route-manifest.d.ts +91 -0
  128. package/dist/route-manifest.d.ts.map +1 -0
  129. package/dist/route-manifest.js +128 -0
  130. package/dist/route-manifest.js.map +1 -0
  131. package/dist/router.d.ts +94 -22
  132. package/dist/router.d.ts.map +1 -1
  133. package/dist/router.js +170 -58
  134. package/dist/router.js.map +1 -1
  135. package/dist/search.d.ts +94 -0
  136. package/dist/search.d.ts.map +1 -0
  137. package/dist/search.js +232 -0
  138. package/dist/search.js.map +1 -0
  139. package/dist/server-only.d.ts +6 -6
  140. package/dist/server-only.js +7 -7
  141. package/dist/service-worker.d.ts +56 -0
  142. package/dist/service-worker.d.ts.map +1 -0
  143. package/dist/service-worker.js +127 -0
  144. package/dist/service-worker.js.map +1 -0
  145. package/dist/vite.d.ts +25 -17
  146. package/dist/vite.d.ts.map +1 -1
  147. package/dist/vite.js +75 -33
  148. package/dist/vite.js.map +1 -1
  149. package/package.json +48 -4
  150. package/server-manifest.d.ts +31 -0
  151. package/src/build-vite.ts +553 -0
  152. package/src/build.ts +563 -172
  153. package/src/bun-dev-entry.ts +184 -0
  154. package/src/chunk-names.ts +1 -1
  155. package/src/client.ts +221 -30
  156. package/src/conformance.ts +3 -3
  157. package/src/deferred.ts +17 -17
  158. package/src/dev-error.ts +3 -3
  159. package/src/dev-port.ts +63 -0
  160. package/src/dev.ts +468 -66
  161. package/src/draft.ts +124 -12
  162. package/src/fn-state.ts +107 -0
  163. package/src/fn.ts +199 -0
  164. package/src/fonts-google.ts +13 -13
  165. package/src/fonts.ts +7 -7
  166. package/src/forms.ts +8 -8
  167. package/src/fs.ts +24 -6
  168. package/src/index.ts +1174 -225
  169. package/src/internal/head-attributes.ts +109 -0
  170. package/src/internal/script-types.ts +18 -0
  171. package/src/internal/server-boundary.ts +187 -0
  172. package/src/internal/timing-safe-equal.ts +24 -0
  173. package/src/internal/vite-import.ts +92 -0
  174. package/src/islands.ts +7 -7
  175. package/src/isr.ts +18 -30
  176. package/src/llms-txt.ts +13 -5
  177. package/src/manifest.ts +264 -48
  178. package/src/module-graph.ts +146 -0
  179. package/src/navigation.ts +151 -5
  180. package/src/plugins/css-modules.ts +0 -0
  181. package/src/plugins/kit.ts +11 -11
  182. package/src/plugins/scss.ts +3 -3
  183. package/src/plugins/vite-leak-guard.ts +110 -0
  184. package/src/plugins/vite-server-fn.ts +67 -0
  185. package/src/plugins/vite-server-only.ts +75 -0
  186. package/src/prerender.ts +10 -10
  187. package/src/public-dir.ts +108 -0
  188. package/src/query.ts +22 -22
  189. package/src/route-manifest.ts +240 -0
  190. package/src/router.ts +288 -74
  191. package/src/search.ts +297 -0
  192. package/src/server-only.ts +7 -7
  193. package/src/service-worker.ts +165 -0
  194. package/src/vite.ts +103 -49
package/src/deferred.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Deferred loader data (`defer()`). A loader may mark slow values as deferred: the critical data
3
3
  * renders in the shell, the deferred data streams in behind a `<Suspense>` (the adapter's `<Await>`
4
4
  * consumes it) and resolves on the client **without a re-fetch**. This module owns the agnostic
5
- * protocol — the marker, the resolved/deferred split, and the tiny client registry that streamed
5
+ * protocol - the marker, the resolved/deferred split, and the tiny client registry that streamed
6
6
  * resolution scripts settle. The per-adapter `<Await>` lives in `@nifrajs/web-{solid,react}/client`.
7
7
  */
8
8
  import { plainJsonCodec, type TransportCodec } from "@nifrajs/core/transport-codec"
@@ -10,7 +10,7 @@ import { plainJsonCodec, type TransportCodec } from "@nifrajs/core/transport-cod
10
10
  /**
11
11
  * A loader value marked to stream in after the shell. The component consumes it with the adapter's
12
12
  * `<Await resolve={...}>`; until the promise settles the shell shows the `<Suspense>` fallback.
13
- * `id` is assigned by the server at serialization time — the streamed resolve script keys off it.
13
+ * `id` is assigned by the server at serialization time - the streamed resolve script keys off it.
14
14
  */
15
15
  export interface Deferred<T> {
16
16
  readonly __nifra_deferred: true
@@ -19,8 +19,8 @@ export interface Deferred<T> {
19
19
  }
20
20
 
21
21
  /**
22
- * Mark a loader value as deferred — it streams in after the shell instead of blocking it. Works
23
- * **anywhere** in the loader's returned data — a top-level key, or nested in objects/arrays:
22
+ * Mark a loader value as deferred - it streams in after the shell instead of blocking it. Works
23
+ * **anywhere** in the loader's returned data - a top-level key, or nested in objects/arrays:
24
24
  *
25
25
  * return { user: await api.user.get(), feed: defer(api.user.feed()),
26
26
  * panels: [{ chart: defer(api.metrics()) }] }
@@ -48,8 +48,8 @@ function isDeferred(value: unknown): value is Deferred<unknown> {
48
48
  /**
49
49
  * Split a loader (or action) result into the **component-facing** data (deferred values carry their
50
50
  * assigned `id` + promise, for `<Await>`) and the **client-serializable** data (deferred values
51
- * replaced with a `{__nifra_deferred: id}` placeholder — promises don't serialize). Walks the tree
52
- * **recursively** — `defer()` works at any depth, inside nested objects and arrays — assigning ids in
51
+ * replaced with a `{__nifra_deferred: id}` placeholder - promises don't serialize). Walks the tree
52
+ * **recursively** - `defer()` works at any depth, inside nested objects and arrays - assigning ids in
53
53
  * walk order. `idOffset` continues the id space when a page splits two results (loader data **and**
54
54
  * action data) into one shared client registry, so their ids don't collide. `deferred` lists the
55
55
  * promises for callers that await them. Recurses plain objects + arrays only; the data is expected to
@@ -65,7 +65,7 @@ export function prepareDeferred(
65
65
  } {
66
66
  const deferred: Array<{ id: number; promise: Promise<unknown> }> = []
67
67
  // Structural sharing: a subtree with no deferred value returns the ORIGINAL node for
68
- // both sides (`fc`/`cl` identical to the input), so a deferred-free result — the common case — is
68
+ // both sides (`fc`/`cl` identical to the input), so a deferred-free result - the common case - is
69
69
  // returned by reference with no persisted clone, no downstream copy, and no throwaway clone
70
70
  // allocations. Only the path to a `defer()` marker is rebuilt; unchanged siblings are shared by
71
71
  // reference.
@@ -140,19 +140,19 @@ export function prepareDeferred(
140
140
  }
141
141
 
142
142
  /**
143
- * Stable, non-leaking payload streamed to the client when a deferred value rejects — never the raw
143
+ * Stable, non-leaking payload streamed to the client when a deferred value rejects - never the raw
144
144
  * error text. The real reason is logged server-side; `<Await>`'s `errorFallback` receives
145
145
  * this code. (A future typed `DeferredError` could opt into a public, intentional message.) Shared by
146
146
  * the NDJSON soft-nav transport and the full-document SSR path (`renderPage`'s `streamDocument`).
147
147
  */
148
148
  export const DEFERRED_ERROR_CODE = "deferred_error"
149
- // Shared, stateless — allocated once at module load, not per stream.
149
+ // Shared, stateless - allocated once at module load, not per stream.
150
150
  const NDJSON_ENCODER = new TextEncoder()
151
151
 
152
152
  /**
153
153
  * Stream a loader result as NDJSON for a client (soft) navigation: line 1 is the critical data with
154
154
  * `{__nifra_deferred: id}` placeholders (`forClient`), then one line per deferred as its promise
155
- * settles — `{"i": id, "v": value}` on resolve, `{"i": id, "e": "deferred_error"}` on reject (a
155
+ * settles - `{"i": id, "v": value}` on resolve, `{"i": id, "e": "deferred_error"}` on reject (a
156
156
  * rejection is data, not a stream error; the opaque code never leaks the raw reason). Closes when all
157
157
  * settle. The client (`defaultFetchData`) returns the
158
158
  * data after line 1 and settles `<Await>`'s markers as the resolution lines arrive. `JSON.stringify`
@@ -210,7 +210,7 @@ export const DEFERRED_RUNTIME = `(() => {
210
210
  return e;
211
211
  };
212
212
  window.__nifraDeferred = (id) => get(id).p;
213
- // Tag the promise (status/value/reason) — React's use() reads them and returns SYNCHRONOUSLY at
213
+ // Tag the promise (status/value/reason) - React's use() reads them and returns SYNCHRONOUSLY at
214
214
  // hydration, so it renders the resolved content directly into the <Suspense> boundary the server
215
215
  // streamed (no re-suspend, no fallback flash). r/j also wake awaiters (Solid uses its own _$HY).
216
216
  window.__nifraResolve = (id, v) => { const e = get(id); e.p.status = "fulfilled"; e.p.value = v; e.r(v); };
@@ -219,7 +219,7 @@ export const DEFERRED_RUNTIME = `(() => {
219
219
 
220
220
  /**
221
221
  * Source emitted into the generated client entry: maps serialized `{__nifra_deferred: id}` placeholders
222
- * (at any depth — nested objects/arrays) to the registry's promises, so the component receives real
222
+ * (at any depth - nested objects/arrays) to the registry's promises, so the component receives real
223
223
  * promises to `<Await>`. A no-op for data without placeholders (so non-deferred pages are unchanged).
224
224
  */
225
225
  export const MAP_DEFERRED_SOURCE = `const mapDeferred = (d) => {
@@ -233,18 +233,18 @@ export const MAP_DEFERRED_SOURCE = `const mapDeferred = (d) => {
233
233
 
234
234
  type Pending = Map<number, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>
235
235
 
236
- /** Replace `{__nifra_deferred: <number>}` placeholders (at any depth — nested objects/arrays) with
236
+ /** Replace `{__nifra_deferred: <number>}` placeholders (at any depth - nested objects/arrays) with
237
237
  * `Deferred` markers whose promises are held open in `pending` (settled later by
238
238
  * {@link parseNdjsonData}'s drain). Mirrors `prepareDeferred`'s recursive walk. */
239
239
  function markersFromPlaceholders(data: unknown, pending: Pending): unknown {
240
- // Markers already created in THIS parse, keyed by id — so a repeated id aliases the first marker.
240
+ // Markers already created in THIS parse, keyed by id - so a repeated id aliases the first marker.
241
241
  const byId = new Map<number, Deferred<unknown>>()
242
242
  const walk = (value: unknown): unknown => {
243
243
  const id = (value as { __nifra_deferred?: unknown } | null)?.__nifra_deferred
244
244
  if (value !== null && typeof value === "object" && typeof id === "number") {
245
245
  // The NDJSON stream is a trust boundary; the server assigns unique ids, but a corrupt/crafted
246
246
  // line-1 could repeat one. Alias a duplicate id to the first marker (same promise) instead of
247
- // overwriting its resolver in `pending` — otherwise the first marker would never settle (a
247
+ // overwriting its resolver in `pending` - otherwise the first marker would never settle (a
248
248
  // permanently-pending promise that leaks on abort). One resolve line then settles both.
249
249
  const seen = byId.get(id)
250
250
  if (seen !== undefined) return seen
@@ -280,7 +280,7 @@ function markersFromPlaceholders(data: unknown, pending: Pending): unknown {
280
280
  * the NDJSON body into a data object whose deferred markers settle as resolution lines arrive.
281
281
  * Returns after **line 1** so the router can apply the critical data + render immediately; the
282
282
  * markers' promises settle/reject in the background. If the stream ends (or `signal` aborts) with
283
- * markers unsettled, they reject — so `<Await>` never hangs.
283
+ * markers unsettled, they reject - so `<Await>` never hangs.
284
284
  */
285
285
  export async function parseNdjsonData(
286
286
  stream: ReadableStream<Uint8Array>,
@@ -331,7 +331,7 @@ export async function parseNdjsonData(
331
331
  else entry.resolve(msg.v)
332
332
  }
333
333
  } catch {
334
- // stream errored — leftovers are rejected below (unless this was an abort)
334
+ // stream errored - leftovers are rejected below (unless this was an abort)
335
335
  } finally {
336
336
  signal?.removeEventListener("abort", onAbort) // no-op if it already fired ({ once: true })
337
337
  // A natural stream end with markers unsettled = truncation → reject (so `<Await>` shows its
package/src/dev-error.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dev-only error overlay. When a loader/action/render throws during `@nifrajs/web/vite` dev, this
3
- * renders a readable full-page overlay — the message, the source-mapped stack (Vite's
4
- * `ssrFixStacktrace` runs first), and the request line — instead of the bare `err.stack` text dump.
3
+ * renders a readable full-page overlay - the message, the source-mapped stack (Vite's
4
+ * `ssrFixStacktrace` runs first), and the request line - instead of the bare `err.stack` text dump.
5
5
  * Dev-only by construction: it's called solely from the Vite middleware's catch, never in
6
6
  * production (production maps errors to the `_error` route boundary).
7
7
  */
@@ -35,7 +35,7 @@ export function renderDevErrorOverlay(err: unknown, req: { method: string; url:
35
35
  frames.length > 0
36
36
  ? `<ol class="frames">${frames.map((f) => `<li>${esc(f)}</li>`).join("")}</ol>`
37
37
  : `<p class="no-frames">No stack frames.</p>`
38
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${name} — nifra dev</title>
38
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${name} - nifra dev</title>
39
39
  <style>
40
40
  :root { color-scheme: dark }
41
41
  body { margin: 0; background: #0b0d10; color: #e6e6e6; font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `@nifrajs/web/dev-port` - what a dev server says when it cannot bind.
3
+ *
4
+ * Both pipelines need this and neither should own it: the Vite server binds through Node's `http`
5
+ * (asynchronous `error` event), the Bun server binds through `Bun.serve` (synchronous throw). Two very
6
+ * different failure mechanics, one thing the developer has to be told.
7
+ *
8
+ * That message is worth this much care because the visible symptom is not the real one. When the port is
9
+ * taken, the EARLIER dev server is still answering on it, still serving the build it started with. Every
10
+ * subsequent edit appears to do nothing. What reaches the developer is "my changes stopped reaching SSR",
11
+ * which reads as broken HMR or a stale module graph and sends them into the module graph - not to the one
12
+ * process that never started. Naming the cause here is the entire fix.
13
+ *
14
+ * This module deliberately imports nothing: it is shared by a Node-http path and a Bun path, and the
15
+ * structural `ListenTarget` keeps it free of either runtime's types.
16
+ */
17
+
18
+ /** The `EADDRINUSE` explanation. Exported so tests pin the exact text a user will read. */
19
+ export function portInUseMessage(port: number): string {
20
+ return (
21
+ `[nifra] dev server can't start: port ${port} is already in use.\n` +
22
+ ` Most likely an earlier \`nifra dev\` is still running. It keeps serving the PREVIOUS build, so ` +
23
+ `every edit will look like it stops reaching SSR while the browser shows stale output.\n` +
24
+ ` Free the port: lsof -ti:${port} | xargs kill\n` +
25
+ ` Or use another: nifra dev --port ${port + 1}`
26
+ )
27
+ }
28
+
29
+ /** True when an unknown thrown value carries the "address already in use" errno. */
30
+ export const isAddressInUse = (err: unknown): boolean =>
31
+ (err as { code?: unknown } | null)?.code === "EADDRINUSE"
32
+
33
+ const asError = (err: unknown): Error => (err instanceof Error ? err : new Error(String(err)))
34
+
35
+ /** Map a bind failure to the port-collision explanation, leaving every other failure as itself. */
36
+ export const explainBindFailure = (err: unknown, port: number): Error =>
37
+ isAddressInUse(err) ? new Error(portInUseMessage(port)) : asError(err)
38
+
39
+ /** The bits of a Node server {@link listenOrExplain} touches - structural, so a test can fake it. */
40
+ export interface ListenTarget {
41
+ listen(port: number, cb: () => void): void
42
+ once(event: "error", cb: (err: unknown) => void): void
43
+ removeListener(event: "error", cb: (err: unknown) => void): void
44
+ }
45
+
46
+ /**
47
+ * `listen`, but a bind failure becomes a readable nifra error instead of Node's raw internal throw.
48
+ *
49
+ * Without an `error` listener attached, Node throws from deep inside `node:events`, the new process dies
50
+ * in the background, and nothing connects that death to the stale page in front of you.
51
+ */
52
+ export function listenOrExplain(server: ListenTarget, port: number): Promise<void> {
53
+ return new Promise<void>((resolve, reject) => {
54
+ const onError = (err: unknown): void => reject(explainBindFailure(err, port))
55
+ server.once("error", onError)
56
+ server.listen(port, () => {
57
+ // Drop the guard once we're listening: leaving it attached would funnel a LATER server error into
58
+ // an already-settled promise, silently swallowing it instead of surfacing it.
59
+ server.removeListener("error", onError)
60
+ resolve()
61
+ })
62
+ })
63
+ }