@lensmcp/cluster 1.18.4 → 1.18.6
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/basic-ssl.js +1 -241
- package/build-scope-patterns.js +1 -40
- package/create-webpack-dev.js +1 -186
- package/create-webpack-prod.js +1 -169
- package/executors/build/build.impl.js +1 -98
- package/executors/gateway/gateway-errors.js +1 -43
- package/executors/gateway/gateway.impl.js +1 -53
- package/executors/gateway/gateway.lib.js +1 -29
- package/executors/gateway/health-check.js +1 -66
- package/executors/gateway/jwks-verify.js +1 -121
- package/executors/gateway/main.prod-gateway.js +2 -573
- package/executors/gateway/main.rollout.js +11 -117
- package/executors/gateway/manifest.js +1 -374
- package/executors/gateway/metrics.js +1 -56
- package/executors/gateway/otel-tracing.js +1 -74
- package/executors/gateway/prod-gateway.lib.js +1 -22
- package/executors/gateway/prod-runtime/access-log.js +1 -24
- package/executors/gateway/prod-runtime/app.js +1 -123
- package/executors/gateway/prod-runtime/auth.js +1 -51
- package/executors/gateway/prod-runtime/cors.js +1 -40
- package/executors/gateway/prod-runtime/edge.js +1 -65
- package/executors/gateway/prod-runtime/handler.js +1 -226
- package/executors/gateway/prod-runtime/hooks.js +1 -42
- package/executors/gateway/prod-runtime/observability.js +1 -125
- package/executors/gateway/prod-runtime/rollout.js +1 -103
- package/executors/gateway/prod-runtime/routing.js +1 -40
- package/executors/gateway/prod-runtime/server.js +1 -79
- package/executors/gateway/prod-runtime/trust.js +1 -32
- package/executors/gateway/prod-runtime/types.js +1 -2
- package/executors/gateway/prod-runtime/upgrade.js +4 -116
- package/executors/gateway/prod-runtime/upstream.js +1 -21
- package/executors/gateway/providers-prod.js +1 -232
- package/executors/gateway/rate-limit.js +2 -75
- package/executors/gateway/registry-source.js +1 -131
- package/executors/gateway/rollout-ops.js +2 -167
- package/executors/gateway/runtime/auth.js +1 -64
- package/executors/gateway/runtime/chooser.js +12 -45
- package/executors/gateway/runtime/control.js +1 -128
- package/executors/gateway/runtime/dev-auth.js +1 -108
- package/executors/gateway/runtime/discovery.js +1 -123
- package/executors/gateway/runtime/edge.js +1 -47
- package/executors/gateway/runtime/handler.js +1 -183
- package/executors/gateway/runtime/hooks.js +1 -55
- package/executors/gateway/runtime/lens-children.js +1 -651
- package/executors/gateway/runtime/lifecycle.js +3 -842
- package/executors/gateway/runtime/observability.js +2 -148
- package/executors/gateway/runtime/pod-env.js +2 -89
- package/executors/gateway/runtime/proxy.js +1 -457
- package/executors/gateway/runtime/route-registry.js +1 -72
- package/executors/gateway/runtime/scope.js +1 -117
- package/executors/gateway/runtime/server.js +3 -487
- package/executors/gateway/runtime/service-keys.js +1 -49
- package/executors/gateway/runtime/types.js +1 -151
- package/executors/gateway/runtime/upgrade.js +1 -71
- package/executors/gateway/runtime/workspace-registry.js +1 -99
- package/executors/gateway/ssrf-guard.js +1 -190
- package/executors/serve/serve.impl.js +1 -280
- package/executors/trust/trust.impl.js +4 -162
- package/gateway.js +1 -35
- package/index.js +1 -16
- package/main.devserver.js +10 -1117
- package/package.json +4 -3
- package/tsgo-check-plugin.js +4 -364
- package/typecheck-bus.js +4 -256
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.6",
|
|
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,9 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@lensmcp/
|
|
69
|
-
"@lensmcp/
|
|
68
|
+
"@lensmcp/core": "1.18.6",
|
|
69
|
+
"@lensmcp/node-instrumentation": "1.18.6",
|
|
70
|
+
"@lensmcp/nx-plugin": "1.18.6",
|
|
70
71
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
71
72
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
72
73
|
"glob": "^11.0.0",
|
package/tsgo-check-plugin.js
CHANGED
|
@@ -1,364 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.resolveTsgoBin = resolveTsgoBin;
|
|
6
|
-
exports.reportTsgoUnavailable = reportTsgoUnavailable;
|
|
7
|
-
exports.writeShimTsconfig = writeShimTsconfig;
|
|
8
|
-
const tslib_1 = require("tslib");
|
|
9
|
-
/**
|
|
10
|
-
* TsgoCheckPlugin — dev type checking with ZERO resident memory (`LENSMCP_TYPECHECK=tsgo`).
|
|
11
|
-
*
|
|
12
|
-
* The default fork-ts-checker keeps an incremental tsc PROGRAM resident between saves — a
|
|
13
|
-
* 2-worker pair (~80MB+) per service, ~1GB across a 13-service cluster, alive even when nobody
|
|
14
|
-
* saves. tsgo (@typescript/native-preview, the native TS compiler) full-checks one of these
|
|
15
|
-
* services cold in ~1-3s, so instead of holding a program we re-check COLD after each successful
|
|
16
|
-
* rebuild (debounced, killing any in-flight check): idle cost is exactly zero.
|
|
17
|
-
*
|
|
18
|
-
* The output CONTRACT is the point: `--pretty false` prints classic `file.ts(1,2): error TS1234:`
|
|
19
|
-
* lines to the executor's console, which the gateway sniffs (`error TS\d+`) into the lens
|
|
20
|
-
* "service error" signal — the same wire the fork-ts-checker fed. Advisory like
|
|
21
|
-
* `typeCheckOptions.async: true`: a type error never fails or blocks the webpack build.
|
|
22
|
-
*
|
|
23
|
-
* SHIM tsconfig: tsgo is TS7 — `moduleResolution: node10` (what node-app tsconfigs often pin) is
|
|
24
|
-
* REMOVED there, so the check runs against a generated shim extending the app tsconfig with
|
|
25
|
-
* `{ module: 'preserve', moduleResolution: 'bundler', isolatedModules: false }`. `extends` is an
|
|
26
|
-
* ABSOLUTE path and tsconfig path-options resolve relative to the file they ORIGINATE in, so the
|
|
27
|
-
* shim lives in the workspace's git-ignored `tmp/` (in-tree, so @types resolution still walks up to
|
|
28
|
-
* the workspace node_modules — see writeShimTsconfig). `isolatedModules: false` because
|
|
29
|
-
* `module: preserve` + `emitDecoratorMetadata` otherwise flags TS1272 on decorated signatures —
|
|
30
|
-
* an emit-safety rule that doesn't apply here (this checker never emits; webpack owns emit).
|
|
31
|
-
* Parity was verified per adopting workspace (0-error parity with tsc on all spot-checked apps);
|
|
32
|
-
* tsgo is a PREVIEW compiler, which is why this MODE is opt-in.
|
|
33
|
-
*
|
|
34
|
-
* OWNERSHIP: `@lensmcp/cluster` DEPENDS on `@typescript/native-preview` — this is our feature, so we
|
|
35
|
-
* ship the compiler that powers it rather than asking every consumer to remember a devDep. That was
|
|
36
|
-
* the old contract, and it made the feature (and, once this plugin became the `typecheck://`
|
|
37
|
-
* producer, the whole typecheck health signal) die quietly in any workspace that forgot. Resolution
|
|
38
|
-
* still PREFERS a workspace's own copy (a workspace pinning a tsgo version keeps winning), and a
|
|
39
|
-
* missing binary is now an ANOMALY: it degrades gracefully — never crashing the dev server — but it
|
|
40
|
-
* reports itself onto the lens bus instead of only warning into a log nobody reads.
|
|
41
|
-
*/
|
|
42
|
-
const node_child_process_1 = require("node:child_process");
|
|
43
|
-
const fs = tslib_1.__importStar(require("node:fs"));
|
|
44
|
-
const path = tslib_1.__importStar(require("node:path"));
|
|
45
|
-
const typecheck_bus_1 = require("./typecheck-bus");
|
|
46
|
-
/** The npm package that ships the tsgo compiler — a real `dependency` of `@lensmcp/cluster`. */
|
|
47
|
-
exports.TSGO_PACKAGE = '@typescript/native-preview';
|
|
48
|
-
/**
|
|
49
|
-
* The spawnable shim names inside a `node_modules/.bin`.
|
|
50
|
-
*
|
|
51
|
-
* On win32 the only shims `child_process.spawn` can execute directly are `tsgo.cmd` / `tsgo.exe`;
|
|
52
|
-
* the EXTENSIONLESS `tsgo` npm writes beside them is a POSIX shell script for git-bash, and
|
|
53
|
-
* spawning it without a shell fails. So win32 never considers the bare name.
|
|
54
|
-
*/
|
|
55
|
-
function shimNames(platform) {
|
|
56
|
-
return platform === 'win32' ? ['tsgo.cmd', 'tsgo.exe'] : ['tsgo'];
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Existence is not enough — a directory, a dangling symlink, or a non-`+x` file all "exist" and
|
|
60
|
-
* then fail at spawn time as an opaque EACCES/ENOENT mid-serve. Probe once, here.
|
|
61
|
-
* `statSync` follows symlinks on purpose (`.bin/tsgo` IS one).
|
|
62
|
-
*/
|
|
63
|
-
function isExecutableFile(candidate, platform) {
|
|
64
|
-
try {
|
|
65
|
-
if (!fs.statSync(candidate).isFile())
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return false; // missing, or a dangling symlink
|
|
70
|
-
}
|
|
71
|
-
// X_OK is meaningless on win32 (it reports every readable file executable), so the extension
|
|
72
|
-
// check in `shimNames` is the real gate there.
|
|
73
|
-
if (platform === 'win32')
|
|
74
|
-
return true;
|
|
75
|
-
try {
|
|
76
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Where OUR copy of {@link TSGO_PACKAGE} lives, or null when it isn't installed.
|
|
85
|
-
*
|
|
86
|
-
* `require.resolve` walks up from THIS module, so it finds the copy installed for
|
|
87
|
-
* `@lensmcp/cluster` whether the consumer's installer HOISTED it
|
|
88
|
-
* (`<root>/node_modules/@typescript/native-preview`) or NESTED it
|
|
89
|
-
* (`<root>/node_modules/@lensmcp/cluster/node_modules/@typescript/native-preview`, the shape you
|
|
90
|
-
* get on a version conflict) or used pnpm's virtual store. A hand-written `../../node_modules/…`
|
|
91
|
-
* relative guess gets exactly one of those right.
|
|
92
|
-
*/
|
|
93
|
-
function resolveTsgoPackageDir() {
|
|
94
|
-
try {
|
|
95
|
-
// The package maps `"./package.json"` in its `exports`, which is the cheap, exact answer.
|
|
96
|
-
return path.dirname(require.resolve(`${exports.TSGO_PACKAGE}/package.json`));
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
/* an `exports` map without ./package.json — fall through to the entry-point walk */
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
let dir = path.dirname(require.resolve(exports.TSGO_PACKAGE));
|
|
103
|
-
// The entry may sit a few levels below the package root (`<pkg>/lib/version.cjs`), so walk up
|
|
104
|
-
// to the manifest that actually NAMES the package rather than assuming a depth.
|
|
105
|
-
for (let i = 0; i < 6; i++) {
|
|
106
|
-
const manifest = path.join(dir, 'package.json');
|
|
107
|
-
if (fs.existsSync(manifest)) {
|
|
108
|
-
try {
|
|
109
|
-
const { name } = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
110
|
-
if (name === exports.TSGO_PACKAGE)
|
|
111
|
-
return dir;
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
/* unreadable manifest — keep walking */
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const parent = path.dirname(dir);
|
|
118
|
-
if (parent === dir)
|
|
119
|
-
break;
|
|
120
|
-
dir = parent;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
/* not installed */
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
/** `bin.tsgo` (or a string `bin`) from a package dir, as an absolute path. */
|
|
129
|
-
function packageDeclaredBin(pkgDir) {
|
|
130
|
-
try {
|
|
131
|
-
const { bin } = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
|
|
132
|
-
const rel = typeof bin === 'string' ? bin : bin?.['tsgo'];
|
|
133
|
-
return rel ? path.join(pkgDir, rel) : null;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/** Candidate binaries for an installed {@link TSGO_PACKAGE}, best first. */
|
|
140
|
-
function ownBinCandidates(pkgDir, platform) {
|
|
141
|
-
// `<…>/node_modules/@typescript/native-preview` ⇒ `<…>/node_modules/.bin` — two levels up holds
|
|
142
|
-
// for a hoisted install, a nested one, and pnpm's store alike (a scoped package is always
|
|
143
|
-
// `<node_modules>/<scope>/<name>`).
|
|
144
|
-
const binDir = path.resolve(pkgDir, '..', '..', '.bin');
|
|
145
|
-
const shims = shimNames(platform).map((n) => path.join(binDir, n));
|
|
146
|
-
// The package's OWN `bin/tsgo` is present whenever the package is, regardless of whether the
|
|
147
|
-
// installer wrote shims — but it is a `#!/usr/bin/env node` script, so it is directly spawnable
|
|
148
|
-
// on POSIX only. On win32 the `.cmd`/`.exe` shims are the only usable entry.
|
|
149
|
-
const declared = platform === 'win32' ? null : packageDeclaredBin(pkgDir);
|
|
150
|
-
return declared ? [declared, ...shims] : shims;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* The tsgo binary to run, or null when none is usable.
|
|
154
|
-
*
|
|
155
|
-
* Precedence — first usable wins:
|
|
156
|
-
* 1. `override` (LENSMCP_TSGO_BIN). Deliberately NOT a fallthrough: an override pointing at a
|
|
157
|
-
* missing/non-executable file is an operator mistake to SURFACE, not to paper over by silently
|
|
158
|
-
* running a different compiler than the one that was asked for.
|
|
159
|
-
* 2. The consuming workspace's `node_modules/.bin/tsgo` — a workspace that pins its own tsgo
|
|
160
|
-
* version still beats the one we ship.
|
|
161
|
-
* 3. OURS, via {@link resolveTsgoPackageDir}. `@lensmcp/cluster` depends on {@link TSGO_PACKAGE},
|
|
162
|
-
* so this is the branch that normally answers, and the reason the feature no longer depends on
|
|
163
|
-
* every consumer independently remembering a devDep.
|
|
164
|
-
*/
|
|
165
|
-
function resolveTsgoBin(workspaceRoot, override, deps = {}) {
|
|
166
|
-
const platform = deps.platform ?? process.platform;
|
|
167
|
-
const isExecutable = deps.isExecutable ?? ((c) => isExecutableFile(c, platform));
|
|
168
|
-
if (override)
|
|
169
|
-
return isExecutable(override) ? override : null;
|
|
170
|
-
for (const name of shimNames(platform)) {
|
|
171
|
-
const candidate = path.join(workspaceRoot, 'node_modules', '.bin', name);
|
|
172
|
-
if (isExecutable(candidate))
|
|
173
|
-
return candidate;
|
|
174
|
-
}
|
|
175
|
-
const pkgDir = (deps.ownPackageDir ?? resolveTsgoPackageDir)();
|
|
176
|
-
if (!pkgDir)
|
|
177
|
-
return null;
|
|
178
|
-
return ownBinCandidates(pkgDir, platform).find(isExecutable) ?? null;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Why tsgo could not run, and what to do — the CAUSE half, shared by both sites that can hit it.
|
|
182
|
-
*
|
|
183
|
-
* Phrased for the new ownership: `@lensmcp/cluster` DEPENDS on the compiler, so "not installed" is
|
|
184
|
-
* no longer an expected opt-out state, it is a broken install (or an unsupported platform, or a bad
|
|
185
|
-
* override). Callers append the CONSEQUENCE, which differs by site.
|
|
186
|
-
*/
|
|
187
|
-
exports.TSGO_MISSING_CAUSE = `tsgo binary not found — @lensmcp/cluster depends on ${exports.TSGO_PACKAGE}, so an absent binary means an ` +
|
|
188
|
-
`incomplete install (reinstall node_modules), a platform with no ${exports.TSGO_PACKAGE} build, or a ` +
|
|
189
|
-
`LENSMCP_TSGO_BIN override pointing at a missing or non-executable file`;
|
|
190
|
-
/**
|
|
191
|
-
* Report — onto the LENS BUS, not just the console — that tsgo cannot run.
|
|
192
|
-
*
|
|
193
|
-
* A `console.warn` is invisible to anything reading `typecheck://current`, which is precisely the
|
|
194
|
-
* failure this whole area exists to eliminate: a resource that looks healthy because nobody is
|
|
195
|
-
* watching. Published as a `toolError` run, so the reducer records the producer as reporting with
|
|
196
|
-
* `lastRunStatus: 'error'` — `status` degrades to `unknown` (never a false `clean`) and `detail`
|
|
197
|
-
* names the cause and the fix.
|
|
198
|
-
*
|
|
199
|
-
* Strictly passive: fully swallowed, never blocks or fails the host build.
|
|
200
|
-
*/
|
|
201
|
-
function reportTsgoUnavailable(args) {
|
|
202
|
-
try {
|
|
203
|
-
(0, typecheck_bus_1.publishTypecheckRun)({
|
|
204
|
-
workspaceRoot: args.workspaceRoot,
|
|
205
|
-
project: args.appName,
|
|
206
|
-
tool: 'tsgo',
|
|
207
|
-
tsConfig: args.tsConfig,
|
|
208
|
-
output: '',
|
|
209
|
-
durationMs: 0,
|
|
210
|
-
toolError: args.detail,
|
|
211
|
-
...(args.eventFile ? { eventFile: args.eventFile } : {}),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
/* never let instrumentation disturb a build */
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Write the shim tsconfig (absolute `extends`; see the header) and return its path. It lives under
|
|
220
|
-
* the WORKSPACE's git-ignored `tmp/` — NOT os.tmpdir(): `types: ['node']` (and every other @types
|
|
221
|
-
* lookup) resolves typeRoots by walking node_modules UP FROM THE CONFIG FILE's directory, so a shim
|
|
222
|
-
* outside the tree fails every service with `TS2688: Cannot find type definition file for 'node'`.
|
|
223
|
-
*/
|
|
224
|
-
function writeShimTsconfig(appName, tsConfig, workspaceRoot) {
|
|
225
|
-
const dir = path.join(workspaceRoot, 'tmp', 'lensmcp-tsgo');
|
|
226
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
227
|
-
const file = path.join(dir, `${appName}.tsconfig.json`);
|
|
228
|
-
fs.writeFileSync(file, JSON.stringify({
|
|
229
|
-
extends: tsConfig,
|
|
230
|
-
// baseUrl: null UNSETS an inherited baseUrl — TS7 removed the option outright (TS5102), and
|
|
231
|
-
// a workspace that anchors its inherited `paths` with baseUrl still resolves them fine
|
|
232
|
-
// without it: TS7 anchors non-relative `paths` at the config file that DECLARES them (the
|
|
233
|
-
// workspace-root base tsconfig — the same directory baseUrl pointed at). A no-op where
|
|
234
|
-
// baseUrl was never set.
|
|
235
|
-
compilerOptions: {
|
|
236
|
-
module: 'preserve',
|
|
237
|
-
moduleResolution: 'bundler',
|
|
238
|
-
isolatedModules: false,
|
|
239
|
-
baseUrl: null,
|
|
240
|
-
},
|
|
241
|
-
}, null, 2));
|
|
242
|
-
return file;
|
|
243
|
-
}
|
|
244
|
-
class TsgoCheckPlugin {
|
|
245
|
-
constructor(opts) {
|
|
246
|
-
this.timer = null;
|
|
247
|
-
this.inflight = null;
|
|
248
|
-
this.shimPath = null;
|
|
249
|
-
this.disabled = false;
|
|
250
|
-
this.opts = opts;
|
|
251
|
-
this.spawnImpl = opts.spawnImpl ?? node_child_process_1.spawn;
|
|
252
|
-
}
|
|
253
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- webpack Compiler (untyped here like DevServerReloadPlugin)
|
|
254
|
-
apply(compiler) {
|
|
255
|
-
compiler.hooks.done.tap('TsgoCheckPlugin', (stats) => {
|
|
256
|
-
if (stats.hasErrors())
|
|
257
|
-
return; // webpack already printed build errors — don't pile on
|
|
258
|
-
this.schedule();
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
/** Debounced: rapid successive rebuilds collapse into one check; an in-flight check is superseded. */
|
|
262
|
-
schedule() {
|
|
263
|
-
if (this.disabled)
|
|
264
|
-
return;
|
|
265
|
-
if (this.timer)
|
|
266
|
-
clearTimeout(this.timer);
|
|
267
|
-
this.timer = setTimeout(() => this.run(), this.opts.debounceMs ?? 500);
|
|
268
|
-
this.timer.unref?.();
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Publish one completed check onto the lens bus so `typecheck://*` reflects
|
|
272
|
-
* it. Strictly passive + fully swallowed: the console output above is the
|
|
273
|
-
* contract webpack/the gateway rely on, this is pure observation on top.
|
|
274
|
-
*/
|
|
275
|
-
publish(args) {
|
|
276
|
-
if (this.opts.publishEvents === false)
|
|
277
|
-
return;
|
|
278
|
-
try {
|
|
279
|
-
(0, typecheck_bus_1.publishTypecheckRun)({
|
|
280
|
-
workspaceRoot: this.opts.workspaceRoot,
|
|
281
|
-
project: this.opts.appName,
|
|
282
|
-
tool: 'tsgo',
|
|
283
|
-
tsConfig: this.opts.tsConfig,
|
|
284
|
-
output: args.output,
|
|
285
|
-
durationMs: args.durationMs,
|
|
286
|
-
...(args.toolError ? { toolError: args.toolError } : {}),
|
|
287
|
-
...(this.opts.eventFile ? { eventFile: this.opts.eventFile } : {}),
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
/* never let instrumentation disturb a build */
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
run() {
|
|
295
|
-
const bin = resolveTsgoBin(this.opts.workspaceRoot, this.opts.binPath);
|
|
296
|
-
if (!bin) {
|
|
297
|
-
// The binary vanished after config time (e.g. a reinstall mid-serve) — warn once, go quiet.
|
|
298
|
-
this.disabled = true;
|
|
299
|
-
// No trailing period: `summariseProducers` renders this INSIDE a sentence it terminates itself.
|
|
300
|
-
const detail = `${exports.TSGO_MISSING_CAUSE}. Type checking is disabled for the rest of this session`;
|
|
301
|
-
console.warn(`[tsgo] ${this.opts.appName}: ${detail}.`);
|
|
302
|
-
// A checker that stopped checking must SAY so: without this the resource
|
|
303
|
-
// would keep serving the last (possibly clean) verdict forever, which is
|
|
304
|
-
// exactly the "looks healthy, nobody is watching" state this work exists
|
|
305
|
-
// to make impossible.
|
|
306
|
-
this.publish({ output: '', durationMs: 0, toolError: detail });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (this.inflight) {
|
|
310
|
-
this.inflight.kill('SIGTERM'); // superseded by a newer build
|
|
311
|
-
this.inflight = null;
|
|
312
|
-
}
|
|
313
|
-
if (!this.shimPath)
|
|
314
|
-
this.shimPath = writeShimTsconfig(this.opts.appName, this.opts.tsConfig, this.opts.workspaceRoot);
|
|
315
|
-
const startedAt = Date.now();
|
|
316
|
-
const child = this.spawnImpl(bin, ['--noEmit', '--pretty', 'false', '-p', this.shimPath], {
|
|
317
|
-
cwd: this.opts.workspaceRoot,
|
|
318
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
319
|
-
});
|
|
320
|
-
this.inflight = child;
|
|
321
|
-
let out = '';
|
|
322
|
-
child.stdout?.on('data', (d) => { out += d.toString(); });
|
|
323
|
-
child.stderr?.on('data', (d) => { out += d.toString(); });
|
|
324
|
-
child.on('error', () => {
|
|
325
|
-
if (this.inflight === child)
|
|
326
|
-
this.inflight = null;
|
|
327
|
-
console.warn(`[tsgo] ${this.opts.appName}: failed to spawn ${bin}`);
|
|
328
|
-
// A spawn failure is NOT a clean check — report it as a tool error so the
|
|
329
|
-
// resource says "the checker died", never "your types are fine".
|
|
330
|
-
this.publish({ output: '', durationMs: Date.now() - startedAt, toolError: `failed to spawn ${bin}` });
|
|
331
|
-
});
|
|
332
|
-
child.on('close', (code, signal) => {
|
|
333
|
-
if (this.inflight === child)
|
|
334
|
-
this.inflight = null;
|
|
335
|
-
if (signal)
|
|
336
|
-
return; // superseded — a newer check owns the verdict
|
|
337
|
-
const durationMs = Date.now() - startedAt;
|
|
338
|
-
const secs = (durationMs / 1000).toFixed(1);
|
|
339
|
-
if (code === 0) {
|
|
340
|
-
console.log(`[tsgo] ${this.opts.appName}: types clean (${secs}s)`);
|
|
341
|
-
this.publish({ output: '', durationMs });
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
// Print the raw `file.ts(1,2): error TSxxxx:` lines — the gateway's `error TS\d+` sniff
|
|
345
|
-
// turns these into the lens service-error signal, exactly like fork-ts-checker output did.
|
|
346
|
-
if (out.trim())
|
|
347
|
-
process.stdout.write(out.endsWith('\n') ? out : `${out}\n`);
|
|
348
|
-
const count = (out.match(/error TS\d+/g) ?? []).length;
|
|
349
|
-
console.log(`[tsgo] ${this.opts.appName}: ${count} type error${count === 1 ? '' : 's'} (${secs}s)`);
|
|
350
|
-
// A non-zero exit with NO parseable diagnostic is the checker itself
|
|
351
|
-
// failing (bad shim tsconfig, OOM) — `publishTypecheckRun` would
|
|
352
|
-
// otherwise read "0 errors" and report CLEAN on a failed run.
|
|
353
|
-
this.publish({
|
|
354
|
-
output: out,
|
|
355
|
-
durationMs,
|
|
356
|
-
...(count === 0 ? { toolError: `tsgo exited ${code} with no diagnostics: ${firstLine(out)}` } : {}),
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
exports.TsgoCheckPlugin = TsgoCheckPlugin;
|
|
362
|
-
function firstLine(s) {
|
|
363
|
-
return s.trim().split('\n')[0]?.slice(0, 300) ?? '';
|
|
364
|
-
}
|
|
1
|
+
"use strict";var d=Object.defineProperty;var l=(s,t)=>d(s,"name",{value:t,configurable:!0});var u=Object.defineProperty,r=l((s,t)=>u(s,"name",{value:t,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.TsgoCheckPlugin=exports.TSGO_MISSING_CAUSE=exports.TSGO_PACKAGE=void 0,exports.resolveTsgoPackageDir=resolveTsgoPackageDir,exports.resolveTsgoBin=resolveTsgoBin,exports.reportTsgoUnavailable=reportTsgoUnavailable,exports.writeShimTsconfig=writeShimTsconfig;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),typecheck_bus_1=require("./typecheck-bus");exports.TSGO_PACKAGE="@typescript/native-preview";function shimNames(s){return s==="win32"?["tsgo.cmd","tsgo.exe"]:["tsgo"]}l(shimNames,"shimNames"),r(shimNames,"shimNames");function isExecutableFile(s,t){try{if(!fs.statSync(s).isFile())return!1}catch{return!1}if(t==="win32")return!0;try{return fs.accessSync(s,fs.constants.X_OK),!0}catch{return!1}}l(isExecutableFile,"isExecutableFile"),r(isExecutableFile,"isExecutableFile");function resolveTsgoPackageDir(){try{return path.dirname(require.resolve(`${exports.TSGO_PACKAGE}/package.json`))}catch{}try{let s=path.dirname(require.resolve(exports.TSGO_PACKAGE));for(let t=0;t<6;t++){const i=path.join(s,"package.json");if(fs.existsSync(i))try{const{name:e}=JSON.parse(fs.readFileSync(i,"utf8"));if(e===exports.TSGO_PACKAGE)return s}catch{}const o=path.dirname(s);if(o===s)break;s=o}}catch{}return null}l(resolveTsgoPackageDir,"resolveTsgoPackageDir"),r(resolveTsgoPackageDir,"resolveTsgoPackageDir");function packageDeclaredBin(s){try{const{bin:t}=JSON.parse(fs.readFileSync(path.join(s,"package.json"),"utf8")),i=typeof t=="string"?t:t?.tsgo;return i?path.join(s,i):null}catch{return null}}l(packageDeclaredBin,"packageDeclaredBin"),r(packageDeclaredBin,"packageDeclaredBin");function ownBinCandidates(s,t){const i=path.resolve(s,"..","..",".bin"),o=shimNames(t).map(n=>path.join(i,n)),e=t==="win32"?null:packageDeclaredBin(s);return e?[e,...o]:o}l(ownBinCandidates,"ownBinCandidates"),r(ownBinCandidates,"ownBinCandidates");function resolveTsgoBin(s,t,i={}){const o=i.platform??process.platform,e=i.isExecutable??(p=>isExecutableFile(p,o));if(t)return e(t)?t:null;for(const p of shimNames(o)){const a=path.join(s,"node_modules",".bin",p);if(e(a))return a}const n=(i.ownPackageDir??resolveTsgoPackageDir)();return n?ownBinCandidates(n,o).find(e)??null:null}l(resolveTsgoBin,"resolveTsgoBin"),r(resolveTsgoBin,"resolveTsgoBin"),exports.TSGO_MISSING_CAUSE=`tsgo binary not found \u2014 @lensmcp/cluster depends on ${exports.TSGO_PACKAGE}, so an absent binary means an incomplete install (reinstall node_modules), a platform with no ${exports.TSGO_PACKAGE} build, or a LENSMCP_TSGO_BIN override pointing at a missing or non-executable file`;function reportTsgoUnavailable(s){try{(0,typecheck_bus_1.publishTypecheckRun)({workspaceRoot:s.workspaceRoot,project:s.appName,tool:"tsgo",tsConfig:s.tsConfig,output:"",durationMs:0,toolError:s.detail,...s.eventFile?{eventFile:s.eventFile}:{}})}catch{}}l(reportTsgoUnavailable,"reportTsgoUnavailable"),r(reportTsgoUnavailable,"reportTsgoUnavailable");function writeShimTsconfig(s,t,i){const o=path.join(i,"tmp","lensmcp-tsgo");fs.mkdirSync(o,{recursive:!0});const e=path.join(o,`${s}.tsconfig.json`);return fs.writeFileSync(e,JSON.stringify({extends:t,compilerOptions:{module:"preserve",moduleResolution:"bundler",isolatedModules:!1,baseUrl:null}},null,2)),e}l(writeShimTsconfig,"writeShimTsconfig"),r(writeShimTsconfig,"writeShimTsconfig");class TsgoCheckPlugin{static{l(this,"TsgoCheckPlugin")}static{r(this,"TsgoCheckPlugin")}constructor(t){this.timer=null,this.inflight=null,this.shimPath=null,this.disabled=!1,this.opts=t,this.spawnImpl=t.spawnImpl??node_child_process_1.spawn}apply(t){t.hooks.done.tap("TsgoCheckPlugin",i=>{i.hasErrors()||this.schedule()})}schedule(){this.disabled||(this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>this.run(),this.opts.debounceMs??500),this.timer.unref?.())}publish(t){if(this.opts.publishEvents!==!1)try{(0,typecheck_bus_1.publishTypecheckRun)({workspaceRoot:this.opts.workspaceRoot,project:this.opts.appName,tool:"tsgo",tsConfig:this.opts.tsConfig,output:t.output,durationMs:t.durationMs,...t.toolError?{toolError:t.toolError}:{},...this.opts.eventFile?{eventFile:this.opts.eventFile}:{}})}catch{}}run(){const t=resolveTsgoBin(this.opts.workspaceRoot,this.opts.binPath);if(!t){this.disabled=!0;const n=`${exports.TSGO_MISSING_CAUSE}. Type checking is disabled for the rest of this session`;console.warn(`[tsgo] ${this.opts.appName}: ${n}.`),this.publish({output:"",durationMs:0,toolError:n});return}this.inflight&&(this.inflight.kill("SIGTERM"),this.inflight=null),this.shimPath||(this.shimPath=writeShimTsconfig(this.opts.appName,this.opts.tsConfig,this.opts.workspaceRoot));const i=Date.now(),o=this.spawnImpl(t,["--noEmit","--pretty","false","-p",this.shimPath],{cwd:this.opts.workspaceRoot,stdio:["ignore","pipe","pipe"]});this.inflight=o;let e="";o.stdout?.on("data",n=>{e+=n.toString()}),o.stderr?.on("data",n=>{e+=n.toString()}),o.on("error",()=>{this.inflight===o&&(this.inflight=null),console.warn(`[tsgo] ${this.opts.appName}: failed to spawn ${t}`),this.publish({output:"",durationMs:Date.now()-i,toolError:`failed to spawn ${t}`})}),o.on("close",(n,p)=>{if(this.inflight===o&&(this.inflight=null),p)return;const a=Date.now()-i,h=(a/1e3).toFixed(1);if(n===0){console.log(`[tsgo] ${this.opts.appName}: types clean (${h}s)`),this.publish({output:"",durationMs:a});return}e.trim()&&process.stdout.write(e.endsWith(`
|
|
2
|
+
`)?e:`${e}
|
|
3
|
+
`);const c=(e.match(/error TS\d+/g)??[]).length;console.log(`[tsgo] ${this.opts.appName}: ${c} type error${c===1?"":"s"} (${h}s)`),this.publish({output:e,durationMs:a,...c===0?{toolError:`tsgo exited ${n} with no diagnostics: ${firstLine(e)}`}:{}})})}}exports.TsgoCheckPlugin=TsgoCheckPlugin;function firstLine(s){return s.trim().split(`
|
|
4
|
+
`)[0]?.slice(0,300)??""}l(firstLine,"firstLine"),r(firstLine,"firstLine");
|
package/typecheck-bus.js
CHANGED
|
@@ -1,256 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.parseTscDiagnosticLine = parseTscDiagnosticLine;
|
|
6
|
-
exports.parseTscOutput = parseTscOutput;
|
|
7
|
-
exports.buildTypecheckRunEvents = buildTypecheckRunEvents;
|
|
8
|
-
exports.resolveEventFile = resolveEventFile;
|
|
9
|
-
exports.appendTypecheckEvents = appendTypecheckEvents;
|
|
10
|
-
exports.publishTypecheckRun = publishTypecheckRun;
|
|
11
|
-
const tslib_1 = require("tslib");
|
|
12
|
-
/**
|
|
13
|
-
* The tsgo checker's bus writer — turns a completed type check into the
|
|
14
|
-
* `typescript`/`typecheck` events `installTypecheckReducer` materialises into
|
|
15
|
-
* `typecheck://*`.
|
|
16
|
-
*
|
|
17
|
-
* ── The failure this fixes ──────────────────────────────────────────────────
|
|
18
|
-
* `typecheck://current` answered `{"status":"unknown","errorCount":0,
|
|
19
|
-
* "revision":0}` on a workspace whose dev cluster was type-checking on every
|
|
20
|
-
* single rebuild. `revision: 0` means no event had EVER arrived: the reducer
|
|
21
|
-
* had no producer at all. The checks were running — `[tsgo] engine: types
|
|
22
|
-
* clean (2.6s)`, and on failure real diagnostics like
|
|
23
|
-
* `…/hazard-analyst.agent.ts(52,12): error TS2416: …` — but that output went
|
|
24
|
-
* to the executor's stdout and nowhere else.
|
|
25
|
-
*
|
|
26
|
-
* The gateway's console sniff (`runtime/lifecycle.ts`, `/error TS\d+/`) does
|
|
27
|
-
* see those lines, but it emits `source:'gateway' / category:'cluster'` with
|
|
28
|
-
* the whole line stuffed verbatim into `raw.line` — no file, no position, no
|
|
29
|
-
* code, no per-project status, throttled to one line per 5s, and filtered out
|
|
30
|
-
* by all three check reducers. It is a crash-banner, not a typecheck feed.
|
|
31
|
-
*
|
|
32
|
-
* So the checker publishes directly. `TsgoCheckPlugin` runs INSIDE the nx
|
|
33
|
-
* serve executor process, which the gateway already spawns with
|
|
34
|
-
* `LENSMCP_EVENT_FILE` and `LENSMCP_PROJECT` in its env
|
|
35
|
-
* (`executors/gateway/runtime/lifecycle.ts`) — the same JSONL rendezvous the
|
|
36
|
-
* MCP server tails at 50 ms. No new transport, no new wiring.
|
|
37
|
-
*
|
|
38
|
-
* ── Why this file MIRRORS `@lensmcp/session` ────────────────────────────────
|
|
39
|
-
* The canonical parser + event builder is
|
|
40
|
-
* `libs/session/src/lib/collectors/typecheck-events.ts`. `@lensmcp/cluster` is
|
|
41
|
-
* PUBLISHED and `@lensmcp/session` is private/bundled-into-the-CLI, so cluster
|
|
42
|
-
* cannot depend on it. This is the same deliberate mirror
|
|
43
|
-
* `libs/bridge/src/lib/env-sink.ts` makes of node-instrumentation's emit
|
|
44
|
-
* contract. Drift is pinned by `TSC_PARITY_FIXTURE`, asserted byte-for-byte in
|
|
45
|
-
* BOTH projects' specs — change one parser without the other and a test fails.
|
|
46
|
-
*
|
|
47
|
-
* Strictly passive: every write is best-effort inside try/catch, the host
|
|
48
|
-
* build is never blocked, delayed, or failed by it.
|
|
49
|
-
*/
|
|
50
|
-
const fs = tslib_1.__importStar(require("node:fs"));
|
|
51
|
-
const path = tslib_1.__importStar(require("node:path"));
|
|
52
|
-
const busHygiene = (() => {
|
|
53
|
-
try {
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
55
|
-
return require('@lensmcp/node-instrumentation');
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
})();
|
|
61
|
-
/** MIRROR of session's regex — `path(line,col): error TS1234: message`. */
|
|
62
|
-
const DIAG_LINE_REGEX = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/;
|
|
63
|
-
// CSI/ANSI escape, built from String.fromCharCode so the literal control char
|
|
64
|
-
// never appears in source (no-control-regex).
|
|
65
|
-
const ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, 'g');
|
|
66
|
-
function stripAnsi(s) {
|
|
67
|
-
return s.replace(ANSI_CSI, '');
|
|
68
|
-
}
|
|
69
|
-
/** MIRROR of session's `parseTscDiagnosticLine`. */
|
|
70
|
-
function parseTscDiagnosticLine(raw) {
|
|
71
|
-
const line = stripAnsi(raw).trim();
|
|
72
|
-
if (!line)
|
|
73
|
-
return null;
|
|
74
|
-
const m = DIAG_LINE_REGEX.exec(line);
|
|
75
|
-
if (!m)
|
|
76
|
-
return null;
|
|
77
|
-
const [, file, lineStr, colStr, severity, code, message] = m;
|
|
78
|
-
if (!file || !lineStr || !colStr || !severity || !code || !message)
|
|
79
|
-
return null;
|
|
80
|
-
return {
|
|
81
|
-
file,
|
|
82
|
-
line: Number(lineStr),
|
|
83
|
-
column: Number(colStr),
|
|
84
|
-
severity: severity === 'warning' ? 'warning' : 'error',
|
|
85
|
-
code,
|
|
86
|
-
message,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
/** MIRROR of session's `parseTscOutput`. */
|
|
90
|
-
function parseTscOutput(output) {
|
|
91
|
-
const out = [];
|
|
92
|
-
for (const line of output.split('\n')) {
|
|
93
|
-
const d = parseTscDiagnosticLine(line);
|
|
94
|
-
if (d)
|
|
95
|
-
out.push(d);
|
|
96
|
-
}
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
99
|
-
/** MIRROR of session's cap — a broken tsconfig can cascade into thousands. */
|
|
100
|
-
exports.MAX_DIAGNOSTICS_PER_RUN = 200;
|
|
101
|
-
let idCounter = 0;
|
|
102
|
-
function makeId() {
|
|
103
|
-
idCounter = (idCounter + 1) % 0xffff;
|
|
104
|
-
return `${Date.now().toString(36)}${idCounter.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Build the ordered event list for one completed check.
|
|
108
|
-
* MIRROR of session's `buildTypecheckRunEvents` — see it for the protocol
|
|
109
|
-
* (START carries the legacy `tsc clear` prefix an older reducer resets on;
|
|
110
|
-
* END deliberately does not, or it would wipe what was just published).
|
|
111
|
-
*/
|
|
112
|
-
function buildTypecheckRunEvents(args) {
|
|
113
|
-
const at = (args.now ?? Date.now)();
|
|
114
|
-
const id = args.idImpl ?? makeId;
|
|
115
|
-
const sessionId = 'pending'; // stamped by the ingesting session, as every file-bus producer does
|
|
116
|
-
const base = {
|
|
117
|
-
sessionId,
|
|
118
|
-
source: 'typescript',
|
|
119
|
-
category: 'typecheck',
|
|
120
|
-
context: { sessionId, projectName: args.project },
|
|
121
|
-
};
|
|
122
|
-
const rawBase = {
|
|
123
|
-
project: args.project,
|
|
124
|
-
tool: args.tool,
|
|
125
|
-
...(args.tsConfig ? { tsConfig: args.tsConfig } : {}),
|
|
126
|
-
};
|
|
127
|
-
const errors = args.diagnostics.filter((d) => d.severity === 'error');
|
|
128
|
-
const forwarded = args.diagnostics.slice(0, exports.MAX_DIAGNOSTICS_PER_RUN);
|
|
129
|
-
const events = [];
|
|
130
|
-
// A run that produced NO verdict emits NO clear: wiping the previous state
|
|
131
|
-
// and replacing it with zero diagnostics would read as a clean check.
|
|
132
|
-
if (!args.toolError) {
|
|
133
|
-
events.push({
|
|
134
|
-
...base,
|
|
135
|
-
id: id(),
|
|
136
|
-
timestamp: at,
|
|
137
|
-
severity: 'info',
|
|
138
|
-
title: `tsc clear: ${args.project}`,
|
|
139
|
-
message: `typecheck run starting for ${args.project}`,
|
|
140
|
-
fingerprint: `tsc:${args.project}:run-start`,
|
|
141
|
-
raw: { ...rawBase, kind: 'typecheck-run', phase: 'start' },
|
|
142
|
-
});
|
|
143
|
-
for (const d of forwarded) {
|
|
144
|
-
events.push({
|
|
145
|
-
...base,
|
|
146
|
-
id: id(),
|
|
147
|
-
timestamp: at,
|
|
148
|
-
severity: d.severity,
|
|
149
|
-
title: `${d.code}: ${d.message}`,
|
|
150
|
-
message: d.message,
|
|
151
|
-
location: { file: d.file, line: d.line, column: d.column },
|
|
152
|
-
// MIRROR of session's `diagnosticFingerprint` — position included so N
|
|
153
|
-
// hits of one code in one file stay N, not 1.
|
|
154
|
-
fingerprint: `tsc:${args.project}:${d.file}:${d.line}:${d.column}:${d.code}`,
|
|
155
|
-
raw: { ...rawBase, kind: 'typecheck-diagnostic', code: d.code },
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const status = args.toolError ? 'error' : errors.length > 0 ? 'failing' : 'clean';
|
|
160
|
-
events.push({
|
|
161
|
-
...base,
|
|
162
|
-
id: id(),
|
|
163
|
-
timestamp: at,
|
|
164
|
-
severity: 'info',
|
|
165
|
-
title: `typecheck run: ${args.project} — ${args.toolError ? 'tool error' : `${errors.length} error${errors.length === 1 ? '' : 's'}`} (${args.durationMs}ms)`,
|
|
166
|
-
...(args.toolError ? { message: args.toolError } : {}),
|
|
167
|
-
fingerprint: `tsc:${args.project}:run-end`,
|
|
168
|
-
raw: {
|
|
169
|
-
...rawBase,
|
|
170
|
-
kind: 'typecheck-run',
|
|
171
|
-
phase: 'end',
|
|
172
|
-
status,
|
|
173
|
-
durationMs: args.durationMs,
|
|
174
|
-
errorCount: errors.length,
|
|
175
|
-
warningCount: args.diagnostics.length - errors.length,
|
|
176
|
-
...(args.diagnostics.length > forwarded.length
|
|
177
|
-
? { truncatedDiagnostics: args.diagnostics.length - forwarded.length }
|
|
178
|
-
: {}),
|
|
179
|
-
...(args.toolError ? { error: args.toolError } : {}),
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
return events;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Where the bus lives for this process. Same resolution order the serve
|
|
186
|
-
* executor uses for the pod env, so the checker and the pods it type-checks
|
|
187
|
-
* always land on ONE file.
|
|
188
|
-
*/
|
|
189
|
-
function resolveEventFile(workspaceRoot) {
|
|
190
|
-
return process.env['LENSMCP_EVENT_FILE'] || path.join(workspaceRoot, '.lensmcp', 'events.jsonl');
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Last directory we successfully created, so the common case costs no syscall.
|
|
194
|
-
*
|
|
195
|
-
* Deliberately NOT a boolean: a `dirReady` flag latches TRUE for the life of
|
|
196
|
-
* the process, so if `.lensmcp/` is later removed (a clean, a `rm -rf tmp`, a
|
|
197
|
-
* different bus path) every subsequent append fails ENOENT and the producer
|
|
198
|
-
* goes silent FOREVER — reporting nothing, which is the precise failure mode
|
|
199
|
-
* this whole file exists to make impossible. Keyed by path + re-ensured on
|
|
200
|
-
* ENOENT instead.
|
|
201
|
-
*/
|
|
202
|
-
let ensuredDir = null;
|
|
203
|
-
function ensureDir(eventFile) {
|
|
204
|
-
const dir = path.dirname(eventFile);
|
|
205
|
-
if (ensuredDir === dir)
|
|
206
|
-
return;
|
|
207
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
208
|
-
ensuredDir = dir;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Append events as NDJSON, honouring the shared rotation contract
|
|
212
|
-
* (`maybeRotateEventFile` is exported from node-instrumentation precisely so
|
|
213
|
-
* every OTHER writer to the bus enforces the same caps — the gateway once was
|
|
214
|
-
* the one writer without it, and the bus outgrew every consumer).
|
|
215
|
-
*
|
|
216
|
-
* Never throws: a lens that cannot write must not break a build.
|
|
217
|
-
*/
|
|
218
|
-
function appendTypecheckEvents(eventFile, events) {
|
|
219
|
-
if (events.length === 0)
|
|
220
|
-
return;
|
|
221
|
-
const payload = events.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
222
|
-
try {
|
|
223
|
-
ensureDir(eventFile);
|
|
224
|
-
busHygiene?.maybeRotateEventFile(eventFile, payload.length);
|
|
225
|
-
fs.appendFileSync(eventFile, payload);
|
|
226
|
-
}
|
|
227
|
-
catch (err) {
|
|
228
|
-
// The directory vanished under us (clean / rotate race). Re-create it once
|
|
229
|
-
// and retry — self-healing beats going permanently quiet.
|
|
230
|
-
if (err?.code === 'ENOENT') {
|
|
231
|
-
try {
|
|
232
|
-
ensuredDir = null;
|
|
233
|
-
ensureDir(eventFile);
|
|
234
|
-
fs.appendFileSync(eventFile, payload);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
/* lens offline — never break the host build */
|
|
238
|
-
}
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
/* lens offline — never break the host build */
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
/** Parse a raw tsgo/tsc run and publish it. Returns the events written (for tests). */
|
|
245
|
-
function publishTypecheckRun(args) {
|
|
246
|
-
const events = buildTypecheckRunEvents({
|
|
247
|
-
project: args.project,
|
|
248
|
-
tool: args.tool,
|
|
249
|
-
...(args.tsConfig ? { tsConfig: args.tsConfig } : {}),
|
|
250
|
-
diagnostics: args.toolError ? [] : parseTscOutput(args.output),
|
|
251
|
-
durationMs: args.durationMs,
|
|
252
|
-
...(args.toolError ? { toolError: args.toolError } : {}),
|
|
253
|
-
});
|
|
254
|
-
appendTypecheckEvents(args.eventFile ?? resolveEventFile(args.workspaceRoot), events);
|
|
255
|
-
return events;
|
|
256
|
-
}
|
|
1
|
+
"use strict";var g=Object.defineProperty;var s=(t,e)=>g(t,"name",{value:e,configurable:!0});var f=Object.defineProperty,i=s((t,e)=>f(t,"name",{value:e,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.MAX_DIAGNOSTICS_PER_RUN=void 0,exports.stripAnsi=stripAnsi,exports.parseTscDiagnosticLine=parseTscDiagnosticLine,exports.parseTscOutput=parseTscOutput,exports.buildTypecheckRunEvents=buildTypecheckRunEvents,exports.resolveEventFile=resolveEventFile,exports.appendTypecheckEvents=appendTypecheckEvents,exports.publishTypecheckRun=publishTypecheckRun;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),busHygiene=(()=>{try{return require("@lensmcp/node-instrumentation")}catch{return}})(),DIAG_LINE_REGEX=/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/,ANSI_CSI=new RegExp("\x1B\\[[0-9;]*[A-Za-z]","g");function stripAnsi(t){return t.replace(ANSI_CSI,"")}s(stripAnsi,"stripAnsi"),i(stripAnsi,"stripAnsi");function parseTscDiagnosticLine(t){const e=stripAnsi(t).trim();if(!e)return null;const n=DIAG_LINE_REGEX.exec(e);if(!n)return null;const[,o,l,u,c,a,p]=n;return!o||!l||!u||!c||!a||!p?null:{file:o,line:Number(l),column:Number(u),severity:c==="warning"?"warning":"error",code:a,message:p}}s(parseTscDiagnosticLine,"parseTscDiagnosticLine"),i(parseTscDiagnosticLine,"parseTscDiagnosticLine");function parseTscOutput(t){const e=[];for(const n of t.split(`
|
|
2
|
+
`)){const o=parseTscDiagnosticLine(n);o&&e.push(o)}return e}s(parseTscOutput,"parseTscOutput"),i(parseTscOutput,"parseTscOutput"),exports.MAX_DIAGNOSTICS_PER_RUN=200;let idCounter=0;function makeId(){return idCounter=(idCounter+1)%65535,`${Date.now().toString(36)}${idCounter.toString(36)}${Math.random().toString(36).slice(2,8)}`}s(makeId,"makeId"),i(makeId,"makeId");function buildTypecheckRunEvents(t){const e=(t.now??Date.now)(),n=t.idImpl??makeId,o="pending",l={sessionId:o,source:"typescript",category:"typecheck",context:{sessionId:o,projectName:t.project}},u={project:t.project,tool:t.tool,...t.tsConfig?{tsConfig:t.tsConfig}:{}},c=t.diagnostics.filter(r=>r.severity==="error"),a=t.diagnostics.slice(0,exports.MAX_DIAGNOSTICS_PER_RUN),p=[];if(!t.toolError){p.push({...l,id:n(),timestamp:e,severity:"info",title:`tsc clear: ${t.project}`,message:`typecheck run starting for ${t.project}`,fingerprint:`tsc:${t.project}:run-start`,raw:{...u,kind:"typecheck-run",phase:"start"}});for(const r of a)p.push({...l,id:n(),timestamp:e,severity:r.severity,title:`${r.code}: ${r.message}`,message:r.message,location:{file:r.file,line:r.line,column:r.column},fingerprint:`tsc:${t.project}:${r.file}:${r.line}:${r.column}:${r.code}`,raw:{...u,kind:"typecheck-diagnostic",code:r.code}})}const d=t.toolError?"error":c.length>0?"failing":"clean";return p.push({...l,id:n(),timestamp:e,severity:"info",title:`typecheck run: ${t.project} \u2014 ${t.toolError?"tool error":`${c.length} error${c.length===1?"":"s"}`} (${t.durationMs}ms)`,...t.toolError?{message:t.toolError}:{},fingerprint:`tsc:${t.project}:run-end`,raw:{...u,kind:"typecheck-run",phase:"end",status:d,durationMs:t.durationMs,errorCount:c.length,warningCount:t.diagnostics.length-c.length,...t.diagnostics.length>a.length?{truncatedDiagnostics:t.diagnostics.length-a.length}:{},...t.toolError?{error:t.toolError}:{}}}),p}s(buildTypecheckRunEvents,"buildTypecheckRunEvents"),i(buildTypecheckRunEvents,"buildTypecheckRunEvents");function resolveEventFile(t){return process.env.LENSMCP_EVENT_FILE||path.join(t,".lensmcp","events.jsonl")}s(resolveEventFile,"resolveEventFile"),i(resolveEventFile,"resolveEventFile");let ensuredDir=null;function ensureDir(t){const e=path.dirname(t);ensuredDir!==e&&(fs.mkdirSync(e,{recursive:!0}),ensuredDir=e)}s(ensureDir,"ensureDir"),i(ensureDir,"ensureDir");function appendTypecheckEvents(t,e){if(e.length===0)return;const n=e.map(o=>JSON.stringify(o)).join(`
|
|
3
|
+
`)+`
|
|
4
|
+
`;try{ensureDir(t),busHygiene?.maybeRotateEventFile(t,n.length),fs.appendFileSync(t,n)}catch(o){if(o?.code==="ENOENT"){try{ensuredDir=null,ensureDir(t),fs.appendFileSync(t,n)}catch{}return}}}s(appendTypecheckEvents,"appendTypecheckEvents"),i(appendTypecheckEvents,"appendTypecheckEvents");function publishTypecheckRun(t){const e=buildTypecheckRunEvents({project:t.project,tool:t.tool,...t.tsConfig?{tsConfig:t.tsConfig}:{},diagnostics:t.toolError?[]:parseTscOutput(t.output),durationMs:t.durationMs,...t.toolError?{toolError:t.toolError}:{}});return appendTypecheckEvents(t.eventFile??resolveEventFile(t.workspaceRoot),e),e}s(publishTypecheckRun,"publishTypecheckRun"),i(publishTypecheckRun,"publishTypecheckRun");
|