@lensmcp/cluster 1.16.26 → 1.16.28

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.
@@ -1 +1 @@
1
- {"version":3,"file":"create-webpack-dev.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/create-webpack-dev.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAOtF,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,qBAAqB,CAAC,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA2CD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAoItF"}
1
+ {"version":3,"file":"create-webpack-dev.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/create-webpack-dev.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAQtF,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,qBAAqB,CAAC,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA2CD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAgKtF"}
@@ -7,6 +7,7 @@ const fs = tslib_1.__importStar(require("node:fs"));
7
7
  const path = tslib_1.__importStar(require("node:path"));
8
8
  const webpack_node_externals_1 = tslib_1.__importDefault(require("webpack-node-externals"));
9
9
  const build_scope_patterns_1 = require("./build-scope-patterns");
10
+ const tsgo_check_plugin_1 = require("./tsgo-check-plugin");
10
11
  // eslint-disable-next-line @typescript-eslint/no-require-imports
11
12
  const webpack = require('webpack');
12
13
  class DevServerReloadPlugin {
@@ -45,6 +46,17 @@ class DevServerReloadPlugin {
45
46
  function createDevWebpackConfig(options) {
46
47
  const { appRoot, outputDir, main, tsConfig, workspaceRoot, assets = [], port, memoryLimit = 8192, buildLibsFromSource = true, orgScopes = [], bundlePackages = [], additionalEntryPoints = [], nodeExternalsConfig: userNodeExternalsConfig, webpackConfigPath, } = options;
47
48
  const { allowlistPatterns, scopePrefixes, scopePatterns } = (0, build_scope_patterns_1.buildScopePatterns)(orgScopes);
49
+ // Type-checking mode (LENSMCP_TYPECHECK): default = the resident fork-ts-checker pair (async,
50
+ // feeds the lens `error TS` service-error signal); 'off' = none (fastest, quietest — the signal
51
+ // is traded away); 'tsgo' = zero-resident cold re-checks via @typescript/native-preview (same
52
+ // signal, no idle memory — see tsgo-check-plugin.ts). tsgo falls back to fork-ts-checker when
53
+ // the binary isn't installed in the workspace, so the flag can ship ahead of the devDep.
54
+ const typecheckMode = process.env.LENSMCP_TYPECHECK;
55
+ const tsgoBin = typecheckMode === 'tsgo' ? (0, tsgo_check_plugin_1.resolveTsgoBin)(workspaceRoot, process.env.LENSMCP_TSGO_BIN) : null;
56
+ if (typecheckMode === 'tsgo' && !tsgoBin) {
57
+ console.warn(`[serve] ${options.appName}: LENSMCP_TYPECHECK=tsgo but no tsgo binary (yarn add -D @typescript/native-preview) — falling back to fork-ts-checker`);
58
+ }
59
+ const useTsgo = !!tsgoBin;
48
60
  // Build combined allowlist: orgScopes + bundlePackages + user-provided
49
61
  const bundlePatterns = bundlePackages.map((pkg) => new RegExp(`^${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(/|$)`));
50
62
  const combinedAllowlist = [
@@ -126,12 +138,25 @@ function createDevWebpackConfig(options) {
126
138
  outputHashing: 'none',
127
139
  generatePackageJson: false,
128
140
  watchDependencies: true,
129
- typeCheckOptions: {
130
- async: true,
131
- },
141
+ // Type checking = the fork-ts-checker pair (2 worker processes, ~80MB per service) AND the
142
+ // source of the in-pod `error TS…` lines the gateway sniffs into the lens "service error"
143
+ // signal. Default ON. `LENSMCP_TYPECHECK=off` trades the signal for memory/CPU;
144
+ // `LENSMCP_TYPECHECK=tsgo` keeps the signal at zero resident cost (the TsgoCheckPlugin
145
+ // below replaces the resident pair with debounced cold native checks).
146
+ typeCheckOptions: process.env.LENSMCP_TYPECHECK === 'off' || useTsgo ? false : { async: true },
132
147
  buildLibsFromSource,
133
148
  }),
134
149
  new DevServerReloadPlugin(port, options.httpsReload === true),
150
+ ...(useTsgo
151
+ ? [
152
+ new tsgo_check_plugin_1.TsgoCheckPlugin({
153
+ appName: options.appName,
154
+ tsConfig,
155
+ workspaceRoot,
156
+ ...(process.env.LENSMCP_TSGO_BIN ? { binPath: process.env.LENSMCP_TSGO_BIN } : {}),
157
+ }),
158
+ ]
159
+ : []),
135
160
  ],
136
161
  snapshot: { managedPaths: [/^(.+?[\\/])?node_modules[\\/]/] },
137
162
  watch: true,
@@ -1 +1 @@
1
- {"version":3,"file":"gateway.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/gateway.impl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAElE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAA8B,eAAe,CAC3C,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAa/B"}
1
+ {"version":3,"file":"gateway.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/gateway.impl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAGlE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAA8B,eAAe,CAC3C,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA0B/B"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = gatewayExecutor;
4
4
  const gateway_lib_1 = require("./gateway.lib");
5
+ const server_1 = require("./runtime/server");
5
6
  /**
6
7
  * `gateway` executor — ONE https front door for the whole workspace,
7
8
  * assembled dynamically from every project's `cluster` declaration:
@@ -20,6 +21,19 @@ const gateway_lib_1 = require("./gateway.lib");
20
21
  * without signals; this wrapper only adds the executor contract.
21
22
  */
22
23
  async function gatewayExecutor(options, context) {
24
+ // LAST-RESORT netting for the long-running daemon (this executor runs for DAYS): a benign socket
25
+ // errno that escaped every layer's local handlers — e.g. a dependency emitting 'error' on a socket
26
+ // it forgot to guard — must cost the ONE connection, never the process. Scoped to exactly the
27
+ // read/write network errnos; anything else preserves crash semantics (print + exit 1, matching
28
+ // Node's default). Lives HERE, not in startGateway, so tests driving the lib keep real crashes loud.
29
+ process.on('uncaughtException', (err) => {
30
+ if ((err.syscall === 'read' || err.syscall === 'write') && server_1.BENIGN_SOCKET_ERRNOS.has(err.code ?? '')) {
31
+ console.warn(`[gateway] survived a stray socket ${err.syscall} error (${err.code}) — one connection dropped, the daemon lives`);
32
+ return;
33
+ }
34
+ console.error(err);
35
+ process.exit(1);
36
+ });
23
37
  let handle;
24
38
  try {
25
39
  handle = await (0, gateway_lib_1.startGateway)(options, context);
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,iBAAiB,CAAC;AA8C/D;;sGAEsG;AACtG,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,oBAAoB,CAAC,EACpG,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,gBAAgB,GACpB,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CAkRvF"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,iBAAiB,CAAC;AA8C/D;;sGAEsG;AACtG,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,oBAAoB,CAAC,EACpG,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,gBAAgB,GACpB,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CAwXvF"}
@@ -150,18 +150,87 @@ function createServiceLayer(rt, obs) {
150
150
  const serviceBus = (svc) => svc.wsKey === rt.wsKey ? undefined : path.join(svc.root, '.lensmcp', 'events.jsonl');
151
151
  const emitSvc = (svc, severity, title, fingerprint, raw) => emit(severity, title, fingerprint, { wsKey: svc.wsKey, ...raw }, undefined, serviceBus(svc));
152
152
  const scanNow = (pool) => { pool.scannedAt = 0; return (0, discovery_1.pickSock)(pool, rt.scanTtlMs) ? pool.socks.length : 0; };
153
+ /**
154
+ * FOREIGN-GENERATION REAPER (the respawn deadlock, foodguard 2026-07-26): nx records a
155
+ * running continuous task in its workspace-data DB, and a NEW `nx run <svc>:serve-hmr`
156
+ * invocation WAITS on that record ("Waiting for <task> in another nx process") — even with
157
+ * NX_DAEMON=false. When the record's holder is an ORPHANED runner (a replaced daemon's
158
+ * tree, a half-killed generation), every respawn parks forever and `ensureUp` hangs each
159
+ * request with no log. Before spawning, kill any nx runner for THIS target that this
160
+ * gateway does not own — the runner is the detached group leader, so `kill(-pgid)` reaps
161
+ * its whole devserver tree with it. One `ps` exec; a clean cold start is a no-op.
162
+ */
163
+ const reapForeignRunners = (svc, target) => {
164
+ let out = '';
165
+ try {
166
+ out = (0, node_child_process_1.execFileSync)('ps', ['ax', '-ww', '-o', 'pid=,pgid=,command='], { encoding: 'utf8', timeout: 5_000 });
167
+ }
168
+ catch {
169
+ return 0;
170
+ }
171
+ const ownPid = svc.proc?.pid;
172
+ let reaped = 0;
173
+ for (const line of out.split('\n')) {
174
+ const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
175
+ if (!m)
176
+ continue;
177
+ const pid = Number(m[1]);
178
+ const pgid = Number(m[2]);
179
+ const cmd = m[3];
180
+ if (!cmd.includes(`run ${target}`))
181
+ continue; // only THIS service's runners
182
+ // Only nx runners (direct nx.js entry or the yarn/npx shim) — never a stray grep/editor.
183
+ if (!/\bnx(\.js)?\b/.test(cmd))
184
+ continue;
185
+ // A direct-entry runner carries its workspace's nx path — scope to OUR workspace root so a
186
+ // same-named project in another workspace is never touched. (The yarn-shim fallback has no
187
+ // path in argv; the target match alone bounds it.)
188
+ if (cmd.includes('/node_modules/nx/') && !cmd.includes(svc.root))
189
+ continue;
190
+ if (pid === process.pid || pid === ownPid)
191
+ continue;
192
+ if (ownPid !== undefined && pgid === ownPid)
193
+ continue; // our own detached tree
194
+ try {
195
+ process.kill(-pgid, 'SIGKILL');
196
+ }
197
+ catch {
198
+ try {
199
+ process.kill(pid, 'SIGKILL');
200
+ }
201
+ catch { /* already gone */ }
202
+ }
203
+ reaped += 1;
204
+ }
205
+ if (reaped > 0) {
206
+ console.log(`[gateway] ${target}: reaped ${reaped} stale runner tree(s) — a foreign generation held the nx task lock`);
207
+ emitSvc(svc, 'warning', `stale-runner reap: ${svc.project} (${reaped} foreign tree(s))`, `cluster-service:${svc.project}`, { kind: 'stale-runner-reap', project: svc.project, reaped });
208
+ }
209
+ return reaped;
210
+ };
153
211
  const spawnService = (svc) => {
154
212
  if (rt.stopped())
155
213
  return; // never spawn a process after the gateway is told to stop
156
214
  const target = `${svc.project}:${svc.decl.serveTarget ?? 'serve-hmr'}`;
157
215
  const min = Math.max(1, svc.decl.minPods ?? 1);
216
+ reapForeignRunners(svc, target); // clear nx's running-tasks lock BEFORE the spawn can park on it
217
+ svc.lockBlockedAt = undefined;
158
218
  console.log(`[gateway] starting ${target} (pods=${min})…`);
159
219
  emitSvc(svc, 'info', `service starting: ${svc.project} (${min} pod${min > 1 ? 's' : ''})`, `cluster-service:${svc.project}`, { kind: 'service-starting', project: svc.project, pods: min });
160
220
  svc.state = 'starting';
161
221
  svc.spawnAt = Date.now();
162
222
  // childCount goes as a CLI option, NOT env: nx executes tasks via its
163
223
  // daemon, whose environment is not the gateway's — env vars get lost.
164
- const proc = (0, node_child_process_1.spawn)('yarn', ['nx', 'run', target, `--childCount=${min}`], {
224
+ // Invoke the workspace's nx entry DIRECTLY (node + nx/dist/bin/nx.js) instead of through the
225
+ // `yarn nx` shim: the shim is a ~40MB node process that does nothing after the handoff yet
226
+ // lives for the service's whole life — ~600MB across a 15-service cluster. Resolved per
227
+ // svc.root so each hosted workspace runs its own nx; falls back to `yarn nx` when the entry
228
+ // isn't where node_modules linkers put it (pnp/odd layouts).
229
+ const nxEntry = path.join(svc.root, 'node_modules', 'nx', 'dist', 'bin', 'nx.js');
230
+ const [spawnCmd, spawnArgs] = fs.existsSync(nxEntry)
231
+ ? [process.execPath, [nxEntry, 'run', target, `--childCount=${min}`]]
232
+ : ['yarn', ['nx', 'run', target, `--childCount=${min}`]];
233
+ const proc = (0, node_child_process_1.spawn)(spawnCmd, spawnArgs, {
165
234
  // The service's OWN workspace root — `rt.root` for the daemon's own workspace, the registered
166
235
  // workspace's root for a second one (so ONE daemon hosts several workspaces' devservers — model (A)).
167
236
  cwd: svc.root,
@@ -181,6 +250,15 @@ function createServiceLayer(rt, obs) {
181
250
  env: {
182
251
  ...process.env,
183
252
  NX_DAEMON: 'false',
253
+ // NX_ISOLATE_PLUGINS=false: with the daemon off (above), EVERY serve client builds the
254
+ // project graph itself — and with plugin isolation on (nx's default) each client spawns
255
+ // 8 plugin-worker processes (~50MB each) that stay ALIVE for the life of the service:
256
+ // ~400MB of nx machinery per ~50MB service pod, ~4-5GB across a 13-service cluster
257
+ // (measured tetros 2026-07-25). Isolation-off loads plugins in-process: the graph is
258
+ // identical and zero workers spawn (nx itself runs isolation-off in sandboxes/WASM —
259
+ // a supported mode). NOTE this is authoritative for spawned clients: nx's dotenv load
260
+ // never overrides an already-set env var, so a workspace .env cannot flip it back on.
261
+ NX_ISOLATE_PLUGINS: 'false',
184
262
  ...(svc.wsKey ? { LENSMCP_WS_KEY: svc.wsKey } : {}),
185
263
  LENSMCP_EVENT_FILE: svc.wsKey === rt.wsKey ? rt.eventFile : path.join(svc.root, '.lensmcp', 'events.jsonl'),
186
264
  ...eastWestEnv(rt, svc.project),
@@ -211,6 +289,25 @@ function createServiceLayer(rt, obs) {
211
289
  }
212
290
  if (/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(line))
213
291
  svc.buildFailed = false;
292
+ // LOCK-BREAK: nx prints exactly this when a FOREIGN runner holds the running-tasks
293
+ // record for our target — this spawn will park forever and every request would hang
294
+ // through ensureUp with no further log. Cure it once per spawn: kill our parked
295
+ // waiter, reap the foreign tree, and respawn clean.
296
+ if (!svc.lockBlockedAt && /Waiting for .+ in another nx process/.test(line)) {
297
+ svc.lockBlockedAt = Date.now();
298
+ console.log(`[gateway] ${target}: spawn blocked by a stale nx runner — breaking the lock + respawning`);
299
+ emitSvc(svc, 'warning', `nx task lock: ${svc.project} blocked by a stale runner — auto-breaking`, `cluster-service:${svc.project}`, { kind: 'nx-lock-break', project: svc.project });
300
+ setTimeout(() => {
301
+ if (rt.stopped() || !svc.lockBlockedAt)
302
+ return;
303
+ killSpawned(svc, 'parked on a foreign nx task lock — recycling');
304
+ reapForeignRunners(svc, target);
305
+ setTimeout(() => {
306
+ if (!rt.stopped() && !svc.proc && svc.state === 'down')
307
+ spawnService(svc);
308
+ }, 1_500).unref?.();
309
+ }, 250).unref?.();
310
+ }
214
311
  }
215
312
  };
216
313
  proc.stdout?.on('data', (d) => { if (rt.stopped())
@@ -327,16 +424,39 @@ function createServiceLayer(rt, obs) {
327
424
  }
328
425
  svc.proc = undefined;
329
426
  svc.state = 'down';
330
- // children die with the tree but their sock FILES survive — remove them
331
- // so the canvas and the proxy never see ghost pods.
427
+ // children die with the tree but their sock FILES survive — remove them so the canvas
428
+ // and the proxy never see ghost pods. OWNER-AWARE: a sock whose `.owner` pidfile names a
429
+ // LIVE process belongs to a NEWER generation binding concurrently — leave it (the
430
+ // devserver's bind-steal owns the path now; unlinking it here recreates the vanished-sock
431
+ // wedge this sweep exists to prevent).
332
432
  setTimeout(() => {
333
433
  try {
334
434
  for (const f of fs.readdirSync(svc.pool.dir)) {
335
- if (f.endsWith('.sock'))
336
- fs.unlinkSync(path.join(svc.pool.dir, f));
435
+ if (!f.endsWith('.sock'))
436
+ continue;
437
+ const sockPath = path.join(svc.pool.dir, f);
438
+ try {
439
+ const owner = Number(fs.readFileSync(`${sockPath}.owner`, 'utf8').trim());
440
+ if (Number.isFinite(owner) && owner > 0) {
441
+ try {
442
+ process.kill(owner, 0); // throws when the owner is dead
443
+ continue; // a LIVE foreign owner — a newer generation's pod; keep it
444
+ }
445
+ catch { /* dead owner → sweep */ }
446
+ }
447
+ }
448
+ catch { /* no owner file (legacy) → sweep */ }
449
+ try {
450
+ fs.unlinkSync(sockPath);
451
+ }
452
+ catch { /* gone */ }
453
+ try {
454
+ fs.unlinkSync(`${sockPath}.owner`);
455
+ }
456
+ catch { /* gone */ }
337
457
  }
338
458
  }
339
- catch { /* gone */ }
459
+ catch { /* dir gone */ }
340
460
  svc.pool.socks = [];
341
461
  svc.pool.scannedAt = 0;
342
462
  }, 1200).unref?.();
@@ -12,5 +12,8 @@ import type { GatewayContext, GatewayHandle, GatewayRuntimeOptions, Route } from
12
12
  * unless the insecure opt-in is set).
13
13
  */
14
14
  export declare function buildEdgeVerifierPool(getRoutes: () => Route[]): EdgeVerifierPool;
15
+ /** Client-abort errnos that are NORMAL front-door traffic (a closed tab, a sleeping laptop, a canceled
16
+ * HMR socket) — dropped quietly. Anything else on an inbound socket is logged before the drop. */
17
+ export declare const BENIGN_SOCKET_ERRNOS: ReadonlySet<string>;
15
18
  export declare function startGateway(options: GatewayRuntimeOptions, context: GatewayContext): Promise<GatewayHandle>;
16
19
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAK/D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AA0BxH;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,KAAK,EAAE,GAAG,gBAAgB,CA4ChF;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAmRxB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAK/D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AA0BxH;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,KAAK,EAAE,GAAG,gBAAgB,CA4ChF;AAED;mGACmG;AACnG,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAA8E,CAAC;AAkBpI,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CA8RxB"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BENIGN_SOCKET_ERRNOS = void 0;
3
4
  exports.buildEdgeVerifierPool = buildEdgeVerifierPool;
4
5
  exports.startGateway = startGateway;
5
6
  const tslib_1 = require("tslib");
@@ -112,6 +113,24 @@ function buildEdgeVerifierPool(getRoutes) {
112
113
  },
113
114
  };
114
115
  }
116
+ /** Client-abort errnos that are NORMAL front-door traffic (a closed tab, a sleeping laptop, a canceled
117
+ * HMR socket) — dropped quietly. Anything else on an inbound socket is logged before the drop. */
118
+ exports.BENIGN_SOCKET_ERRNOS = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'ECONNABORTED', 'ECANCELED']);
119
+ /** Attach the inbound-socket error guard. EVERY socket the front door accepts gets an 'error' listener
120
+ * the moment it exists — both the raw TCP socket ('connection') and the post-handshake TLSSocket
121
+ * ('secureConnection', a DIFFERENT emitter). A client abort is routine, but with NO listener Node turns
122
+ * it into an UNHANDLED 'error' event that kills the whole daemon (observed: a days-old gateway died on
123
+ * `read ECONNRESET` at TLSWrap.onStreamRead — the exposed window is a socket the http layer has handed
124
+ * off, e.g. right after an 'upgrade', where no internal handler is attached any more). Handling =
125
+ * destroy the ONE socket; never the process. */
126
+ function guardInboundSocket(socket) {
127
+ socket.on('error', (err) => {
128
+ if (!exports.BENIGN_SOCKET_ERRNOS.has(err.code ?? '')) {
129
+ console.warn(`[gateway] inbound socket error (${err.code ?? err.message}) — dropping this connection only`);
130
+ }
131
+ socket.destroy();
132
+ });
133
+ }
115
134
  async function startGateway(options, context) {
116
135
  // NB: this local `https` (TLS-on flag) deliberately SHADOWS the `node:https`
117
136
  // module — which is why TLS/agents are reached via `require('node:https')`.
@@ -303,6 +322,18 @@ async function startGateway(options, context) {
303
322
  : http.createServer(handler);
304
323
  server.on('upgrade', upgrade);
305
324
  server.on('error', (err) => console.error(`[gateway] port ${port}: ${err.code}`));
325
+ // Inbound-socket resilience (the ECONNRESET-crash fix): guard every accepted socket, and end
326
+ // handshake-phase ('tlsClientError') / H1 parse-phase ('clientError') faults on the ONE connection.
327
+ // Registering 'clientError' replaces Node's default 400-writer, so keep its semantics: a best-effort
328
+ // 400 on a still-writable socket, then destroy.
329
+ server.on('connection', guardInboundSocket);
330
+ server.on('secureConnection', guardInboundSocket); // never fires on a plain-http server — harmless
331
+ server.on('tlsClientError', (_err, tlsSocket) => tlsSocket.destroy());
332
+ server.on('clientError', (err, socket) => {
333
+ if (err.code !== 'ECONNRESET' && socket.writable)
334
+ socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
335
+ socket.destroy();
336
+ });
306
337
  // h2 SESSION resilience: without a `sessionError` handler ANY session-level
307
338
  // fault (a client GOAWAY mid-burst, a flood-protection trip, a decode error)
308
339
  // is unhandled → Node tears the session down, failing EVERY sibling stream at
@@ -147,6 +147,11 @@ export interface ServiceCtl {
147
147
  * stops a rapid source-churn (e.g. a codegen run) from thrash-recycling the same pod. NOT counted against
148
148
  * the crash-loop cap ({@link restarts}) — staleness is not a crash. */
149
149
  lastStaleRecycleAt?: number;
150
+ /** Self-heal: set when THIS spawn printed nx's "Waiting for <task> in another nx process" — a FOREIGN
151
+ * (orphaned) runner holds nx's running-tasks lock and the spawn will never produce a pod. The sniffer
152
+ * fires the lock-break once per spawn (kill our waiter → reap the foreign tree → respawn); cleared on
153
+ * the next spawn. */
154
+ lockBlockedAt?: number;
150
155
  }
151
156
  /** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
152
157
  export declare const SERVICE_ERROR_THROTTLE_MS = 5000;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/upgrade.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,cAAc,CAAC;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAEvG,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,CAqChE"}
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/upgrade.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,cAAc,CAAC;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAEvG,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,CA6ChE"}
@@ -59,5 +59,13 @@ function createUpgrade(deps) {
59
59
  }
60
60
  proxy.proxy.ws(req, socket, head, { target, changeOrigin: !route.pool, agent: proxy.agentForTarget(target) }, () => socket.destroy());
61
61
  };
62
- return (req, socket, head) => { void run(req, socket, head); };
62
+ return (req, socket, head) => {
63
+ // Guard the handed-off socket IMMEDIATELY: on 'upgrade' the http layer removes its OWN listeners,
64
+ // so until proxy.ws attaches handlers (an `await` away in `run`) a client abort (ECONNRESET — an
65
+ // HMR socket torn mid-upgrade) would be an unhandled 'error' event → the whole daemon dies. And a
66
+ // rejection out of `run` (a throwing auth/hook) must likewise cost the ONE socket, never the
67
+ // process — a floating promise rejection is fatal by default.
68
+ socket.on('error', () => socket.destroy());
69
+ run(req, socket, head).catch(() => socket.destroy());
70
+ };
63
71
  }
package/main.devserver.js CHANGED
@@ -124,13 +124,57 @@ const SOCK_DIR = LENSMCP_WS_KEY
124
124
  function childSockPath(id) {
125
125
  return path.join(SOCK_DIR, `child-${id}.sock`);
126
126
  }
127
- function cleanupSock(sockPath) {
127
+ /**
128
+ * SOCKET OWNERSHIP (the generation race, foodguard 2026-07-26): when devserver generations
129
+ * overlap — a killed nx runner's orphaned tree dying while the daemon's fresh spawn binds —
130
+ * the DYING generation's cleanup used to unlink `child-1.sock` that the NEW generation had
131
+ * just bound. The file vanished from disk while the live child held the (now anonymous)
132
+ * inode: every gateway connect hit ENOENT/ghost, with no log and no evictable pod.
133
+ *
134
+ * The rule that closes it: **BIND steals, DEATH is guarded.** A binder writes a sidecar
135
+ * `<sock>.owner` pidfile FIRST (so any concurrently-dying process already sees the new
136
+ * owner), then unlinks the stale path and binds. Every death/cleanup path unlinks ONLY if
137
+ * the owner file still names the pid it is cleaning up for — a foreign live owner means a
138
+ * newer generation took the path, and the dying one must leave it alone.
139
+ */
140
+ function sockOwnerPath(sockPath) { return `${sockPath}.owner`; }
141
+ /** Bind-time steal: claim ownership (pidfile first), then clear the stale path. */
142
+ function claimSock(sockPath, ownerPid = process.pid) {
143
+ try {
144
+ fs.mkdirSync(path.dirname(sockPath), { recursive: true });
145
+ }
146
+ catch { /* exists */ }
147
+ try {
148
+ fs.writeFileSync(sockOwnerPath(sockPath), String(ownerPid));
149
+ }
150
+ catch { /* best-effort */ }
151
+ try {
152
+ fs.unlinkSync(sockPath);
153
+ }
154
+ catch { /* no stale socket */ }
155
+ }
156
+ /**
157
+ * Death-path cleanup: unlink the socket ONLY when `expectedOwnerPid` (default: this
158
+ * process) still owns it. A missing/unreadable owner file falls back to unlinking
159
+ * (legacy sockets from a pre-ownership build).
160
+ */
161
+ function cleanupSock(sockPath, expectedOwnerPid = process.pid) {
128
162
  if (!sockPath)
129
163
  return;
164
+ try {
165
+ const owner = fs.readFileSync(sockOwnerPath(sockPath), 'utf8').trim();
166
+ if (owner && owner !== String(expectedOwnerPid))
167
+ return; // a newer generation owns the path
168
+ }
169
+ catch { /* no owner file → legacy behavior */ }
130
170
  try {
131
171
  fs.unlinkSync(sockPath);
132
172
  }
133
173
  catch { /* best-effort: socket already gone */ }
174
+ try {
175
+ fs.unlinkSync(sockOwnerPath(sockPath));
176
+ }
177
+ catch { /* best-effort */ }
134
178
  }
135
179
  // delete from CJS cache + require fresh
136
180
  async function importFresh(spec) {
@@ -270,10 +314,7 @@ if (process.env.APP_RUNNER === '1') {
270
314
  // SIGTERM that missed us), the IPC channel closes — exit immediately and
271
315
  // remove our socket so no ghost pod (live process + stale sock) survives.
272
316
  process.on('disconnect', () => {
273
- try {
274
- fs.unlinkSync(sockPath);
275
- }
276
- catch { /* already gone */ }
317
+ cleanupSock(sockPath); // owner-guarded: never unlink a newer generation's socket
277
318
  process.exit(0);
278
319
  });
279
320
  // boot once, then listen on Unix socket
@@ -281,7 +322,7 @@ if (process.env.APP_RUNNER === '1') {
281
322
  await swapNow();
282
323
  const resolvedBundle = path.isAbsolute(BUNDLE_PATH) ? BUNDLE_PATH : path.join(__dirname, BUNDLE_PATH);
283
324
  console.log(`[child] Using bundle: ${resolvedBundle}`);
284
- cleanupSock(sockPath); // remove stale socket from previous run
325
+ claimSock(sockPath); // bind steals: claim ownership, then clear the stale path
285
326
  server.listen(sockPath, () => {
286
327
  console.log(`[child] Listening on ${sockPath}`);
287
328
  if (process.send)
@@ -570,7 +611,7 @@ else {
570
611
  info.lastError = `Child exited abnormally (code=${code}, signal=${signal ?? 'none'})`;
571
612
  }
572
613
  info.healthy = false;
573
- cleanupSock(info.socketPath);
614
+ cleanupSock(info.socketPath, info.proc.pid); // guarded by the DEAD child's claim
574
615
  info.socketPath = undefined;
575
616
  // Supervise the pool: a CRASHED pod (not a clean shutdown, not a
576
617
  // reload/idle-kill) must be replaced or the pool silently shrinks to
@@ -619,7 +660,7 @@ else {
619
660
  }
620
661
  catch { /* ignore */
621
662
  }
622
- cleanupSock(c.socketPath);
663
+ cleanupSock(c.socketPath, c.proc.pid); // guarded by the replaced child's claim
623
664
  c.socketPath = undefined;
624
665
  // Remove from array
625
666
  const idx = children.indexOf(c);
@@ -1008,7 +1049,7 @@ else {
1008
1049
  // derivation the gateway uses for the pod pool dir. Plain HTTP: it's a
1009
1050
  // per-user unix socket, TLS adds nothing.
1010
1051
  const controlSockPath = path.join(SOCK_DIR, 'parent.sock');
1011
- cleanupSock(controlSockPath); // stale socket from a crashed previous parent
1052
+ claimSock(controlSockPath); // bind steals: claim + clear a crashed previous parent's leftover
1012
1053
  const controlServer = http.createServer(requestHandler);
1013
1054
  controlServer.on('error', (err) => {
1014
1055
  console.warn(`[parent] control socket unavailable (${err.code}) — lensmcp logs/debug attach disabled.`);
@@ -1039,7 +1080,7 @@ else {
1039
1080
  catch { /* ignore */
1040
1081
  }
1041
1082
  if (c.socketPath)
1042
- cleanupSock(c.socketPath);
1083
+ cleanupSock(c.socketPath, c.proc.pid);
1043
1084
  }
1044
1085
  // Clean up socket directory
1045
1086
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.16.26",
3
+ "version": "1.16.28",
4
4
  "description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -65,8 +65,8 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@lensmcp/node-instrumentation": "1.16.26",
69
- "@lensmcp/nx-plugin": "1.16.26",
68
+ "@lensmcp/node-instrumentation": "1.16.28",
69
+ "@lensmcp/nx-plugin": "1.16.28",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",
@@ -0,0 +1,61 @@
1
+ /**
2
+ * TsgoCheckPlugin — dev type checking with ZERO resident memory (`LENSMCP_TYPECHECK=tsgo`).
3
+ *
4
+ * The default fork-ts-checker keeps an incremental tsc PROGRAM resident between saves — a
5
+ * 2-worker pair (~80MB+) per service, ~1GB across a 13-service cluster, alive even when nobody
6
+ * saves. tsgo (@typescript/native-preview, the native TS compiler) full-checks one of these
7
+ * services cold in ~1-3s, so instead of holding a program we re-check COLD after each successful
8
+ * rebuild (debounced, killing any in-flight check): idle cost is exactly zero.
9
+ *
10
+ * The output CONTRACT is the point: `--pretty false` prints classic `file.ts(1,2): error TS1234:`
11
+ * lines to the executor's console, which the gateway sniffs (`error TS\d+`) into the lens
12
+ * "service error" signal — the same wire the fork-ts-checker fed. Advisory like
13
+ * `typeCheckOptions.async: true`: a type error never fails or blocks the webpack build.
14
+ *
15
+ * SHIM tsconfig: tsgo is TS7 — `moduleResolution: node10` (what node-app tsconfigs often pin) is
16
+ * REMOVED there, so the check runs against a generated shim extending the app tsconfig with
17
+ * `{ module: 'preserve', moduleResolution: 'bundler', isolatedModules: false }`. `extends` is an
18
+ * ABSOLUTE path and tsconfig path-options resolve relative to the file they ORIGINATE in, so the
19
+ * shim lives in the workspace's git-ignored `tmp/` (in-tree, so @types resolution still walks up to
20
+ * the workspace node_modules — see writeShimTsconfig). `isolatedModules: false` because
21
+ * `module: preserve` + `emitDecoratorMetadata` otherwise flags TS1272 on decorated signatures —
22
+ * an emit-safety rule that doesn't apply here (this checker never emits; webpack owns emit).
23
+ * Parity was verified per adopting workspace (0-error parity with tsc on all spot-checked apps);
24
+ * tsgo is a PREVIEW compiler, which is why this mode is opt-in and falls back to fork-ts-checker
25
+ * when the binary is absent.
26
+ */
27
+ import { spawn } from 'node:child_process';
28
+ export interface TsgoCheckPluginOptions {
29
+ appName: string;
30
+ /** Absolute path to the app tsconfig the check covers. */
31
+ tsConfig: string;
32
+ workspaceRoot: string;
33
+ debounceMs?: number;
34
+ /** Binary override (LENSMCP_TSGO_BIN); default: <workspaceRoot>/node_modules/.bin/tsgo. */
35
+ binPath?: string;
36
+ /** Injectable for tests. */
37
+ spawnImpl?: typeof spawn;
38
+ }
39
+ /** The workspace's tsgo binary, or null when not installed (caller falls back to fork-ts-checker). */
40
+ export declare function resolveTsgoBin(workspaceRoot: string, override?: string): string | null;
41
+ /**
42
+ * Write the shim tsconfig (absolute `extends`; see the header) and return its path. It lives under
43
+ * the WORKSPACE's git-ignored `tmp/` — NOT os.tmpdir(): `types: ['node']` (and every other @types
44
+ * lookup) resolves typeRoots by walking node_modules UP FROM THE CONFIG FILE's directory, so a shim
45
+ * outside the tree fails every service with `TS2688: Cannot find type definition file for 'node'`.
46
+ */
47
+ export declare function writeShimTsconfig(appName: string, tsConfig: string, workspaceRoot: string): string;
48
+ export declare class TsgoCheckPlugin {
49
+ private readonly opts;
50
+ private readonly spawnImpl;
51
+ private timer;
52
+ private inflight;
53
+ private shimPath;
54
+ private disabled;
55
+ constructor(opts: TsgoCheckPluginOptions);
56
+ apply(compiler: any): void;
57
+ /** Debounced: rapid successive rebuilds collapse into one check; an in-flight check is superseded. */
58
+ schedule(): void;
59
+ private run;
60
+ }
61
+ //# sourceMappingURL=tsgo-check-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tsgo-check-plugin.d.ts","sourceRoot":"","sources":["../../../libs/cluster/src/tsgo-check-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAI9D,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,sGAAsG;AACtG,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGtF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CA0BlG;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,QAAQ,CAAS;gBAEb,IAAI,EAAE,sBAAsB;IAMxC,KAAK,CAAC,QAAQ,EAAE,GAAG,GAAG,IAAI;IAO1B,sGAAsG;IACtG,QAAQ,IAAI,IAAI;IAOhB,OAAO,CAAC,GAAG;CA0CZ"}
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TsgoCheckPlugin = void 0;
4
+ exports.resolveTsgoBin = resolveTsgoBin;
5
+ exports.writeShimTsconfig = writeShimTsconfig;
6
+ const tslib_1 = require("tslib");
7
+ /**
8
+ * TsgoCheckPlugin — dev type checking with ZERO resident memory (`LENSMCP_TYPECHECK=tsgo`).
9
+ *
10
+ * The default fork-ts-checker keeps an incremental tsc PROGRAM resident between saves — a
11
+ * 2-worker pair (~80MB+) per service, ~1GB across a 13-service cluster, alive even when nobody
12
+ * saves. tsgo (@typescript/native-preview, the native TS compiler) full-checks one of these
13
+ * services cold in ~1-3s, so instead of holding a program we re-check COLD after each successful
14
+ * rebuild (debounced, killing any in-flight check): idle cost is exactly zero.
15
+ *
16
+ * The output CONTRACT is the point: `--pretty false` prints classic `file.ts(1,2): error TS1234:`
17
+ * lines to the executor's console, which the gateway sniffs (`error TS\d+`) into the lens
18
+ * "service error" signal — the same wire the fork-ts-checker fed. Advisory like
19
+ * `typeCheckOptions.async: true`: a type error never fails or blocks the webpack build.
20
+ *
21
+ * SHIM tsconfig: tsgo is TS7 — `moduleResolution: node10` (what node-app tsconfigs often pin) is
22
+ * REMOVED there, so the check runs against a generated shim extending the app tsconfig with
23
+ * `{ module: 'preserve', moduleResolution: 'bundler', isolatedModules: false }`. `extends` is an
24
+ * ABSOLUTE path and tsconfig path-options resolve relative to the file they ORIGINATE in, so the
25
+ * shim lives in the workspace's git-ignored `tmp/` (in-tree, so @types resolution still walks up to
26
+ * the workspace node_modules — see writeShimTsconfig). `isolatedModules: false` because
27
+ * `module: preserve` + `emitDecoratorMetadata` otherwise flags TS1272 on decorated signatures —
28
+ * an emit-safety rule that doesn't apply here (this checker never emits; webpack owns emit).
29
+ * Parity was verified per adopting workspace (0-error parity with tsc on all spot-checked apps);
30
+ * tsgo is a PREVIEW compiler, which is why this mode is opt-in and falls back to fork-ts-checker
31
+ * when the binary is absent.
32
+ */
33
+ const node_child_process_1 = require("node:child_process");
34
+ const fs = tslib_1.__importStar(require("node:fs"));
35
+ const path = tslib_1.__importStar(require("node:path"));
36
+ /** The workspace's tsgo binary, or null when not installed (caller falls back to fork-ts-checker). */
37
+ function resolveTsgoBin(workspaceRoot, override) {
38
+ const candidate = override || path.join(workspaceRoot, 'node_modules', '.bin', 'tsgo');
39
+ return fs.existsSync(candidate) ? candidate : null;
40
+ }
41
+ /**
42
+ * Write the shim tsconfig (absolute `extends`; see the header) and return its path. It lives under
43
+ * the WORKSPACE's git-ignored `tmp/` — NOT os.tmpdir(): `types: ['node']` (and every other @types
44
+ * lookup) resolves typeRoots by walking node_modules UP FROM THE CONFIG FILE's directory, so a shim
45
+ * outside the tree fails every service with `TS2688: Cannot find type definition file for 'node'`.
46
+ */
47
+ function writeShimTsconfig(appName, tsConfig, workspaceRoot) {
48
+ const dir = path.join(workspaceRoot, 'tmp', 'lensmcp-tsgo');
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ const file = path.join(dir, `${appName}.tsconfig.json`);
51
+ fs.writeFileSync(file, JSON.stringify({
52
+ extends: tsConfig,
53
+ // baseUrl: null UNSETS an inherited baseUrl — TS7 removed the option outright (TS5102), and
54
+ // a workspace that anchors its inherited `paths` with baseUrl still resolves them fine
55
+ // without it: TS7 anchors non-relative `paths` at the config file that DECLARES them (the
56
+ // workspace-root base tsconfig — the same directory baseUrl pointed at). A no-op where
57
+ // baseUrl was never set.
58
+ compilerOptions: {
59
+ module: 'preserve',
60
+ moduleResolution: 'bundler',
61
+ isolatedModules: false,
62
+ baseUrl: null,
63
+ },
64
+ }, null, 2));
65
+ return file;
66
+ }
67
+ class TsgoCheckPlugin {
68
+ constructor(opts) {
69
+ this.timer = null;
70
+ this.inflight = null;
71
+ this.shimPath = null;
72
+ this.disabled = false;
73
+ this.opts = opts;
74
+ this.spawnImpl = opts.spawnImpl ?? node_child_process_1.spawn;
75
+ }
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- webpack Compiler (untyped here like DevServerReloadPlugin)
77
+ apply(compiler) {
78
+ compiler.hooks.done.tap('TsgoCheckPlugin', (stats) => {
79
+ if (stats.hasErrors())
80
+ return; // webpack already printed build errors — don't pile on
81
+ this.schedule();
82
+ });
83
+ }
84
+ /** Debounced: rapid successive rebuilds collapse into one check; an in-flight check is superseded. */
85
+ schedule() {
86
+ if (this.disabled)
87
+ return;
88
+ if (this.timer)
89
+ clearTimeout(this.timer);
90
+ this.timer = setTimeout(() => this.run(), this.opts.debounceMs ?? 500);
91
+ this.timer.unref?.();
92
+ }
93
+ run() {
94
+ const bin = resolveTsgoBin(this.opts.workspaceRoot, this.opts.binPath);
95
+ if (!bin) {
96
+ // The binary vanished after config time (e.g. a reinstall mid-serve) — warn once, go quiet.
97
+ this.disabled = true;
98
+ console.warn(`[tsgo] ${this.opts.appName}: tsgo binary not found — type checking disabled for this session`);
99
+ return;
100
+ }
101
+ if (this.inflight) {
102
+ this.inflight.kill('SIGTERM'); // superseded by a newer build
103
+ this.inflight = null;
104
+ }
105
+ if (!this.shimPath)
106
+ this.shimPath = writeShimTsconfig(this.opts.appName, this.opts.tsConfig, this.opts.workspaceRoot);
107
+ const startedAt = Date.now();
108
+ const child = this.spawnImpl(bin, ['--noEmit', '--pretty', 'false', '-p', this.shimPath], {
109
+ cwd: this.opts.workspaceRoot,
110
+ stdio: ['ignore', 'pipe', 'pipe'],
111
+ });
112
+ this.inflight = child;
113
+ let out = '';
114
+ child.stdout?.on('data', (d) => { out += d.toString(); });
115
+ child.stderr?.on('data', (d) => { out += d.toString(); });
116
+ child.on('error', () => {
117
+ if (this.inflight === child)
118
+ this.inflight = null;
119
+ console.warn(`[tsgo] ${this.opts.appName}: failed to spawn ${bin}`);
120
+ });
121
+ child.on('close', (code, signal) => {
122
+ if (this.inflight === child)
123
+ this.inflight = null;
124
+ if (signal)
125
+ return; // superseded — a newer check owns the verdict
126
+ const secs = ((Date.now() - startedAt) / 1000).toFixed(1);
127
+ if (code === 0) {
128
+ console.log(`[tsgo] ${this.opts.appName}: types clean (${secs}s)`);
129
+ return;
130
+ }
131
+ // Print the raw `file.ts(1,2): error TSxxxx:` lines — the gateway's `error TS\d+` sniff
132
+ // turns these into the lens service-error signal, exactly like fork-ts-checker output did.
133
+ if (out.trim())
134
+ process.stdout.write(out.endsWith('\n') ? out : `${out}\n`);
135
+ const count = (out.match(/error TS\d+/g) ?? []).length;
136
+ console.log(`[tsgo] ${this.opts.appName}: ${count} type error${count === 1 ? '' : 's'} (${secs}s)`);
137
+ });
138
+ }
139
+ }
140
+ exports.TsgoCheckPlugin = TsgoCheckPlugin;