@nwire/cli 0.12.1 → 0.13.1

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 (131) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/commands/cache.js +1 -1
  4. package/dist/commands/dev.d.ts +20 -7
  5. package/dist/commands/dev.js +142 -45
  6. package/dist/commands/ls.js +5 -3
  7. package/dist/commands/please.js +4 -2
  8. package/dist/commands/run.js +6 -2
  9. package/dist/commands/studio.js +74 -7
  10. package/dist/lib/dev-entry.d.ts +8 -9
  11. package/dist/lib/dev-entry.js +24 -25
  12. package/dist/lib/dev-host.d.ts +88 -0
  13. package/dist/lib/dev-host.js +438 -0
  14. package/dist/lib/ensure-built.d.ts +32 -0
  15. package/dist/lib/ensure-built.js +62 -0
  16. package/dist/lib/ensure-scan.d.ts +9 -4
  17. package/dist/lib/ensure-scan.js +46 -19
  18. package/dist/lib/studio-host-api.d.ts +97 -0
  19. package/dist/lib/studio-host-api.js +337 -0
  20. package/dist/lib/studio-probe.d.ts +63 -0
  21. package/dist/lib/studio-probe.js +132 -0
  22. package/dist/lib/vite-node.d.ts +7 -6
  23. package/dist/lib/vite-node.js +8 -8
  24. package/dist/lib/vite-run.d.ts +15 -0
  25. package/dist/lib/vite-run.js +33 -0
  26. package/dist/load-config.d.ts +10 -10
  27. package/dist/load-config.js +3 -3
  28. package/dist/studio/assets/abap-DVwoIrM0.js +1 -0
  29. package/dist/studio/assets/apex-B9XtvxSu.js +1 -0
  30. package/dist/studio/assets/azcli-D7JTNGKs.js +1 -0
  31. package/dist/studio/assets/bat-BNHAuPwR.js +1 -0
  32. package/dist/studio/assets/bicep-C3w6oSfK.js +2 -0
  33. package/dist/studio/assets/cameligo-DM9kSiq7.js +1 -0
  34. package/dist/studio/assets/clojure-FWLBUPxU.js +1 -0
  35. package/dist/studio/assets/codicon-ngg6Pgfi.ttf +0 -0
  36. package/dist/studio/assets/coffee-DCoMPIwW.js +1 -0
  37. package/dist/studio/assets/cpp-BNbIvdcw.js +1 -0
  38. package/dist/studio/assets/csharp-Dj4ULDZr.js +1 -0
  39. package/dist/studio/assets/csp-C-n5jZKF.js +1 -0
  40. package/dist/studio/assets/css-COIa8ZTR.js +3 -0
  41. package/dist/studio/assets/css.worker-CpJJqcA4.js +89 -0
  42. package/dist/studio/assets/cssMode-CFR5_xwk.js +1 -0
  43. package/dist/studio/assets/cypher-CW08XVUh.js +1 -0
  44. package/dist/studio/assets/dart-Bz550Pyv.js +1 -0
  45. package/dist/studio/assets/dockerfile-DW5REF8E.js +1 -0
  46. package/dist/studio/assets/ecl-BqdYhwmw.js +1 -0
  47. package/dist/studio/assets/editor-Br_kD0ds.css +1 -0
  48. package/dist/studio/assets/editor.api2-CTGEM8gT.js +872 -0
  49. package/dist/studio/assets/editor.main-sW1qgHFj.js +6 -0
  50. package/dist/studio/assets/elixir-Oi_9aIAu.js +1 -0
  51. package/dist/studio/assets/flow9-CIb9youF.js +1 -0
  52. package/dist/studio/assets/freemarker2-CPrni8hw.js +3 -0
  53. package/dist/studio/assets/fsharp-64tUaD-0.js +1 -0
  54. package/dist/studio/assets/go-DLKGL0rd.js +1 -0
  55. package/dist/studio/assets/graphql-Bz88xn3Q.js +1 -0
  56. package/dist/studio/assets/handlebars-DkvSNpQB.js +1 -0
  57. package/dist/studio/assets/hcl-Cq76tSVN.js +1 -0
  58. package/dist/studio/assets/html-ceN7ITxG.js +1 -0
  59. package/dist/studio/assets/html.worker-wsVgX3gp.js +502 -0
  60. package/dist/studio/assets/htmlMode-wduanCXn.js +1 -0
  61. package/dist/studio/assets/index-4tH0-1cA.js +41 -0
  62. package/dist/studio/assets/index-Fy3xDmV2.css +1 -0
  63. package/dist/studio/assets/ini-BTNe9zdh.js +1 -0
  64. package/dist/studio/assets/java-DzRJKRF3.js +1 -0
  65. package/dist/studio/assets/javascript-WF3LGLje.js +1 -0
  66. package/dist/studio/assets/json.worker-CcNiYOcv.js +58 -0
  67. package/dist/studio/assets/jsonMode-DSujY8tB.js +7 -0
  68. package/dist/studio/assets/julia-Bgv08lKa.js +1 -0
  69. package/dist/studio/assets/kotlin-Dzz8TWAt.js +1 -0
  70. package/dist/studio/assets/less-ak6GUtsl.js +2 -0
  71. package/dist/studio/assets/lexon-zuaObGAE.js +1 -0
  72. package/dist/studio/assets/liquid-C5Z7zFr3.js +1 -0
  73. package/dist/studio/assets/lspLanguageFeatures-B55yfFgf.js +4 -0
  74. package/dist/studio/assets/lua-ClKCZMmP.js +1 -0
  75. package/dist/studio/assets/m3-C7XHeDz_.js +1 -0
  76. package/dist/studio/assets/markdown-LT3qFBoR.js +1 -0
  77. package/dist/studio/assets/mdx-Bu5jRl19.js +1 -0
  78. package/dist/studio/assets/mips-B8clQ9KB.js +1 -0
  79. package/dist/studio/assets/monaco.contribution-KjQ4yOxj.js +2 -0
  80. package/dist/studio/assets/msdax-DBxc5qPJ.js +1 -0
  81. package/dist/studio/assets/mysql-qocW_xba.js +1 -0
  82. package/dist/studio/assets/objective-c-DhkpBlGX.js +1 -0
  83. package/dist/studio/assets/pascal-C_PJR40u.js +1 -0
  84. package/dist/studio/assets/pascaligo-BI_Gz9Bp.js +1 -0
  85. package/dist/studio/assets/perl-CIqGOHTo.js +1 -0
  86. package/dist/studio/assets/pgsql-DI_z9qfW.js +1 -0
  87. package/dist/studio/assets/php-Dkwn_yn0.js +1 -0
  88. package/dist/studio/assets/pla-DvzjACL6.js +1 -0
  89. package/dist/studio/assets/postiats-Cc9-hkUx.js +1 -0
  90. package/dist/studio/assets/powerquery-IGzsITFg.js +1 -0
  91. package/dist/studio/assets/powershell-BHlZlUN6.js +1 -0
  92. package/dist/studio/assets/protobuf-pGrmMUz5.js +2 -0
  93. package/dist/studio/assets/pug-B4eH693Y.js +1 -0
  94. package/dist/studio/assets/python-DpEFuk0I.js +1 -0
  95. package/dist/studio/assets/qsharp-CwO3kTIU.js +1 -0
  96. package/dist/studio/assets/r-CiZUpdIa.js +1 -0
  97. package/dist/studio/assets/razor-BF1svRn9.js +1 -0
  98. package/dist/studio/assets/redis-DjdIzLdf.js +1 -0
  99. package/dist/studio/assets/redshift-vCL5QMyw.js +1 -0
  100. package/dist/studio/assets/restructuredtext-D5hoMHB1.js +1 -0
  101. package/dist/studio/assets/ruby-ByPQrqP4.js +1 -0
  102. package/dist/studio/assets/rust-Nz5wukP7.js +1 -0
  103. package/dist/studio/assets/sb-DgR1RbMJ.js +1 -0
  104. package/dist/studio/assets/scala-BCgNuXrV.js +1 -0
  105. package/dist/studio/assets/scheme-TgKpKGpb.js +1 -0
  106. package/dist/studio/assets/scss-BKxAkvnT.js +3 -0
  107. package/dist/studio/assets/shell-COgstXIQ.js +1 -0
  108. package/dist/studio/assets/solidity-DaqmtBSV.js +1 -0
  109. package/dist/studio/assets/sophia-Da67i9pL.js +1 -0
  110. package/dist/studio/assets/sparql-CtN8jEDV.js +1 -0
  111. package/dist/studio/assets/sql-BnJfQHXt.js +1 -0
  112. package/dist/studio/assets/st-cGKU4FoP.js +1 -0
  113. package/dist/studio/assets/swift-DyyME8zA.js +1 -0
  114. package/dist/studio/assets/systemverilog-BYZY5TEG.js +1 -0
  115. package/dist/studio/assets/tcl-CHv1_zaR.js +1 -0
  116. package/dist/studio/assets/ts.worker-DfMAw22J.js +67719 -0
  117. package/dist/studio/assets/tsMode-BiiF1JOM.js +11 -0
  118. package/dist/studio/assets/twig-CNwULq4h.js +1 -0
  119. package/dist/studio/assets/typescript-BefpzegH.js +1 -0
  120. package/dist/studio/assets/typespec-B3KUNs_P.js +1 -0
  121. package/dist/studio/assets/vb-phZZ2pCs.js +1 -0
  122. package/dist/studio/assets/wgsl-Df-y4I4e.js +298 -0
  123. package/dist/studio/assets/workers-DqAl3RFu.js +1 -0
  124. package/dist/studio/assets/xml-CWw7bbeo.js +1 -0
  125. package/dist/studio/assets/yaml-CZ0k9DUm.js +1 -0
  126. package/dist/studio/index.html +13 -0
  127. package/dist/wire-runner.d.ts +12 -0
  128. package/dist/wire-runner.js +19 -0
  129. package/package.json +8 -6
  130. package/dist/cache-runner.d.ts +0 -1
  131. package/dist/cache-runner.js +0 -206
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The one-Vite dev host — `nwire dev`'s single process.
3
+ *
4
+ * A programmatic Vite server that loads the wire via `ssrLoadModule` (real TS
5
+ * + HMR, no `vite-node` child) and serves, on ONE port:
6
+ * - the wire's HTTP surface — mounted as Connect-style middleware from
7
+ * each adapter's transport-agnostic `handler()`. Adapters self-dispatch:
8
+ * if they don't own a URL they call `next()` and the next handler (or
9
+ * Vite's middleware chain) gets a shot. No prefix hard-coding in the host.
10
+ * - the Studio data API (`/__nwire/*`) — served from the in-process runtime
11
+ * directly via `studio-host-api.ts`. No disk/proxy indirection.
12
+ * - the built Studio SPA (everything else, with SPA fallback).
13
+ *
14
+ * PROD IS UNTOUCHED: the wire's `main.ts` still calls `endpoint().run()`. This
15
+ * host only sets `globalThis.__nwireDevHost` before loading it, which flips the
16
+ * endpoint into mount-without-listen mode and hands back the booted app(s) +
17
+ * each adapter's `(req,res,next)` handler. Non-HTTP transports (queue/cron/nats)
18
+ * just run in-process; the host never names a transport.
19
+ *
20
+ * ## HMR reload model
21
+ *
22
+ * When a file that feeds the wire changes, Vite emits a `change` event on its
23
+ * watcher. The host:
24
+ * 1. Acquires a reload lock — concurrent file saves are coalesced into one
25
+ * reload; any in-flight reload is let complete before the next starts.
26
+ * 2. Stops all booted apps in reverse order (mirrors the prod shutdown loop).
27
+ * 3. Invalidates the changed module and its entire import chain in Vite's SSR
28
+ * module graph so the next `ssrLoadModule` re-evaluates from disk.
29
+ * 4. Resets `mounts[]` and re-calls `ssrLoadModule(entry)` so the new wire
30
+ * is collected afresh.
31
+ * 5. Atomically swaps `wireHandlers` (a `let` the request handler closure
32
+ * reads) so in-flight requests are not interrupted.
33
+ *
34
+ * There is no double-boot risk: step 2 tears down the previous generation
35
+ * before step 4 boots the next. The global collector is reset in step 4 before
36
+ * loading so stale collect() calls from the previous generation are inert.
37
+ *
38
+ * Studio's own assets are the built SPA (static, served by sirv) — HMR for
39
+ * the wire source does not affect the Studio bundle.
40
+ */
41
+ import { type Server } from "node:http";
42
+ import { type ViteDevServer } from "vite";
43
+ import type { DevHostMount } from "@nwire/endpoint";
44
+ export interface DevHostOptions {
45
+ /** Project root — Vite's root + where the wire + `.nwire/` live. */
46
+ readonly cwd: string;
47
+ /** Wire entry, root-relative (e.g. `/app/main.ts`). */
48
+ readonly entry: string;
49
+ /** Port for the single HTTP front. */
50
+ readonly port: number;
51
+ /** Bind host. Default `127.0.0.1`. */
52
+ readonly host?: string;
53
+ /**
54
+ * Initial active endpoint name. When provided, the host will front that
55
+ * endpoint's HTTP handlers. Defaults to the first mount with HTTP handlers.
56
+ */
57
+ readonly activeEndpoint?: string;
58
+ }
59
+ export interface DevHost {
60
+ readonly vite: ViteDevServer;
61
+ readonly server: Server;
62
+ /** What each loaded endpoint handed over (one per `endpoint().run()`). */
63
+ readonly mounts: readonly DevHostMount[];
64
+ /** Name of the currently active HTTP front (or undefined when none present). */
65
+ readonly activeEndpointName: string | undefined;
66
+ /**
67
+ * Switch the active HTTP front at runtime. Throws when no mount with the
68
+ * given name exists. The change takes effect on the next incoming request.
69
+ */
70
+ setActiveEndpoint(name: string): void;
71
+ close(): Promise<void>;
72
+ }
73
+ /**
74
+ * Determine which mount should front the HTTP surface.
75
+ *
76
+ * Selection rules (in order):
77
+ * 1. When `preferredName` is given, use the mount with that name (if it has HTTP handlers).
78
+ * 2. Otherwise pick the first mount that carries at least one HTTP handler.
79
+ * 3. When no mount has HTTP handlers, fall back to the first mount (for non-HTTP projects).
80
+ *
81
+ * Returns `undefined` when `mounts` is empty.
82
+ */
83
+ export declare function resolveActiveMount(mounts: readonly DevHostMount[], preferredName?: string): DevHostMount | undefined;
84
+ /**
85
+ * Boot the dev host. Registers the dev-host collector, loads the wire (which
86
+ * mounts port-less + collects), then serves the wire + Studio on one port.
87
+ */
88
+ export declare function startDevHost(opts: DevHostOptions): Promise<DevHost>;
@@ -0,0 +1,438 @@
1
+ /**
2
+ * The one-Vite dev host — `nwire dev`'s single process.
3
+ *
4
+ * A programmatic Vite server that loads the wire via `ssrLoadModule` (real TS
5
+ * + HMR, no `vite-node` child) and serves, on ONE port:
6
+ * - the wire's HTTP surface — mounted as Connect-style middleware from
7
+ * each adapter's transport-agnostic `handler()`. Adapters self-dispatch:
8
+ * if they don't own a URL they call `next()` and the next handler (or
9
+ * Vite's middleware chain) gets a shot. No prefix hard-coding in the host.
10
+ * - the Studio data API (`/__nwire/*`) — served from the in-process runtime
11
+ * directly via `studio-host-api.ts`. No disk/proxy indirection.
12
+ * - the built Studio SPA (everything else, with SPA fallback).
13
+ *
14
+ * PROD IS UNTOUCHED: the wire's `main.ts` still calls `endpoint().run()`. This
15
+ * host only sets `globalThis.__nwireDevHost` before loading it, which flips the
16
+ * endpoint into mount-without-listen mode and hands back the booted app(s) +
17
+ * each adapter's `(req,res,next)` handler. Non-HTTP transports (queue/cron/nats)
18
+ * just run in-process; the host never names a transport.
19
+ *
20
+ * ## HMR reload model
21
+ *
22
+ * When a file that feeds the wire changes, Vite emits a `change` event on its
23
+ * watcher. The host:
24
+ * 1. Acquires a reload lock — concurrent file saves are coalesced into one
25
+ * reload; any in-flight reload is let complete before the next starts.
26
+ * 2. Stops all booted apps in reverse order (mirrors the prod shutdown loop).
27
+ * 3. Invalidates the changed module and its entire import chain in Vite's SSR
28
+ * module graph so the next `ssrLoadModule` re-evaluates from disk.
29
+ * 4. Resets `mounts[]` and re-calls `ssrLoadModule(entry)` so the new wire
30
+ * is collected afresh.
31
+ * 5. Atomically swaps `wireHandlers` (a `let` the request handler closure
32
+ * reads) so in-flight requests are not interrupted.
33
+ *
34
+ * There is no double-boot risk: step 2 tears down the previous generation
35
+ * before step 4 boots the next. The global collector is reset in step 4 before
36
+ * loading so stale collect() calls from the previous generation are inert.
37
+ *
38
+ * Studio's own assets are the built SPA (static, served by sirv) — HMR for
39
+ * the wire source does not affect the Studio bundle.
40
+ */
41
+ import { createServer as createHttpServer, } from "node:http";
42
+ import { existsSync } from "node:fs";
43
+ import { dirname, resolve } from "node:path";
44
+ import { fileURLToPath } from "node:url";
45
+ import { createRequire } from "node:module";
46
+ import sirv from "sirv";
47
+ import { createServer as createViteServer } from "vite";
48
+ import { createStudioHostApi } from "./studio-host-api.js";
49
+ import { ensureScanFresh } from "./ensure-scan.js";
50
+ // ── Active-endpoint selection helpers ────────────────────────────────────────
51
+ /**
52
+ * Determine which mount should front the HTTP surface.
53
+ *
54
+ * Selection rules (in order):
55
+ * 1. When `preferredName` is given, use the mount with that name (if it has HTTP handlers).
56
+ * 2. Otherwise pick the first mount that carries at least one HTTP handler.
57
+ * 3. When no mount has HTTP handlers, fall back to the first mount (for non-HTTP projects).
58
+ *
59
+ * Returns `undefined` when `mounts` is empty.
60
+ */
61
+ export function resolveActiveMount(mounts, preferredName) {
62
+ if (mounts.length === 0)
63
+ return undefined;
64
+ // Preferred by name
65
+ if (preferredName !== undefined) {
66
+ const byName = mounts.find((m) => m.name === preferredName);
67
+ if (byName)
68
+ return byName;
69
+ }
70
+ // First with HTTP handlers
71
+ const firstHttp = mounts.find((m) => m.handlers.length > 0);
72
+ if (firstHttp)
73
+ return firstHttp;
74
+ // Fallback: first mount (non-HTTP project)
75
+ return mounts[0];
76
+ }
77
+ /** Resolve the Studio SPA dist dir.
78
+ *
79
+ * Resolution order:
80
+ * 1. Bundled copy vendored into the CLI's own dist/studio at build time —
81
+ * the normal path for any scaffolded app (no @nwire/studio dep needed).
82
+ * 2. Consumer project's node_modules — for projects that install @nwire/studio
83
+ * explicitly (dev-deps workflow, in-repo hacking).
84
+ * 3. CLI's own node_modules — covers the framework workspace dev loop.
85
+ */
86
+ function resolveStudioDist(cwd) {
87
+ // 1. Bundled inside the CLI's own dist/studio.
88
+ const bundled = resolve(dirname(fileURLToPath(import.meta.url)), "..", "studio");
89
+ if (existsSync(bundled))
90
+ return bundled;
91
+ // 2 & 3. Fall back to resolving @nwire/studio from node_modules.
92
+ const tryFrom = (base) => {
93
+ try {
94
+ const pkg = createRequire(base).resolve("@nwire/studio/package.json");
95
+ const dist = resolve(dirname(pkg), "dist");
96
+ return dist;
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ };
102
+ return tryFrom(resolve(cwd, "package.json")) ?? tryFrom(fileURLToPath(import.meta.url));
103
+ }
104
+ /**
105
+ * Wrap a raw `(req, res)` adapter handler as a Connect-style handler.
106
+ *
107
+ * Koa's `callback()` (and equivalents) already behave like a Connect-style
108
+ * middleware for routes they don't own — they respond with 404 for unknown
109
+ * routes rather than calling a next function. We normalise them here: if the
110
+ * handler writes a 404 status without any body, we treat that as "not owned"
111
+ * and call `next()` instead, so the host's chain continues.
112
+ *
113
+ * Most adapters already short-circuit on their owned prefix; this wrapper is
114
+ * defensive — it ensures unknown routes fall through cleanly in all cases.
115
+ */
116
+ function toConnectHandler(raw) {
117
+ return (req, res, next) => {
118
+ // Intercept the response to detect a "not found" pass-through from the adapter.
119
+ let headerWritten = false;
120
+ const origWriteHead = res.writeHead.bind(res);
121
+ const origWrite = res.write.bind(res);
122
+ const origEnd = res.end.bind(res);
123
+ // Track whether the adapter wrote a real response body.
124
+ let bodyWritten = false;
125
+ let capturedStatus;
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ res.writeHead = (statusCode, ...args) => {
128
+ capturedStatus = statusCode;
129
+ headerWritten = true;
130
+ return origWriteHead(statusCode, ...args);
131
+ };
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ res.write = (...args) => {
134
+ bodyWritten = true;
135
+ return origWrite(...args);
136
+ };
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ res.end = (...args) => {
139
+ // Restore patched methods before deciding
140
+ res.writeHead = origWriteHead;
141
+ res.write = origWrite;
142
+ res.end = origEnd;
143
+ // Effective status: `capturedStatus` when the adapter called writeHead()
144
+ // explicitly; otherwise fall back to `res.statusCode` (Koa sets this
145
+ // directly without ever calling writeHead before end).
146
+ const effectiveStatus = capturedStatus ?? res.statusCode;
147
+ const firstArg = args[0];
148
+ // "Not Found" is Koa's default catch-all body for unmatched routes (Koa
149
+ // sets res.statusCode = 404 and calls res.end("Not Found") directly,
150
+ // never calling writeHead). Treat it as "not mine" so the host's chain
151
+ // continues to Vite + sirv.
152
+ const isDefaultNotFound = firstArg === "Not Found" ||
153
+ (firstArg instanceof Buffer && firstArg.toString() === "Not Found");
154
+ const hasBody = bodyWritten ||
155
+ (!isDefaultNotFound &&
156
+ firstArg !== undefined &&
157
+ firstArg !== null &&
158
+ firstArg !== "" &&
159
+ !(firstArg instanceof Function));
160
+ // If the adapter signalled a 404 with no meaningful body and no prior
161
+ // writes, treat as "not mine" and fall through to the next handler in
162
+ // the chain (Vite → sirv). Otherwise commit the response.
163
+ if (effectiveStatus !== 404 || hasBody) {
164
+ return origEnd(...args);
165
+ }
166
+ // Hand the socket to the next handler in the chain (Vite → sirv).
167
+ //
168
+ // Koa sets response headers (Content-Type, Content-Length, x-correlation-id, etc.)
169
+ // on `res` BEFORE calling `res.end`. When we intercept and redirect to `next()`,
170
+ // those stale headers must be removed so sirv can write a fresh 200 response.
171
+ // Otherwise the client reads the wrong Content-Length and misparses the body.
172
+ //
173
+ // Koa (and other async-middleware frameworks) also schedule a `handleResponse`
174
+ // call on the next microtask tick. By the time that fires, `res.writableEnded`
175
+ // will be `true` (sirv's write completed), so Node's internal http machinery
176
+ // silently ignores the second end() call. No guard needed.
177
+ for (const name of res.getHeaderNames()) {
178
+ res.removeHeader(name);
179
+ }
180
+ res.statusCode = 200;
181
+ next();
182
+ };
183
+ raw(req, res);
184
+ };
185
+ }
186
+ // ── Reload helpers ───────────────────────────────────────────────────────────
187
+ /**
188
+ * Drain all mounts in reverse-registration order, mirroring the prod shutdown
189
+ * loop. Each mount's `shutdown()` tears down its adapters (reverse) then its
190
+ * apps — so non-HTTP transports (queue/cron/nats) that started in-process get
191
+ * cleaned up, not just the apps. Errors are swallowed so a single failing mount
192
+ * doesn't block the rest (reload + close must always continue).
193
+ */
194
+ async function stopMounts(mounts, reason = "dev-host") {
195
+ for (let i = mounts.length - 1; i >= 0; i--) {
196
+ try {
197
+ await mounts[i].shutdown(reason);
198
+ }
199
+ catch {
200
+ // Swallow — reload/close must continue even if one mount fails to drain.
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * Invalidate a changed file and its entire SSR import chain in Vite's module
206
+ * graph. This ensures `ssrLoadModule` re-evaluates from disk on the next call
207
+ * rather than returning a stale cached module.
208
+ */
209
+ function invalidateModuleGraph(vite, file) {
210
+ const mods = vite.moduleGraph.getModulesByFile(file);
211
+ if (!mods)
212
+ return;
213
+ for (const mod of mods) {
214
+ vite.moduleGraph.invalidateModule(mod);
215
+ }
216
+ }
217
+ /**
218
+ * Boot the dev host. Registers the dev-host collector, loads the wire (which
219
+ * mounts port-less + collects), then serves the wire + Studio on one port.
220
+ */
221
+ export async function startDevHost(opts) {
222
+ // `mounts` is the live array that `createStudioHostApi` reads on every request.
223
+ // It is replaced (reset + repopulated) on each HMR reload without the server
224
+ // or the Studio handler needing to be recreated.
225
+ const mounts = [];
226
+ // `activeEndpointName` tracks which mount is fronting the HTTP surface.
227
+ // Reads are synchronous (the request handler closure snaps it per request).
228
+ // Writes take effect immediately — the next request sees the new front.
229
+ let activeEndpointName = opts.activeEndpoint;
230
+ // Install the global collector. The endpoint reads this and hands back its
231
+ // apps + handlers without binding a port.
232
+ function installCollector() {
233
+ globalThis.__nwireDevHost = {
234
+ collect: (m) => mounts.push(m),
235
+ };
236
+ }
237
+ installCollector();
238
+ const vite = await createViteServer({
239
+ root: opts.cwd,
240
+ // Disable consumer vite.config.ts discovery — the host only needs Vite for
241
+ // SSR module loading + the HMR client. Pulling in frontend plugins (vue,
242
+ // tailwind, svelte) would slow boot and may conflict with the SSR pipeline.
243
+ configFile: false,
244
+ server: { middlewareMode: true },
245
+ appType: "custom",
246
+ logLevel: "warn",
247
+ });
248
+ // `wireHandlers` is a `let` so reloads can atomically swap the reference the
249
+ // request handler closure reads without re-creating the HTTP server.
250
+ let wireHandlers = [];
251
+ // `activeOwns` answers "does the wire own this URL path?" for the active
252
+ // mount. When present, the host routes owned paths to the wire and everything
253
+ // else straight to Studio — no response-sniffing. Undefined when the adapter
254
+ // doesn't compute ownership; the host then runs handlers + sniffs a 404.
255
+ let activeOwns;
256
+ /** Point the HTTP front at a resolved mount (handlers + ownership). */
257
+ function adoptMount(mount) {
258
+ wireHandlers = (mount?.handlers ?? []).map((h) => toConnectHandler(h));
259
+ activeOwns = mount?.owns;
260
+ }
261
+ async function loadEntry() {
262
+ await vite.ssrLoadModule(opts.entry);
263
+ // Re-resolve the active mount after each (re)load. When the user specified
264
+ // a preferred name, honour it; otherwise fall back to the first HTTP mount.
265
+ const active = resolveActiveMount(mounts, activeEndpointName);
266
+ activeEndpointName = active?.name;
267
+ // Only the active mount's handlers front the HTTP surface. Other mounts
268
+ // (workers/cron/non-HTTP) still run in-process and are observable via
269
+ // telemetry + manifest.
270
+ adoptMount(active);
271
+ // Build/refresh `.nwire/manifest.json` so Studio's manifest-backed pages
272
+ // (Map, Trace, Plugins, schemas…) have data from the first request and stay
273
+ // current across HMR reloads. Static scan + topology fold — no app boot of
274
+ // its own; fingerprint-gated so an unchanged tree is a near no-op. A scan
275
+ // failure must never take down the dev host, so it's best-effort.
276
+ try {
277
+ await ensureScanFresh(opts.cwd, { quiet: true });
278
+ }
279
+ catch {
280
+ // manifest stays as-is; Studio data pages degrade, the wire keeps serving.
281
+ }
282
+ }
283
+ // Initial load — boot the wire for the first time.
284
+ await loadEntry();
285
+ // ── Studio data API ─────────────────────────────────────────────────────
286
+ // `mounts` is passed by reference — the API reads the current snapshot on
287
+ // every request, so it automatically picks up the new runtime after a reload.
288
+ // The getter/setter let the endpoints route switch the active front at runtime.
289
+ const studioApi = createStudioHostApi({
290
+ mounts,
291
+ cwd: opts.cwd,
292
+ getActiveEndpointName: () => activeEndpointName,
293
+ setActiveEndpoint: (name) => {
294
+ const target = mounts.find((m) => m.name === name);
295
+ if (!target)
296
+ throw new Error(`No mounted endpoint named "${name}"`);
297
+ activeEndpointName = name;
298
+ adoptMount(target);
299
+ },
300
+ });
301
+ // ── Studio SPA (static) ─────────────────────────────────────────────────
302
+ const studioDist = resolveStudioDist(opts.cwd);
303
+ const serveStudio = studioDist ? sirv(studioDist, { single: true, dev: false }) : undefined;
304
+ // ── HTTP server ─────────────────────────────────────────────────────────
305
+ // Serve Vite + the built Studio SPA (with SPA fallback). The tail of the
306
+ // request chain for any path the wire does not own.
307
+ const serveStudioChain = (req, res) => {
308
+ // Vite middlewares (transform/HMR client) first, then the built SPA.
309
+ vite.middlewares(req, res, () => {
310
+ if (serveStudio) {
311
+ serveStudio(req, res, () => {
312
+ res.statusCode = 404;
313
+ res.end("not found");
314
+ });
315
+ }
316
+ else {
317
+ res.statusCode = 404;
318
+ res.end("@nwire/studio not found");
319
+ }
320
+ });
321
+ };
322
+ const server = createHttpServer((req, res) => {
323
+ const pathname = new URL(req.url ?? "/", "http://x").pathname;
324
+ // 1. Studio data API — in-process runtime, no disk/proxy indirection.
325
+ if (studioApi(req, res))
326
+ return;
327
+ // 2. When the active mount declares ownership, route deterministically:
328
+ // a path the wire doesn't own (`/`, Studio assets + deep links) goes
329
+ // straight to Studio — even when the wire has adopter-wide auth that
330
+ // would otherwise 401 every path and shadow Studio.
331
+ if (activeOwns && !activeOwns(pathname)) {
332
+ serveStudioChain(req, res);
333
+ return;
334
+ }
335
+ // 3. Wire HTTP surface — Connect-style chain. Each adapter self-dispatches:
336
+ // if it doesn't own the URL it calls next() and the chain continues.
337
+ // The toConnectHandler 404-sniff covers adapters without `owns`.
338
+ const handlers = wireHandlers; // read the current generation
339
+ let idx = 0;
340
+ const next = () => {
341
+ const h = handlers[idx++];
342
+ if (h) {
343
+ h(req, res, next);
344
+ }
345
+ else {
346
+ // 4. Fall through to Vite + the built Studio SPA.
347
+ serveStudioChain(req, res);
348
+ }
349
+ };
350
+ next();
351
+ });
352
+ // ── HMR reload loop ─────────────────────────────────────────────────────
353
+ //
354
+ // When any file that feeds the wire changes, Vite emits `change`. We:
355
+ // 1. Acquire a reload lock (one in-flight reload at a time; concurrent
356
+ // saves coalesce — the last queued reload wins).
357
+ // 2. Stop the current apps (reverse order, mirrors prod shutdown).
358
+ // 3. Invalidate the changed file in Vite's SSR module graph.
359
+ // 4. Reset `mounts[]` and re-execute the entry.
360
+ // 5. Atomically update `wireHandlers` so the request handler sees the
361
+ // new generation immediately.
362
+ //
363
+ // The global collector is reinstalled in step 4 (via `installCollector`)
364
+ // before `loadEntry` runs so any stale collect() calls from the old
365
+ // generation write into the freshly-reset `mounts[]`.
366
+ let reloading = false;
367
+ let pendingReload = false;
368
+ async function reload(changedFile) {
369
+ if (reloading) {
370
+ // Another reload is in flight — mark that a further reload is needed
371
+ // once the current one completes. This coalesces rapid saves.
372
+ pendingReload = true;
373
+ return;
374
+ }
375
+ reloading = true;
376
+ try {
377
+ // Stop the previous generation of apps.
378
+ await stopMounts(mounts);
379
+ // Reset the collector target and invalidate the module graph.
380
+ mounts.length = 0;
381
+ invalidateModuleGraph(vite, changedFile);
382
+ installCollector();
383
+ await loadEntry();
384
+ }
385
+ catch (err) {
386
+ // A load error (syntax/runtime) leaves `wireHandlers` empty until the
387
+ // user fixes the source. Log clearly; keep the server alive so the
388
+ // watcher can retry on the next save.
389
+ console.error("[nwire dev] reload failed:", err);
390
+ }
391
+ finally {
392
+ reloading = false;
393
+ }
394
+ if (pendingReload) {
395
+ pendingReload = false;
396
+ // Re-run with a sentinel path — the full module graph was already
397
+ // invalidated by the previous reload; an additional invalidation of the
398
+ // entry covers any further changes.
399
+ await reload(opts.entry);
400
+ }
401
+ }
402
+ vite.watcher.on("change", (file) => {
403
+ // Only reload when the changed file is part of the loaded SSR module
404
+ // graph. Changes to Studio assets (already built) are irrelevant.
405
+ const mods = vite.moduleGraph.getModulesByFile(file);
406
+ if (!mods?.size)
407
+ return;
408
+ void reload(file);
409
+ });
410
+ // ── Listen ──────────────────────────────────────────────────────────────
411
+ await new Promise((resolve, reject) => {
412
+ server.once("error", reject);
413
+ server.listen(opts.port, opts.host ?? "127.0.0.1", resolve);
414
+ });
415
+ return {
416
+ vite,
417
+ server,
418
+ mounts,
419
+ get activeEndpointName() {
420
+ return activeEndpointName;
421
+ },
422
+ setActiveEndpoint(name) {
423
+ const target = mounts.find((m) => m.name === name);
424
+ if (!target)
425
+ throw new Error(`No mounted endpoint named "${name}"`);
426
+ activeEndpointName = name;
427
+ adoptMount(target);
428
+ },
429
+ async close() {
430
+ // Stop accepting new connections first.
431
+ await new Promise((r) => server.close(() => r()));
432
+ // Close Vite (stops the watcher + HMR WebSocket).
433
+ await vite.close();
434
+ // Drain the current wire generation cleanly.
435
+ await stopMounts(mounts);
436
+ },
437
+ };
438
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Keep workspace `dist/` fresh — but only inside the framework monorepo.
3
+ *
4
+ * Inside the nwire repo, examples link to `@nwire/*` via the source
5
+ * packages' built `dist/`. Nothing rebuilds those on `nwire dev` / `nwire
6
+ * studio`, so a stale `dist` surfaces as runtime crashes like
7
+ * `captureSourceLocation is not a function`. Real consumer apps install
8
+ * prebuilt `@nwire/*` from npm and must never trigger a build — so this is
9
+ * a strict no-op outside the monorepo.
10
+ */
11
+ /**
12
+ * Walk up from `start` looking for the nwire monorepo root: a directory
13
+ * that has BOTH a `pnpm-workspace.yaml` AND the `@nwire/*` source packages
14
+ * (`packages/core-runtime/src`). The second check is what distinguishes
15
+ * the framework repo from an arbitrary consumer monorepo that merely
16
+ * happens to use pnpm workspaces — those install nwire prebuilt and must
17
+ * not be rebuilt.
18
+ *
19
+ * Returns the repo root, or `undefined` when `start` is not inside the
20
+ * framework monorepo.
21
+ */
22
+ export declare function findMonorepoRoot(start: string): string | undefined;
23
+ /**
24
+ * Ensure the workspace `@nwire/*` packages are built before a dev session.
25
+ * No-op outside the framework monorepo. Inside it, runs the turbo-backed
26
+ * `pnpm build` from the repo root — which is a ~200ms no-op when everything
27
+ * is fresh and only rebuilds what changed otherwise.
28
+ *
29
+ * A failing build is surfaced as a warning but never hard-blocks: a partial
30
+ * build still beats refusing to start, and the user sees the error inline.
31
+ */
32
+ export declare function ensureWorkspaceBuilt(cwd?: string): void;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Keep workspace `dist/` fresh — but only inside the framework monorepo.
3
+ *
4
+ * Inside the nwire repo, examples link to `@nwire/*` via the source
5
+ * packages' built `dist/`. Nothing rebuilds those on `nwire dev` / `nwire
6
+ * studio`, so a stale `dist` surfaces as runtime crashes like
7
+ * `captureSourceLocation is not a function`. Real consumer apps install
8
+ * prebuilt `@nwire/*` from npm and must never trigger a build — so this is
9
+ * a strict no-op outside the monorepo.
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { dirname, resolve } from "node:path";
13
+ import { palette } from "./colors.js";
14
+ import { execSync } from "./exec.js";
15
+ /**
16
+ * Walk up from `start` looking for the nwire monorepo root: a directory
17
+ * that has BOTH a `pnpm-workspace.yaml` AND the `@nwire/*` source packages
18
+ * (`packages/core-runtime/src`). The second check is what distinguishes
19
+ * the framework repo from an arbitrary consumer monorepo that merely
20
+ * happens to use pnpm workspaces — those install nwire prebuilt and must
21
+ * not be rebuilt.
22
+ *
23
+ * Returns the repo root, or `undefined` when `start` is not inside the
24
+ * framework monorepo.
25
+ */
26
+ export function findMonorepoRoot(start) {
27
+ let dir = resolve(start);
28
+ // Guard against symlink loops / odd filesystems with a generous bound.
29
+ for (let depth = 0; depth < 64; depth++) {
30
+ const hasWorkspace = existsSync(resolve(dir, "pnpm-workspace.yaml"));
31
+ const hasSource = existsSync(resolve(dir, "packages", "core-runtime", "src"));
32
+ if (hasWorkspace && hasSource)
33
+ return dir;
34
+ const parent = dirname(dir);
35
+ if (parent === dir)
36
+ break;
37
+ dir = parent;
38
+ }
39
+ return undefined;
40
+ }
41
+ /**
42
+ * Ensure the workspace `@nwire/*` packages are built before a dev session.
43
+ * No-op outside the framework monorepo. Inside it, runs the turbo-backed
44
+ * `pnpm build` from the repo root — which is a ~200ms no-op when everything
45
+ * is fresh and only rebuilds what changed otherwise.
46
+ *
47
+ * A failing build is surfaced as a warning but never hard-blocks: a partial
48
+ * build still beats refusing to start, and the user sees the error inline.
49
+ */
50
+ export function ensureWorkspaceBuilt(cwd = process.cwd()) {
51
+ const root = findMonorepoRoot(cwd);
52
+ if (!root)
53
+ return; // consumer app — installs prebuilt @nwire/*, never build.
54
+ // eslint-disable-next-line no-console
55
+ console.log(palette.dim(" ensuring workspace build…"));
56
+ const code = execSync("pnpm", ["build"], { cwd: root, quiet: true });
57
+ if (code !== 0) {
58
+ // eslint-disable-next-line no-console
59
+ console.warn(palette.warn(" workspace build reported errors — continuing anyway.") +
60
+ palette.dim(` (pnpm build exited ${code} in ${root})`));
61
+ }
62
+ }
@@ -4,9 +4,14 @@
4
4
  * every CLI command that reads the scan cache.
5
5
  *
6
6
  * Cheap path: compute fingerprint, compare to saved; if match, return.
7
- * Expensive path: spawn the cache-runner under vite-node to rebuild,
8
- * then save the new fingerprint. The expensive path runs at most once
9
- * per source change.
7
+ * Expensive path: rebuild the manifest IN-PROCESS via `@nwire/scan`'s
8
+ * `buildManifest` + `writeManifest` a pure static scan of the source
9
+ * tree plus the folded `.nwire/topology.json` a running app self-emits.
10
+ * No app boot, no loader, no subprocess.
11
+ *
12
+ * If `.nwire/topology.json` is absent (the app has never run), the
13
+ * manifest still writes — static-only; the topology layer fills in the
14
+ * next time the app runs and re-emits.
10
15
  */
11
16
  export interface EnsureScanOptions {
12
17
  /**
@@ -25,4 +30,4 @@ export interface EnsureScanResult {
25
30
  /** The fingerprint that's now saved on disk. */
26
31
  readonly fingerprint: string;
27
32
  }
28
- export declare function ensureScanFresh(cwd?: string, opts?: EnsureScanOptions): EnsureScanResult;
33
+ export declare function ensureScanFresh(cwd?: string, opts?: EnsureScanOptions): Promise<EnsureScanResult>;