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